dev_공부일지/JPA 에러 정리

예외처리 만들기

dev_0hoon 2024. 3. 11. 16:38

 

토이 프로젝트를 진행함에 앞서 예외처리를 근사하게(?) 좀 실무에 가까운 방식으로 만들기 위한 이야기를 적어보려한다.

 

 

1. 서블릿

 

서블릿은 다음 2가지 방식으로 예외 처리를 지원한다.

- Exception (예외)

- response.sendError(HTTP 상태 코드, 오류메시지)

 

try-catch에서 예외를 잡지 않으면 서블릿 밖으로 까지 전달되는데 어떻게 동작 될까?

이런 순으로 전파가 된다.

 

일단 기본부터 적어본다.

- 서버 내부에서 처리할 수 없는 오류 : 500

- 페이지를 찾을 수 없는 오류 : 404 (not find)

 

Exception (예외)

WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)

서블릿 밖으로 까지 예외를 던져 버린다.

1.

    @GetMapping("/error-ex")
    public void errorEx(){
        throw new RuntimeException("예외 발생");
    }

이렇게 단순 Exception을 던져버리면 500이 뜬다.

 

2. /no-page로 매핑 없는걸 찾게 되면 404가 뜬다.

 

 

response.sendError

서블릿한테 오류가 발생했다는 점을 전달할 수 있다.

이 메서드를 사용하면 HTTP 상태코드와 오류메세지를 직접 넣을 수 있다.

WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(response.sendError())

 

    @GetMapping("/error-404")
    public void error404(HttpServletResponse response) throws IOException {
        response.sendError(404,"404 오류");
    }

    @GetMapping("/error-500")
    public void error500(HttpServletResponse response) throws IOException {
        response.sendError(501);
    }

 

 

response에 오류 status를 담아 보내면 그대로 찍히게 된다. 상태코드를 지정 할 수 있다.

 

 

정리

1. 일반 예외는 서버내부에서 오류가 날 경우 500으로 처리가 된다. notfind는 404

2. response.sendError는 상태코드를 직접작성해서 날릴 수 있다.


 

일반 HTML 페이지의 경우 4xx,5xx와 같은 오류 페이지만 있으면 대부분의 문제를 해결할 수 있다.

 

api의 경우에는 생각할 내용이 더 많다.

API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 한다.

 

이제 깊게 들어가서 spring boot가 어떻게 예외를 처리해 주는지를 공부해봤다.

 

참고 url : https://mangkyu.tistory.com/204

 

[Spring] 스프링의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)

예외 처리는 애플리케이션을 만드는데 매우 중요한 부분을 차지한다. Spring 프레임워크는 매우 다양한 에러 처리 방법을 제공하는데, 어떠한 방법들이 있고 가장 좋은 방법(Best Practice)은 무엇인

mangkyu.tistory.com

 

내가 봤던 역대 최고의 블로그 인것 같다. 예외 처리 뿐만 아니라 스프링에 대한 좋은 내용을 많이 담고 있는 블로그다.

 

 

스프링 예외처리 방식

 

HandlerExceptionResolver는 아래 4개의 구현체들을 빈으로 등록해서 사용한다.

DefaultErrorAttributes : 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.

ExceptionHandlerExceptionResolver : 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함

ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함

DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리한다.

 

ResponseStatusExceptionResolver 는 @ExceptionHandler를 사용하는 메소드가 구현되어 있어 상속해서 사용하기에도 좋다.

Spring은 아래와 같은 도구들로 ExceptionResolver를 동작시켜 에러 처리를 진행한다.

  1. ResponseStatus
  2. ResponseStatusException
  3. ExceptionHandler
  4. ControllerAdvice, RestControllerAdvice

이 중에 내가 사용할 방식은 Api 구축이므로 @RestControllerAdvice를 사용할 것이다. 예외가 터질 경우 해당 어노테이션을 붙인 클래스가 실행 되게 된다. 만약 @ExceptionHandler에 이에 맞는 예외가 없을 경우에는 DefaultHandlerExceptionResolver 가 처리하게 되므로, Exception 객체를 ExceptionHandler에 넣어 사용하거나 모든 예외를 담고있는 ResponseEntityExceptionHandler를 상속시키는게 좋다고 한다.

 


 

사용한 방식

package com.loopcreative.web.error.handler;

import com.loopcreative.web.error.CommonErrorCode;
import com.loopcreative.web.error.ErrorCode;
import com.loopcreative.web.error.RestApiException;
import com.loopcreative.web.error.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import org.springframework.validation.BindException;
import java.util.List;
import java.util.stream.Collectors;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    
    
    @ExceptionHandler(RestApiException.class)
    public ResponseEntity<Object> handleCustomException(RestApiException e){
        ErrorCode errorCode = e.getErrorCode();
        return handleExceptionInternal(errorCode);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Object> handlerIllegalArgument(IllegalArgumentException e){
        log.warn("handleIllegalArgument",e);
        ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
        return handleExceptionInternal(errorCode,e.getMessage());
    }

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatusCode status,
            WebRequest request) {
        log.warn("handleIllegalArgument", ex);
        ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
        return handleExceptionInternal(ex, errorCode);
    }

    @ExceptionHandler({Exception.class})
    public ResponseEntity<Object> handleAllException(Exception ex){
        log.warn("handleAllException", ex);
        ErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR;
        return handleExceptionInternal(errorCode);
    }


    //ResponseEntityExceptionHandler는  스프링 ExceptionHandler가 모두 구현 되어있다.
    //하지만 에러 메세지는 반환하지 않으므로 스프링 예외에 대한 에러 응답을 보내기 위해 오버라이딩을 진행한다.
    private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(makeErrorResponse(errorCode));
    }

    private ErrorResponse makeErrorResponse(ErrorCode errorCode){
        return ErrorResponse.builder()
                .code(errorCode.name())
                .message(errorCode.getMessage())
                .build();
    }

    private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode, String message) {
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(makeErrorResponse(errorCode, message));
    }

    private ErrorResponse makeErrorResponse(ErrorCode errorCode, String message) {
        return ErrorResponse.builder()
                .code(errorCode.name())
                .message(message)
                .build();
    }

    private ResponseEntity<Object> handleExceptionInternal(BindException e, ErrorCode errorCode){
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(makeErrorResponse(e, errorCode));
    }

    private ErrorResponse makeErrorResponse(BindException e, ErrorCode errorCode){
        List<ErrorResponse.ValidationError> validationErrorList = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(ErrorResponse.ValidationError::of)
                .collect(Collectors.toList());

        return ErrorResponse.builder()
                .code(errorCode.name())
                .message(errorCode.getMessage())
                .errors(validationErrorList)
                .build();
    }
}

