반응형
Review 에서는 학습한 내용을 다시금 기록합니다.
Unit Review는 학습한 내용 중 기존에 알고 있었지만 정확하게 이해하지 못하던 정보와 새롭게 알게된 정보를 기록합니다. 추가적인 설명을 요하는 부분은 댓글로 남겨주세요.
Section Review는 전반적인 Section을 되돌아보고 학습했던 시간과 과정, 내용을 총괄하여 기록합니다.
Spring MVC에서의 예외 처리
- HTTP 통신을 하다 보면, 필요한 인자가 누락되거나 URL 경로에 오류가 있는 등 예외가 발생할 수 있다.
- 이 때, 예외처리를 하지 않으면 단순히 400 Bad Request 등의 HTTP 결과값만 반환되기 때문에, 어디서 오류가 발생했는지 알 수 없다.
- 따라서, 예외 처리를 통해 어디에서 오류가 발생했는지를 파악하고 디버깅이 용이하게 가능케 해야한다.
* @ExceptionHandler를 이용한 예외 처리
- @ExceptionHandler를 이용한 Controller 레벨에서의 예외 처리
- @ExceptionHandler 애너테이션은 컨트롤러레벨 이상의 빈에서 예외 처리 메서드를 사용할 때 사용할 수 있다.
- 기본적인 사용법은 아래와 같다.
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e){
/**
* 발생한 예외에서 BindingResult 객체를 가져온다.
* 가져온 BindingResult 객체에서 다시 FieldError의 리스트를 가져온다.
*/
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
}
- 컨트롤러 클래스에 예상되는 예외를 인자로 받는 예외처리 메서드를 작성하고 @ExceptionHandler 애너테이션을 붙여준다.
- 위의 hanleException() 메서드는 HTTP 메서드를 수행하는 메서드에서 엔티티 객체에 전달되는 인자에 오류가 있을 때(MethodArgumentNotValidException이 발생할 때) 발생한 예외를 처리한다.
- 이렇게 예외를 처리하면 구체적인 에러 메세지를 Response Body에 담아 전달해주지만, 지나치게 많은 정보가 담기게 된다. 따라서 우리가 원하는 '어디에서 어떻게 에러가 발생했는가?' 에 대한 정보만 얻고 싶다면 우리가 원하는 정보만을 담은 객체를 새롭게 정의해야한다.
- 이렇게 많은 정보가 담기는 이유는 FieldError의 API를 뜯어보면 알 수 있다. FieldError는 ObjectError를 상속받고 있고, ObjectError는 DefaultMessageSourceResolvable를 상속 받고 있다.
- 결국 세 개의 클래스에 담긴 정보를 모두 Body에 담아 응답하니, 당연히 정보가 지나치게 많아질 수밖에 없다.
- ErrorResponse 클래스 적용
- 우리가 원하는 정보만을 표현할 수 있게, 예외 정보가 담긴 Response Body를 담을 ErrorResponse 클래스를 생성하고, ErrorResponse 객체를 통해 ResponseEntity를 생성해보자.
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public class ErrorResponse {
private List<FieldError> fieldErrors; // 에러를 담을 리스트. 에러는 여러 부분에서 동시에 발생할 수 있다.
@Getter
@AllArgsConstructor
public static class FieldError {
private String field; // 어떤 필드값에서 에러가 발생했는지를 보여줌.
private Object rejectedValue; // 에러가 발생한 필드에 초기화 된 값을 보여줌.
private String reason; // 설정한 에러 메세지를 보여줌.
}
}
- 앞서 예시에서 사용한 FieldError 타입은 스프링에 내장된 API이다.
- 이제부터 사용할 FieldError는 ErrorResponse에 담길 static 변수다. 둘은 명백히 다르니 혼동하지 말자.
- 위에서 사용했던 handleException 메서드를 리팩토링 한 결과는 아래와 같다.
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e){
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
/**
* 생성한 FileError의 리스트를 ErrorResponse의 static 멤버인 FieldError객체의 리스트로 변환한다.
*/
List<ErrorResponse.FieldError> errors =
fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
/**
* 생성한 리스트를 사용해 ErrorResponse 객체를 생성한 뒤 ResponseEntity객체를 생성한다.
*/
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
- ResponseEntity의 Body에 ErrorResponse 객체를 담아줬다. ResponseEntity는 이제 ErrorResponse에 담긴 정보들을 바탕으로 JSON 객체를 생성할 것이다.
- ErrorResponse에는 FieldError 객체가 담겨있고, FieldError객체에는 우리가 원하는 세 가지 정보만 담겨있다.
- 결과적으로 우리는 아래와 같이 우리가 원하는 정보만을 담 응답을 받을 수 있다.
{
"fieldErrors": [
{
"field": "email",
"rejectedValue": "2222",
"reason": "올바른 형식의 이메일 주소여야 합니다"
}
]
}
- @ExceptionHandler를 컨트롤러 레벨에서 사용하는 것의 단점
- @ExceptionHandler는 분명 예외를 확실히 처리할 수 있게 해준다.
- 그러나 하나의 메서드에만 적용되기 때문에 각기 다른 예외를 처리하려면 각기 다른 메서드를 만들어야한다.
- 문제는 컨트롤러 단에서 예외를 처리하게 되면, 각 컨트롤러마다 같은 예외처리 코드를 작성해야 한다는 것이다. 즉 코드의 중복이 발생한다.
- 또한 컨트롤러의 기능에 대해 생각해보았을 때, 컨트롤러에서 '예외를 처리하는 것'이 과연 컨트롤러가 담당해야 할 기능인가에 대해서도 고민해볼 법하다.
- 컨트롤러는 어떤 요청을 받고 그에 해당하는 기능을 하는 다른 객체에 요청을 넘기는 기능을 하는 객체다. 예외 처리는 컨트롤러가 할 일을 아득히 넘어섰다.
- 결국 우리는 컨트롤러 레벨에서 예외를 처리할 것이 아니라, 예외 처리를 담당할 객체를 만들어서 예외처리를 할 필요가 있다.
* @RestControllerAdvice를 이용한 예외처리
- @ControllerAdvice VS @RestControllerAdvice
- @RestControllerAdvice는 @ControllerAdvice에 @ResponseBody를 합친 애너테이션이다.
- @ControllerAdivce는 전통적인 MVC 아키텍처에서 사용되는 컨트롤러에서 발생하는 예외를 처리하는데 사용된다. 즉, View를 반환하는 방식으로 예외 처리를 수행한다.
- 반면 @RestControllerAdvice는 RESTful API에서 반환하는 데이터의 형식이 JSON 또는 XML과 같은 데이터 형식인 경우에 사용다. 즉, 데이터를 반환하는 방식으로 예외 처리를 수행한다.
- @RestControllerAdvice를 사용한 예외 처리 공통화
- @RestControllerAdvice는 일종의 AOP로, @RestControllerAdvice를 붙인 클래스에 @ExceptionHandler, @InitBinder 또는 @ModelAttribute 애너테이션을 붙여 작성한 메서드들을 모든 컨트롤러 클래스에서 사용할 수 있게 해준다.
- @ControllerAdvice는 빈으로 다뤄진다.
- 기존의 컨트롤러에 할당되었던 예외 처리 메서드를 @RestControllerAdvice가 붙은 예외 처리 클래스에 이양하면, 예외 발생 시 Spring에서 해당 클래스에 작성된 예외처리 메서드를 실행한다.
비즈니스 로직에 대한 예외 처리
* 비즈지스적인 예외 던지기 및 예외 처리
- Checked Exception과 Unchecked Exception
- Checked Exception
- 컴파일 시점에 발생할 수 있는 예외들이다.
- 컴파일 시점에 에러가 발생할 수 있기 때문에 (프로그램이 실행조차 안될 수 있기 때문에), 컴파일 하기 전에(코드의 작성 단계에서) 반드시 예외처리를 해야한다. 그래서 Checked Exception이라고 불린다.
- IOException, ClassNotFoundException, SQLException 등이 대표적이다.
- Unchecked Exception
- 런타임 시점에 발생할 수 있는 예외들이다.
- 런타임 시점에서 에러가 발생할 수 있기 때문에 (프로그램이 실행은 되기 때문에), 코드 작성 단계에서 예외 처리를 강제하지는 않는다. 그래서 Unchecked Exception이라고 불린다.
- NullPointException, IndexOutOfBoundException 등이 대표적이다.
- 트랜잭션 롤백?
- 해당 정리본은 스프링에서의 예외처리를 다루기 때문에 기본적으로 런타임 예외는 롤백을 한다.
- 자세한 내용은 백기선님의 영상을 참조하자.
- 개발자가 의도적으로 예외를 던질 수 있는 상황
- 클라이언트에서 잘못된 입력을 보냈을 때.
- 형식에 맞지 않는 입력
- 서버 내부에 없거나 부족한 자원을 요청할 때
- 외부 API에 요청한 자원이 부족하거나 없을 때.
- 의도적인 예외 던지기 / 받기
- 서비스 계층의 모든 객체의 기능은 컨트롤러에서 사용한다.
- 즉, 서비스 계층에서 던진 예외는 컨트롤러에서 받게된다.
- 그런데, 컨트롤러에서 발생하는 예외는 Controller Advice 객체가 처리하도록 공통화했다.
- 결과적으로, 모든 예외는 Advice가 담당하여 처리한다.
- 사용자 정의 예외(Custom Exception) 사용
- 모든 예외를 한정된 예외 타입으로만 사용하는 것은 어떤 예외가 발생했는지를 명확하게 보여줄 수 없다.
- 특히 런타임 에러의 경우, 발생할 수 있는 예외가 다양하고 예측이 어렵기 때문에 더더욱 어떤 예외가 발생했는지를 명확하게 표현할 필요가 있다.
- 이럴 때, RuntimeException을 상속받은 예외 클래스를 만들어 사용해보자.
import lombok.Getter;
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member Not Found");
@Getter
private int status;
@Getter
private String message;
ExceptionCode(int status, String message) {
this.status = status;
this.message = message;
}
}
더보기
먼저, 반환할 http 상태 코드와 예외 내용을 담은 Enum 클래스를 만든다.
import lombok.Getter;
public class BusinessLogicException extends RuntimeException {
@Getter
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
더보기
RuntimeException을 상속받은 클래스를 만들고, 해당 클래스로 예외를 이양한다.
반응형
'CodeStatesBootCamp > Review' 카테고리의 다른 글
Section 3 - Unit 6 : [Spring MVC] 트랜잭션 Review (0) | 2023.04.26 |
---|---|
Section 3 - Unit 4 : [Spring MVC] JDBC 기반 데이터 액세스 계층 Review (0) | 2023.04.18 |
Section 3 - Unit 1 : [Spring MVC] API 계층 Review (0) | 2023.04.12 |
Section 2 - Section Review (1) | 2023.04.11 |
Section 2 - Unit 7 : Spring Framework 기본 Review (0) | 2023.03.31 |
댓글