예외 처리 하기 (exception handling)
개념
이 글을 참고했다.
@ControllerAdvice
& @RestControllerAdvice
란?
@Controller
나 @RestController
에서 발생한 예외를 한 곳에서 관리하고 처리할 수 있게 도와주는 어노테이션이다.
@Advice
어노테이션이 붙은 클래스는, Exception이 나는 모든 경우에 거쳐간다.
@ExceptionHandler
란?
모든 에러가 그쪽으로 Catch될 수 있는 처리를 해 주는 기능이다
프로젝트에 적용
이 글을 참고했다.
위의 @RestControllerAdvice
와 @ExceptionHandler
를 사용해서 Exception을 처리 해 보도록 하자
적용하는 과정은 다음과 같다.
- 에러코드 정리 enum 클래스로 작성
- 예외에 대한 응답 정보를 저장하는 클래스 작성
- 사용자 정의 Exception 클래스 작성
- Exception 발생시 전역으로 처리할 exception handler 작성
- 일기 조회 서비스에서 exception handling
- 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번째 일기를 조회하려고 시도한다면?
위와 같이 직접 정의한 exception이 발생한다.
💛 개인 공부 기록용 블로그입니다. 👻