개발 환경

  • Java 17
  • SpringBoot 3.2.1


도입 배경

Custom Exception을 적용하게 된 계기는 다음과 같습니다.

바로 서버에서 예외가 발생했을 때, 클라이언트에게 어떤 문제가 발생했는지 더 명확하게 전달할 수 있다는 점입니다.

예를 들어, 400 에러나 404 에러가 발생할 이유는 다양합니다. 회원이 존재하지 않다거나 회원 이메일이 존재하지 않다거나 등등..

이럴 때 Custom Exception을 사용하면 클라이언트는 에러 핸들링 하기에 용이하며, 이는 곧 사용자에게 세분화된 에러 메시지를 보여줄 수 있게 됩니다.

따라서 ErrorCode를 문서화하고 전역처리를 하게 된다면 생산성 및 유지보수성이 높아질 것으로 기대합니다.


Global 하게 적용하기

처음에는 다음 코드처럼 모든 예외를 custom exception class로 만들고 도메인 별로 ErrorHandler를 만들었습니다.

@Slf4j
@DomainSpecificAdvice
public class MemberErrorHandler {

    @ExceptionHandler(AlreadyExistsMemberException.class)
    public ResponseEntity<ErrorResponse> alreadyExistsMemberExceptionHandle(AlreadyExistsMemberException e) {
        log.error("{}", e.getMessage());

        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ErrorResponse.createError(ErrorCode.ALREADY_EXISTS_MEMBER));
    }

    @ExceptionHandler(MemberNotFoundException.class)
    public ResponseEntity<ErrorResponse> memberNotFoundExceptionHandle(MemberNotFoundException e) {
        log.error("{}", e.getMessage());

        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(ErrorResponse.createError(ErrorCode.MEMBER_NOT_FOUND));
    }

    @ExceptionHandler(PasswordMismatchException.class)
    public ResponseEntity<ErrorResponse> passwordMismatchExceptionHandle(PasswordMismatchException e) {
        log.error("{}", e.getMessage());

        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(ErrorResponse.createError(ErrorCode.PASSWORD_MISMATCH));
    }

    @ExceptionHandler(EmailNotFoundException.class)
    public ResponseEntity<ErrorResponse> emailNotFoundExceptionHandle(EmailNotFoundException e) {
        log.error("{}", e.getMessage());

        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(ErrorResponse.createError(ErrorCode.EMAIL_NOT_FOUND));
    }

    @ExceptionHandler(AlreadyExistsEmailException.class)
    public ResponseEntity<ErrorResponse> alreadyExistsEmailExceptionHandle(AlreadyExistsEmailException e) {
        log.error("{}", e.getMessage());

        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ErrorResponse.createError(ErrorCode.ALREADY_EXISTS_EMAIL));
    }
}

하지만 이렇게 작성하게 되니 exception 별로 클래스를 만들어야 하고, 패키지가 지저분해지는 단점이 보이게 되었습니다.

따라서 Exception을 공통으로 처리할 RuntimeException을 상속받은 CustomApiException이라는 클래스를 만들었습니다.

이제 예외가 발생하면 클라이언트는 다음과 같은 응답을 받을 수 있습니다.

//status: 400Bad Request

{
  "errorCode": "MEMBER_002",
  "errorDescription": "이미 존재하는 회원입니다."
}

응답으로 보내질 HttpStatus200 OK로 고정하는 것이 아닌, 400 Bad Request 등의 적절한 status를 적용했습니다.


구현

ErrorCode

@Getter
@AllArgsConstructor
public enum ErrorCode {
    // Member
    MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER_001", "존재하지 않는 회원입니다."),
    IS_EXISTS_MEMBER(HttpStatus.BAD_REQUEST, "MEMBER_002", "이미 존재하는 회원입니다."),
    ;

    private final HttpStatus httpStatus;
    private final String errorCode;
    private final String errorDescription;
}

Custom Error Code 입니다.

Enum으로 만들고 HttpStatus, Custom Error Code, Error Description을 정의했습니다.


ErrorResponse

public record ErrorResponse (String errorCode, String errorDescription) {

    public ErrorResponse(ErrorCode errorCode) {
        this(errorCode.getErrorCode(), errorCode.getErrorDescription());
    }
}

ResponseBody에 들어갈 데이터입니다.

errorCodeerrorDescription을 담았습니다.


CustomApiException

@Getter
public class CustomApiException extends RuntimeException {

    private final HttpStatus httpStatus;
    private final ErrorResponse errorResponse;

    public CustomApiException(ErrorCode errorCode) {
        this.httpStatus = errorCode.getHttpStatus();
        this.errorResponse = new ErrorResponse(errorCode);
    }
}

RuntimeException을 상속받은 CustomApiException입니다.


GlobalRestControllerAdvice

@Slf4j
@RestControllerAdvice
public class GlobalRestControllerAdvice {

    @ExceptionHandler(CustomApiException.class)
    public ResponseEntity<?> customExceptionHandle(CustomApiException e) {
        log.error("Error occurs {}", e.getErrorResponse());
        return ResponseEntity.status(e.getHttpStatus()).body(e.getErrorResponse());
    }
}

전역으로 Custom Exception을 받아서 처리할 클래스입니다.


사용 예시

@RequiredArgsConstructor
@Service
public class MemberService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    /**
     * 회원 가입
     */
    public void signUp(SignUpRequestDto requestDto) {
        String username = requestDto.username();
        if (memberRepository.isExistsMember(username)) {
            throw new CustomApiException(ErrorCode.IS_EXISTS_MEMBER);
        }

        memberRepository.save(
                Member.builder()
                        .username(username)
                        .password(requestDto.password())
                        .name(requestDto.name())
                        .passwordEncoder(passwordEncoder)
                        .build()
        );
    }
}

위 코드와 같이 CustomApiException을 던지고 적절한 ErrorCode를 인자로 담아줍니다.

{
  "errorCode": "MEMBER_002",
  "errorDescription": "이미 존재하는 회원입니다."
}

Categories:

Updated:

Leave a comment