2 분 소요

개념

이 글을 참고했다.

@ControllerAdvice & @RestControllerAdvice 란?

@Controller@RestController에서 발생한 예외를 한 곳에서 관리하고 처리할 수 있게 도와주는 어노테이션이다.
@Advice 어노테이션이 붙은 클래스는, Exception이 나는 모든 경우에 거쳐간다.

@ExceptionHandler 란?

모든 에러가 그쪽으로 Catch될 수 있는 처리를 해 주는 기능이다
스크린샷 2023-03-03 오후 4 33 05

프로젝트에 적용

이 글을 참고했다.

위의 @RestControllerAdvice@ExceptionHandler를 사용해서 Exception을 처리 해 보도록 하자

적용하는 과정은 다음과 같다.

  1. 에러코드 정리 enum 클래스로 작성
  2. 예외에 대한 응답 정보를 저장하는 클래스 작성
  3. 사용자 정의 Exception 클래스 작성
  4. Exception 발생시 전역으로 처리할 exception handler 작성
  5. 일기 조회 서비스에서 exception handling
  6. api 실행 및 exception 결과 확인

1. 에러코드 정리 enum 클래스로 작성

ErrorCode

@Getter
@AllArgsConstructor
public enum ErrorCode {

    /* 400 BAD_REQUEST : 잘못된 요청 */
    CONSTRAINT_VIOLATION(HttpStatus.BAD_REQUEST, "제약 조건을 위배한 요청입니다 (constraint violation)"),
    METHOD_ARG_NOT_VALID(HttpStatus.BAD_REQUEST, "제약 조건을 위배한 요청입니다 (method argument not valid)"),
    STAMP_LIST_SIZE_ERROR(HttpStatus.BAD_REQUEST, "최대 3개의 스탬프를 선택할 수 있습니다"),
    INVALID_FILE_ERROR(HttpStatus.BAD_REQUEST, "확장자가 jpg, jpeg, png 인 파일만 업로드 가능합니다."),

    /* 404 NOT_FOUND : Resource 를 찾을 수 없음 */
    USER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다"),
    DIARY_NOT_FOUND(HttpStatus.NOT_FOUND, "일기를 찾을 수 없습니다"),
    PET_NOT_FOUND(HttpStatus.NOT_FOUND, "반려 동물을 찾을 수 없습니다"),
    STAMP_NOT_FOUND(HttpStatus.NOT_FOUND, "스탬프를 찾을 수 없습니다"),

    /* 500 INTERNAL_SERVER_ERROR : 서버 에러 */
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "예상치 못한 에러가 발생했습니다."),
    FILE_CAN_NOT_UPLOAD(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다."),
    FILE_CAN_NOT_DELETE(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제에 실패했습니다."),

    ;

    private final HttpStatus httpStatus;
    private final String message;

}

2. 예외에 대한 응답 정보를 저장하는 클래스 작성

ErrorResponse

@Getter
@Builder
public class ErrorResponse {
    private final String error;
    private final String message;

    public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) {
        return ResponseEntity
            .status(errorCode.getHttpStatus())
            .body(ErrorResponse.builder()
                .error(errorCode.getHttpStatus().name())
                .message(errorCode.getMessage())
                .build()
            );
    }

    public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode, String defaultMessage) {
        return ResponseEntity
            .status(errorCode.getHttpStatus())
            .body(ErrorResponse.builder()
                .error(errorCode.getHttpStatus().name())
                .message(defaultMessage)
                .build()
            );
    }
}

3. 사용자 정의 Exception 클래스 작성

CustomException

@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
    private final ErrorCode errorCode;
}

4. Exception 발생시 전역으로 처리할 exception handler 작성

GlobalExceptionHandler

@RestControllerAdvice
public class GlobalExceptionHandler {

  // 모든 예외를 핸들링하여 ErrorResponse 형식으로 반환한다.
    @ExceptionHandler(Exception.class) // ✅
    protected ResponseEntity<ErrorResponse> handleAllException(Exception ex) {
        log.error("handleAllException throw Exception : {}", ex.getClass().getName());
        return ErrorResponse.toResponseEntity(INTERNAL_SERVER_ERROR);
    }

    // @Valid 검증 실패 시 Catch
    @ExceptionHandler(MethodArgumentNotValidException.class) // ✅
    protected Object handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        String defaultMessage = Objects.requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage();
        return ErrorResponse.toResponseEntity(METHOD_ARG_NOT_VALID, defaultMessage);
    }

    // 제약 조건 위배 시 Catch
    @ExceptionHandler(value = {ConstraintViolationException.class, DataIntegrityViolationException.class}) // ✅
    protected ResponseEntity<ErrorResponse> handleDataException() {
        log.error("handleDataException throw Exception : {}", CONSTRAINT_VIOLATION);
        return ErrorResponse.toResponseEntity(CONSTRAINT_VIOLATION);
    }

    // CustomException 을 상속받은 클래스가 예외를 발생 시킬 시, Catch 하여 ErrorResponse 를 반환한다.
    @ExceptionHandler(CustomException.class) // ✅
    protected ResponseEntity<ErrorResponse> handleCustomException(CustomException ex) {
        log.error("handleCustomException throw CustomException : {}", ex.getErrorCode());
        return ErrorResponse.toResponseEntity(ex.getErrorCode());
    }
}
  • REST API에 맞게, @RestControllerAdvice 를 사용했고, @ExceptionHandler 를 적용하여 Exception을 받을 객체라는것을 명시했다.
  • 함수 인자로 Exception을 받아 해당하는 Exception 처리를 다룰 수 있도록 했다.
  • 위 코드는 MethodArgumentNotValidException, ConstraintViolationException, DataIntegrityViolationException, CustomException 까지
    총 네 개의 exception 에 대한 예외 처리를 해주었다.
    • ex.getClass().getName() 을 출력해보면 현재의 Exception이 어떤 Exception인지 알 수 있어서, 해당하는 내역에 따라 따로 처리 해 줄 수 있다.

5. 일기 조회 서비스에서 exception handling

DiaryServiceImpl에서 단건 일기를 조회할 때,
존재하지 않는 일기를 조회하려고 할 경우 CustomException을 발생시키며,
GlobalExceptionHandler에서 해당 exception을 캐치해서 적절한 에러응답을 생성해서 json결과를 내려준다.

@Slf4j
@Service
@RequiredArgsConstructor
public class DiaryServiceImpl implements DiaryService {
    @Override
    @Transactional
    public DiaryDetailResDto getDetailedDiary(Long diaryId) {
        Diary diary = diaryRepository.findById(diaryId)
                .orElseThrow(() -> new CustomException(DIARY_NOT_FOUND)); // ✅
        return new DiaryDetailResDto(diary);
    }

    ...
}

6. api 실행 및 exception 결과 확인

만약, 존재하지 않는 100번째 일기를 조회하려고 시도한다면?
스크린샷 2023-03-03 오후 1 56 34
위와 같이 직접 정의한 exception이 발생한다.



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기

태그:

카테고리:

업데이트: