ErrorHandler 알고 사용했을까?

ezi·2024년 1월 17일

(참고) @Controller는 주로 View를 반환하기 위해, @RestController는 Json 형태로 데이터를 반환하기 위해 사용한다. @ResponseBody가 없으면 text.html로 "view"로 반환, 있으면 client한테 json 형식으로 반환한다.

Exception 처리해야하는 이유

아무 exception 처리를 해주지 않는다면 text.html 형식으로 반환된다.

API 오류 메시지에 대해 일관된 형식으로 응답하도록 설계해야 한다.

신규 API를 구현할 때마다 작성하도록 설계하는 경우 작업자마다 일관된 응답 구조를 보장하기 어렵기 때문에 별도의 작업을 하지 않아도 일관된 오류 메시지 형식으로 응답할 수 있도록 해야 합니다.

ApiException

(API 호출 중 발생한 예외를 처리하기)

예를 들어 멤버를 업데이트 한다고 가정하자.

존재하지 않는 멤버를 업데이트하면 만들어둔 Enum(하드코딩 하기위해)ErrorCode의 MEMBER_NOT_FOUND를 담군 새로운 ApiException을 만들어 예외를 발생하도록 하였다.

    public Long updateMember(UpdateMemberServiceRequest request)
    {
        Member member = memberRepository.findById(request.memberId()).orElseThrow(
                () -> new ApiException(ErrorCode.MEMBER_NOT_FOUND)
        );
        member.update(request.email(), request.nickname());
        return member.getMemberId();
    }

즉, Service 에서 ApiException 발생 (try-catch로 잡아주지 않았다면) > Controller 로 예외 던짐 > GlobalExceptionHandler의 @RestControllerAdvice (ResponseBody + ControllerAdvice) 가 에러를 잡아옴 > @ExceptionHandler(ApiException.class)이 ApiException를 잡아옴 > handelApiException에서 처리해준다.

    @ExceptionHandler(ApiException.class)
    public ResponseEntity<ErrorResponse> handelApiException(ApiException exception)
    {
        ErrorCode errorCode = exception.getErrorCode();
        return ErrorResponse.apiError(errorCode);
    }

exception.getErrorCode() 안에는 ErrorCode를 필드로 갖고 받은 메세지를 초기화해준다.

package com.study.velog.config.exception;

import lombok.Getter;

@Getter
public class ApiException extends RuntimeException {
    private ErrorCode errorCode;

    public ApiException(ErrorCode errorCode)
    {
        super(errorCode.getErrorMessage());
        this.errorCode = errorCode;
    }
}

ErrorCode는 HttpStatus와 errorMessage를 필드로 갖고 초기화 해준다.

// ErrorCode
package com.study.velog.config.exception;

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


@Getter
public enum ErrorCode {
    MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "멤버가 없습니다."),
    MEMBER_EMAIL_DUPLICATE(HttpStatus.INTERNAL_SERVER_ERROR, "이메일이 중복되었습니다.")
    ;
    private final HttpStatus httpStatus;
    private final String errorMessage;

    ErrorCode(HttpStatus httpStatus, String errorMessage)
    {
        this.httpStatus = httpStatus;
        this.errorMessage = errorMessage;
    }
}

다시 정리하자면 컨트롤러까지 넘어온 예외를 에러 핸들러로 잡아오고 처리해준다.
어떻게?
new ApiException(ErrorCode.MEMBER_NOT_FOUND)

  • MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "멤버가 없습니다.") > ErrorCode 생성(httpStatus = HttpStatus.NOT_FOUND, errorMessage = "멤버가 없습니다.")

서비스 단에서 ApiException 터졌다. Controller로 넘겨졌다. 에러 핸들러로 잡아왔다.
@ExceptionHandler(ApiException.class) 예외는 이제 여기서 처리해 줄 거다.
던져진 ApiException에서 에러코드 가져온다. (httpStatus = HttpStatus.NOT_FOUND, errorMessage = "멤버가 없습니다.")
담구고 이제 return 해 줄 건데 ErrorResponse에 담궈서 해줄거다.

왜 ErrorResponse로 return ?