먼저 이제 모든 오류는 이곳을 통하게 만들었다.

makeErrorResponse 메소드는 json 모양을 이쁘게 만들기위한 객체인 ErrorResponse객체를 가지고 있다. 

(아래는 내가 사용한 버전, 커스텀 했다)

package com.loopcreative.web.error.handler;

import com.loopcreative.web.error.ErrorCode;
import com.loopcreative.web.error.RestApiException;
import com.loopcreative.web.error.UserErrorCode;
import com.loopcreative.web.error.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import org.springframework.validation.BindException;
import java.util.List;
import java.util.stream.Collectors;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {


    @ExceptionHandler(RestApiException.class)
    public ResponseEntity<Object> handleCustomException(RestApiException e){
        ErrorCode errorCode = e.getErrorCode();
        return handleExceptionInternal(errorCode);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleCustomException(Exception e){
        ErrorCode errorCode = UserErrorCode.EXCEPTION_BASIC;
        return handleExceptionInternal(errorCode);
    }


    //ResponseEntityExceptionHandler는  스프링 ExceptionHandler가 모두 구현 되어있다.
    //하지만 에러 메세지는 반환하지 않으므로 스프링 예외에 대한 에러 응답을 보내기 위해 오버라이딩을 진행한다.
    private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
        return ResponseEntity
                .status(errorCode.getHttpStatus())
                .body(makeErrorResponse(errorCode));
    }

    private ErrorResponse makeErrorResponse(ErrorCode errorCode){
        return ErrorResponse.builder()
                .status(errorCode.getStatus())
                .code(errorCode.name())
                .message(errorCode.getMessage())
                .build();
    }


}

 

 

package com.loopcreative.web.error.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.bind.validation.ValidationErrors;
import org.springframework.validation.FieldError;

import java.util.List;

@Getter
@Builder
@RequiredArgsConstructor
public class ErrorResponse {

    private final String code;
    private final int status;
    private final String message;

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private final List<ValidationError> errors;

    @Getter
    @Builder
    @RequiredArgsConstructor
    public static class ValidationError {
        private final String filed;
        private final String message;

        public static ValidationError of(final FieldError fieldError) {
            return ValidationError.builder()
                    .filed(fieldError.getField())
                    .message(fieldError.getDefaultMessage())
                    .build();
        }
    }
}

위와 같이 사용해도 되며, 입맛에 따라 변경해도 된다.

 

 

 

package com.loopcreative.web.error;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class RestApiException extends RuntimeException{ //언체크 예외만 상속

    private final ErrorCode errorCode;

}

RestApiException이라는 객체를 만들어 RuntimeException을 상속받게 했다. 앞으로 예외가 터틀리 상황에서는 이 Exception을 터트릴건데, ErrorCode 필드는 다음과 같다.

 

package com.loopcreative.web.error;

import org.springframework.http.HttpStatus;

public interface ErrorCode {

    String name();
    int getStatus();
    HttpStatus getHttpStatus();
    String getMessage();

}

 

package com.loopcreative.web.error;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode{

    INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter included"),
    RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not exists"),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error");

    private final HttpStatus httpStatus;
    private final String message;

}

 

package com.loopcreative.web.error;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum UserErrorCode implements ErrorCode{

    EXCEPTION_BASIC(HttpStatus.BAD_REQUEST.value(),HttpStatus.BAD_REQUEST,"잠시 서비스를 이용하실 수 없습니다."),
    INACTIVE_USER(HttpStatus.FORBIDDEN.value(),HttpStatus.FORBIDDEN, "User is inactive"),
    BASIC_EXCEPTION(HttpStatus.BAD_REQUEST.value(),HttpStatus.FORBIDDEN,"fasfa");

    private final int status;
    private final HttpStatus httpStatus;
    private final String message;
}

ErrorCode를 인터페이스로 만들고 Enum이 상속한다. 그럼 코드 메세지를 정해서 용도에 따라 추가해서 사용하면 된다.

 

 

사용예시

    @GetMapping("/error-ex")
    public void errorEx() throws Exception {

        throw new Exception();
    }
    @GetMapping("/error-ex2")
    public void errorEx2() throws Exception {

        throw new RestApiException(UserErrorCode.INACTIVE_USER);
    }

 

서비스 단에서 사용하면 좋을 것 같다.