Just Do IT!
@RestControllerAdivce 적용해서 Spring 전역 예외처리하기 본문
ControllerAdvice
- 전역적으로 ExceptionHandler를 적용할 수 있는 @ControllerAdvice와 @RestControllerAdvice 어노테이션을 제공하고 있다.
- @ControllerAdvice는 여러 컨트롤러에 전역적으로 ExceptionHandler를 적용해준다
- 만약 특정 클래스에만 제한적으로 적용하고 싶다면 @RestControllerAdvice의 basePackages 등을 설정함으로 제한할 수 있다.
- @RestControllerAdvice는 @ControllerAdvice와 다리 @ResponseBody가 붙어 있어 응답을 JSON으로 내려준다
@RestControllerAdvice 와 @ControllerAdvice 의 특징과 차이
- @RestControllerAdvice 는 @ControllerAdvice + @ResponseBody 의 조합으로, RESTful API를 개발할 때 사용한다.
- 응답을 JSON 형식으로 내려줍니다.
- @ControllerAdvice 는 MVC 패턴을 사용할 때 적합하며, 응답에 @ResponseBody 를 추가해야 한다.
- @RestControllerAdvice 와 @ControllerAdvice 는 패키지 단위로 적용할 수 있다.
- 예를 들어, @ControllerAdvice ("com.controller") 와 @RestControllerAdvice ("com.rest.controller") 로 각각의 컨트롤러에 맞는 예외 처리를 할 수 있다.
@RestControllerAdvice, @ControllerAdvice의 장점
- 하나의 클래스로 모든 컨트롤러에 대해 전역적으로 예외 처리가 가능하다.
- 직접 정의한 에러 응답을 일관성 있게 클라이언트에게 내려줄 수 있다.
- 여러 컨트롤러에서 발생하는 동일한 예외에 대해 한 곳에서 처리할 수 있다.
- 별도의 try-catch문이 없어 코드의 가독성이 높아진다.
- 예외에 따라 다른 처리 로직을 적용할 수 있다.
- @ExceptionHandler 어노테이션을 사용하여 특정 에러 상황에 따다른 핸들러 메소드를 정의하여 사용할 수 있다.
@RestController 적용 예시
위의 장점들을 통해 예외 처리를 common 패키지에 추가하여 전역적으로 처리하기로 했다.
지금 진행하는 프로젝트에 적용했던 예외 처리 예시이다.
ErrorController
@Slf4j
@RestControllerAdvice(annotations = RestController.class)
public class ErrorController {
@ExceptionHandler(BaseException.class)
protected ResponseEntity<ErrorResponse> handleBaseException(BaseException e) {
return ResponseEntity
.status(e.getHttpStatus())
.body(ErrorResponse.from(e.getErrorCode()));
}
// @valid 에서 binding error 발생
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<String> params = new ArrayList<>();
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
params.add(fieldError.getField() + ":" + fieldError.getDefaultMessage());
}
String errorMessage = String.join(", ", params);
ErrorResponse response = ErrorResponse.from(ErrorCode.VALIDATION_FAILED);
response.changeMessage(errorMessage);
return response;
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(RuntimeException.class)
protected ErrorResponse handleRuntimeException(RuntimeException e) {
log.error(e.getMessage());
return ErrorResponse.from(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
- @RestControllerAdvice(annotations = RestController.class)
- 전역 예외 처리하는 controller임을 나타낸다.
- @RestController 어노테이션이 붙은 모든 컨트롤러에서 발생하는 예외를 처리한다.
- handleBaseException
- BaseException 타입의 예외를 처리한다.
- BaseException은 사용자가 직접 정의한 예외이다.
- 예외에서 HTTP 상태 코드와 에러 코드를 가져와서 ErroResponse 객체를 생성한 후, 해당 객체를 클라이언트에 반환한다.
- handleMethodArgumentNotValidException
- @Valid로 검증 시 발생하는 바인딩 오류를 처리한다.
- 오류 필드와 메세지를 수집하여 params 리스트에 추가한다.
- 최종적으로 오류 메세지를 하나의 문자열로 결합하고 ErrorResponse 객체를 생성하여 클라이언트에 반환한다.
- handleRuntimeException
- 일반적인 런타임 에외를 처리한다.
- 예외 메세지를 로그로 기록하고 내부 서버 오류를 나타내는 INTERNAL_SERVER_ERROR를 반환한다.
- 각 메서드는 예외에 대한 적절한 HTTP 상태 코드와 함께 ErrorResponse 객체를 반환하면서 API 응답의 일관성을 유지한다.
ErrorResponse
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
@JsonProperty("errorCode")
private String code;
@JsonProperty("errorMessage")
private String message;
public static ErrorResponse from(ErrorCode errorCode) {
return ErrorResponse
.builder()
.code(errorCode.getCode())
.message(errorCode.getMessage())
.build();
}
public void changeMessage(String message) {
this.message = message;
}
}
- @AllArgsConstructor(access = AccessLevel.PRIVATE)
- 모든 필드를 매개변수로 받는 생성자를 생성
- 접근 수준을 PRIVATE로 설정하여 외부에서 직접 인스턴스를 생성할 수 없게 한다.
- @NoArgsConstructor(access = AccessLevel.PROTECTED)
- 매개변수가 없는 생성자를 생성=
- 이 생성자는 PROTECTED로 설정되어 있어 서브클래스에서만 사용할 수 있다.
- @JsonProperty("errorCode")
- JSON 직렬화 및 역직렬화 시 errorCode라는 이름으로 변환
- 이 필드는 오류 코드 문자열을 저장
- @JsonProperty("errorMessage")
- 마찬가지로 JSON 변환 시 errorMessage라는 이름을 사용
- 이 필드는 오류 메시지를 저장
- from(ErrorCode errorCode)
- 주어진 ErrorCode를 기반으로 ErrorResponse 객체를 생성한다.
- changeMessage
- 오류 메세지를 변경하는 메서드이다.
- API에서 발생하는 오류에 대한 구조화된 응답을 정의한다.
BaseException
@Getter
@RequiredArgsConstructor
public class BaseException extends RuntimeException {
public static final BaseException VALIDATION_FAILED = new BaseException(ErrorCode.VALIDATION_FAILED);
public static final BaseException BOOK_CATEGORY_ERROR = new BaseException(ErrorCode.BOOK_CATEGORY_ERROR);
public static final BaseException BOOK_CATEGORY_NOT_FOUND = new BaseException(ErrorCode.BOOK_CATEGORY_NOT_FOUND);
public static final BaseException BOOK_SEARCH_NOT_FOUND = new BaseException(ErrorCode.BOOK_SEARCH_NOT_FOUND);
public static final BaseException INTERNAL_SERVER_ERROR = new BaseException(ErrorCode.INTERNAL_SERVER_ERROR);
private final ErrorCode errorCode;
@Override
public synchronized Throwable fillInStackTrace() {
return this; // 스택 트레이스 생략
}
public HttpStatus getHttpStatus() {
return errorCode.getHttpStatus();
}
}
- RuntimeException을 확장하여 사용자 정의 예외를 정의하는 클래스이다
- 정적 예외 인스턴스를 정의해서 각기 다른 오류 상황을 표현한다.
- fillInstaceTrace
- 스택 트레이스를 생략하여 성능 개선
- 예외의 발생 위치를 추적할 필요가 없는 경우에 유용하다.
- getHttpStatus
- 관련된 ErrorCode에서 HTTP 상태 코드를 반환하는 메서드
ErrorCode (enum)
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
/* 400 */
VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "VALIDATION_FAILED", "입력값 유효성 검사에 실패했습니다."),
/* 404 */
BOOK_CATEGORY_ERROR(HttpStatus.NOT_FOUND, "BOOK_CATEGORY_ERROR", "해당 카테고리는 존재하지 않습니다"),
BOOK_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOK_CATEGORY_NOT_FOUND", "해당 카테고리에 포함된 문제집이 없습니다."),
BOOK_SEARCH_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOK_SEARCH_NOT_FOUND", "검색어와 일치하는 문제집이 없습니다."),
/* 500 */
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "예상치 못한 서버 에러가 발생했습니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
}
- 각 오류에 대한 세부 정보를 정의하는 enum
- HTTP 상태 코드, 코드 문자열, 사용자에게 보여줄 메세지를 포함한다.
예외 처리 적용 예시
@Override
public List<BookResponse> searchBooksByKeyword(String keyword) {
List<Book> bookList = bookRepository.findAllByTitleContainingAndSecretFalse(keyword);
// 해당 검색어와 일치하는 문제집이 없는 경우 예외처리
if(bookList.isEmpty()) {
log.warn("No books found for keyword: {}", keyword);
throw new BaseException(ErrorCode.BOOK_SEARCH_NOT_FOUND);
}
return convertToBookResponseList(bookList);
}
검색어와 일치하는 문제집이 없는 경우 error message가 나오도록 예외처리를 해주었다.
이렇게 지정했던 404 Not Found 오류와 함께 에러 메세지가 함께 나오는 걸 확인할 수 있다.
프로젝트를 진행하다 보니 예외 처리에 대한 고민이 생겼다.
깃허브의 다른 프로젝트를 구경하다보니 전역적으로 예외 처리를 구현해놓은 프로젝트가 있어서
그걸 참고로 우리 프로젝트에 맞게 예외 처리를 추가해주었다.
정말...이번 프로젝트에서 새로 해보는 게 많은 것 같아서 즐겁다.
참고 링크:
Spring 전역 예외 처리: @RestControllerAdivce 적용
Spring Boot에서 @RestControllerAdivce 어노테이션을 사용한 전역 예외 처리 방법에 대해 정리한 글입니다.
velog.io
https://github.com/DevTraces/BackEnd
GitHub - DevTraces/BackEnd: 🖼 일상의 예술을 공유하는 SNS
🖼 일상의 예술을 공유하는 SNS. Contribute to DevTraces/BackEnd development by creating an account on GitHub.
github.com
https://velog.io/@yoonuk/Spring-%EC%A0%84%EC%97%AD-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC
[Spring] 전역 예외 처리
Spring에서는 @ControllerAdvice 어노테이션과 @RestControllerAdvice 어노테이션을 사용하여 컨트롤러에서 발생하는 예외를 전역적으로 처리할 수 있습니다. Spring에서 예외 처리를 할 때, @RestControllerAdvice
velog.io
'개발 공부 > Spring' 카테고리의 다른 글
JPA Cascade 알아보기 (0) | 2024.10.29 |
---|---|
[Spring Boot] 북마크 생성/삭제 토글 형식으로 구현하기 (0) | 2024.10.22 |
[Spring Boot] H2 Database를 사용하여 테스트 코드 작성하기 (1) | 2024.10.17 |
JWT에서 Access Token, Refresh Token이 필요한 이유 (1) | 2024.09.11 |
JWT + spring security 로그인 구현 실습 (0) | 2024.09.10 |