이 상태로 실행하면 우리가 원한대로 JSON 형식으로 reponse가 올 것이다.

{
    "message": "이메일 형식이 아닙니다",
    "errorCode": "Bad Request",
    "status": 400
}

하지만 이건 그저 JSON일 뿐, 정말 HTTP 헤더의 STATUS는 200 OK 이다.
400 에러를 반환했는데 200이 뜨면 안되지 않을까? 따라서 이때 사용하는 것이 ResponseEntity 이다.

ResponseEntity 를 사용하여 헤더의 상태코드를 우리가 상황별로 다이나믹하게 적용시킬 수 있도록 한다.

ResponseEntity 의 status엔 우리가 만든 errorCode의 status를 적용시키고 body엔 에러 메세지, 상태 상태코드, 상태코드 값을 넣어 return 하도록 한다.

package com.study.velog.config.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

public record ErrorResponse(
        String message,
        String errorCode,
        int status
) {
    public static ResponseEntity<ErrorResponse> apiError(ErrorCode errorCode)
    {
        return
                ResponseEntity.status(errorCode.getHttpStatus())
                        .body(
                                new ErrorResponse(
                                        errorCode.getErrorMessage(),
                                        errorCode.getHttpStatus().getReasonPhrase(),
                                        errorCode.getHttpStatus().value()
                                )
                        );
    }
}

다시 정리하자면,
서비스 단에서 예외가 터졌는데 서비스 단에서 예외를 처리해주지 않았다면
컨트롤러로 예외가 넘어온다.
그럼 RestControllerAdvice 가 컨트롤러에서 발생한 예외를 잡아온다.
잡아 온 에러를 ExceptionHandler로 해당 예외에 맞는 핸들러가 처리해준다.


@ResponseStatus(HttpStatus.BAD_REQUEST) 를 사용하면 HTTP의 Header의 Status를 고정적으로 설정한다. 여기선 BAD_REQUEST - 400 이 작동된다.


만약 ApiException 처럼 다양한 에러코드를 담는 있는 예외라면 고정적인 Status 보다 다이나믹하게 설정해야 알맞는 상태코드가 뜰 것이다.
이럴땐 ResponseEntity에 담궈서 보내주면 된다.


MethodArgumentNotValidException

(유효성 검사시 발생하는 exception)

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException exception)
    {
        String message = exception.getFieldErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return ErrorResponse.badRequest(message);
    }

@ExceptionHandler(MethodArgumentNotValidException.class) 에러 잡아오고

getFieldErrors 는 MethodArgumentNotValidException 에만 있는 함수이다.
여튼 stream을 사용해서 유효성 검사 에러가 여러개 나더라도 해당 메세지를 다 보여주도록 하였다.
@ResponseStatus(HttpStatus.BAD_REQUEST) HTTP header의 상태코드는 고정적으로 BAD_REQUEST-400

public record ErrorResponse(
        String message,
        String errorCode,
        int status
) {
    public static ErrorResponse badRequest(String message)
    {
        return new ErrorResponse(
                message,
            HttpStatus.BAD_REQUEST.getReasonPhrase(),
            HttpStatus.BAD_REQUEST.value()
        );
    }
}

Exception

제일 중요한건 부모인 Exception 가 아닐까 한다.

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public ErrorResponse handleException(Exception exception)
    {
        log.error("exception class : {}", exception.getClass());
        return ErrorResponse.error(exception.getMessage());
    }

만약 ExceptionHandler 가 없다면,
우리가 생각하지 못한 에러를 잡는 코드가 없다고 가정하자. 그럼 컨트롤러에서 예외가 넘어오지만 예외를 처리해주는 코드가 없으니 main으로 넘어가고 결국은 종료되게 된다.
모든 코드는 Exception의 자식이다. 따라서 생각하지 못한 에러를 잡는 코드가 없다고 하더라도 결국 부모인 Exception으로 넘어가서 ExceptionHandler 가 잡아준다.

그러니 그 어떤 에러 핸들러보다 ExceptionHandler가 가장 중요하다.

ErrorHandler 를 이해하려면 Flow를 꼭 이해하자.

https://leeys.tistory.com/30

profile
차곡차곡

0개의 댓글