1. Spring 예외 종류
2. 예외 발생 시 동작 순서
3. 예외 처리를 위한 클래스 생성 순서
4. 일반적인 예외 처리 클래스 생성 순서
5. 의도적인 비즈니스 로직 예외 처리
6. 서비스 메서드 발생의 경우 예외 발생시 처리 순서
- 추후 다시 확인 필요 // 구글드라이브에 올려진 be-homework-exception 알집 참고
[Spring 기반 애플리케이션의 예외 종류]
1. 클라이언트 요청 데이터에 대한 유효성 검증 에서 발생하는 예외
2. 서비스 계층의 비즈니스 로직에서 던져지는 의도된 예외
3. 웹 애플리케이션 실행 중에 발생하는 예외 (Runtime Exception)
[예외 발생 시 동작 순서]
1. 클라이언트가 DTO 타입의 데이터로 요청 메시지 전송.
2. Controller 클래스-핸들러 메서드가 요청으로 받은 DTO 데이터를 유효성 검증을 진행.
3. 예외 발생할 경우, 예외 처리 클래스인 @RestControllerAdvice annoted 클래스로 전달.
4. @RestControllerAdvice 클래스에서 예외에 해당하는 핸들러 메서드의 응답 데이터를
ErrorResponse 클래스에서 가져옴.
5. @RestControllerAdvice 클래스는 ErrorResponse 클래스로부터 받은
응답 데이터 ErrorReponse 객체를 클라이언트에게 전달.
[예외 처리를 위한 클래스 생성 순서]
1. ErrorResponse 클래스 생성
2. @RestControllerAdvice 애너테이션을 가진 GlobalExceptionAdvice 클래스 생성.
예외별로 발생할 예외 Controller - 핸들러 메서드 작성 (Post, Patch, Get, Delete)
3. ErrorResponse 클래스에 GlobalExceptionAdvice 클래스의
예외 메서드에 전달할 응답 데이터 생성 코드 작성
<일반적인 예외 처리 클래스 순서>
1. ErrorResponse 클래스 생성
예외 발생의 응답 메시지에 전달할 데이터들을 필드로 선언
@Getter
@AllArgsConstructor
public class ErrorResponse {
}
2. 예외 공통 처리를 위해 GlobalExceptionAdvice 클래스 생성 후 예외 별 핸들러 메서드 생성
A. 클래스 레벨에 @RestContollerAdvice 애너테이션 작성.
B. 각 메서드마다 @ExceptionalHandler 애너테이션 작성.
C. 핸들러 메서드 타입은 2가지 종류로 나눌 수 있다.
C-1. ErrorResponse 타입
@ExceptionalHandler + @ResponseStatus(HttpStatus. ~ ) 추가하여 Http 코드까지 반환.
C-2. ResponseEntity 타입 // ErrorResponse + HttpStatus 합쳐진 타입.
@ExceptionalHandler 애너테이션만 필요.
D. 메서드 이름은 handle + 예외의 이름/종류로 지정하고 파라미터는(예외명 e) 선언.
E. 동작문에는 final ErrorResponse response = ErrorResponse.of(e.get예외명());
F. Return 객체 타입은 메서드 시그니처의 메서드 타입과 동일해야 한다.
i) ErrorResponse 타입이면
메서드 레벨에 @ResponseStatus(HttpStatus.~~) 사용, 반환 객체는 ErrorResponse타입만 반환.
ii)ResponseEntity 타입이면, return 위치에 ErrorResponse객체와 HttpStatus 함께 작성.
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(
ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
}
3. ErrorResponse 클래스 작성
A. 예상되는 예외들과 응답 메시지 내에 전달할 항목들을 필드에 선언. = 필드들은 응답 메시지에 포함됨.
B. 멤버 클래스로 생성된 예외들은 List<예외_클래스명> 으로 필드 선언.
C. 예상되는 예외들은 정보를 간추리기 위해 ErrorResponse 클래스의 멤버 클래스로 선언
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors; // (1)
private List<ConstraintViolationError> violationErrors; // (2)
// (3)
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
// (4) BindingResult에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult), null);
}
// (5) Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
// (6) Field Error 가공
@Getter
public static class FieldError {
private String field; // 예외 발생 필드
private Object rejectedValue;// 예외발생 필드에 입력된 값
private String reason; // 예외 발생 이유
private FieldError(String field, Object rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<FieldError> of(BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors =
bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ?
"" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
}
// (7) ConstraintViolation Error 가공
@Getter
public static class ConstraintViolationError {
private String propertyPath;
private Object rejectedValue;
private String reason;
private ConstraintViolationError(String propertyPath, Object rejectedValue,
String reason) {
this.propertyPath = propertyPath;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<ConstraintViolationError> of(
Set<ConstraintViolation<?>> constraintViolations) {
return constraintViolations.stream()
.map(constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()
)).collect(Collectors.toList());
}
}
}
[비즈니스 적인 예외 던지기 및 예외 처리]
<체크 예외(Chekced Exception) 와 언체크 예외 (Unchekced Exception)>
1. 체크 예외 (Checked Exception)
발생한 예외를 잡아서, 체크한 후 해당 예외를 복구 하던가, 아니면 회피 하던가 등의 구체적인 처리가 필요.
Ex) ClassNotFoundException
2. 언체크 예외 (Unchecked Exception)
발생한 예외를 잡아서, 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외.
<개발자가 의도적으로 예외를 던질 수 있는 경우>
1. 백엔드 서버와 외부 시스템과의 연동에서 발생하는 에러 처리.
Ex) A가 B에게 송금하려 하였으나, A의 잔고가 부족하다는 메시지를 받고 프로세스 중단.
이런 경우, 잔고가 부족한 상황을 클라이언트 쪽에 알려야 하는 상황이므로,
백엔드 서버 쪽에서 예외를 의도적으로 던져서 클라이언트 쪽에 에러가 발생한 정보를 알려줘야 한다.
2. 시스템 내부에서 조회하려는 리소스 자원이 없는 경우
회원 정보를 조회하려고 클라이언트 쪽에서 Get 요청을 보냈으나 DB에 정보가 없다.
이런 경우 [서비스 계층]에서 회원정보가 없다는 예외를 의도적으로 전송하여 클라이언트에게 알려야함.
< 의도적인 예외 던지기 / 받기 >
[서비스 계층]에서 예외를 던지면 서비스 계층 메서드를 사용하는 [API 계층]에서 처리해야 한다.
하지만 [API 계층]에서 발생하는 예외는 GlobalExceptionAdvice 클래스에서 처리하므로,
[서비스 계층]에서 발생한 예외 또한 GlobalExceptionAdvice 클래스에서 처리하면 된다.
<사용자 정의 예외 사용 (Custom Exception)>
1. 커스텀 예외 enum 클래스 생성.
2. enum 클래스를 사용할 BusinessLogicException 클래스 생성 및 구현.
3. [서비스 계층] 메서드에서 BusinessLogicException을 사용하도록 셋팅
4. GlobalExceptionAdvice 클래스에서 handleBusinessLogiceException라는 예외 전용 메서드 생성
5. 위의 메서드가 반환하는 객체 response를 생성하는 메서드를 ErrorResponse 클래스에 생성함.
<BusinessLogicException 클래스>
import lombok.Getter;
public class BusinessLogicException extends RuntimeException {
@Getter
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
RuntimeException이라는 상위 클래스를 상속받음으로써 제공받는 예외의 정보보다
상세한 정보를 클라이언트에게 전달하기위해 커스텀 예외 enum 클래스에 추가된 예외 정보 중
필요한 예외 정보만 뽑아서 응답 데이터에 실어주는 역.
즉 커스텀 에외 클래스에서 원하는 데이터만 골라 응답으로 주는 역.
<사용자 정의 예외 사용 (Custom Exception)를 사용한 예외 처리 순서>
1. 커스텀 예외 클래스 (enum)
추가하고 싶은 예외 응답 메시지, 코드를 추가한다.
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error");
@Getter
private int status;
@Getter
private String message;
ExceptionCode(int code, String message) {
this.status = code;
this.message = message;
}
}
2. BusinessLogicException 클래스 작성
커스텀 예외 클래스 중에 원하는 정보만 응답 데이터로 실어주는 역할.
public class BusinessLogicException extends RuntimeException {
@Getter
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
3. BusinessLogicException 클래스 작성
[서비스 계층]의 메서드에 BusinessLogicException 클래스를 적용
public Member findMember(long memberId) {
throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
}
4. GlobalExceptionAdvice 클래스에서 BusinessLogicException 처리 용 메서드 틀만 작성.
@ExceptionHandler
public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
}
5. GlobalExceptionAdvice 클래스에 예외별 응답 데이터를 생성하는 ErrorResponse 클래스 작성.
@Getter
public class ErrorResponse {
(1) private int status;
private String message;
private List<FieldError> fieldErrors;
private List<ConstraintViolationError> violationErrors;
private ErrorResponse(final ExceptionCode exceptionCode) {
this.status = exceptionCode.getStatus();
this.message = exceptionCode.getMessage();
}
private ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
(2)
public static ErrorResponse of(ExceptionCode exceptionCode) {
return new ErrorResponse(exceptionCode.getStatus(),exceptionCode.getMessage());
}
public static ErrorResponse of
(HttpRequestMethodNotSupportedException httpRequestMethodNotSupportedException) {
return new ErrorResponse(ExceptionCode.METHOD_NOT_ALLOWED);
}
public static ErrorResponse of(NullPointerException nullPointerException) {
return new ErrorResponse(ExceptionCode.INTERNAL_SERVER_ERROR);
}
(1) : ErrorResponse의 필드들은 응답 데이터 메시지에 포함된다.
(2) : 예외 별로 of 메서드를 생성하고 enum에 추가해둔 예외 코드를 포함한 ErrorResponse 객체를 반환.
6. GlobalExceptionAdvice 클래스의 메서드 완성.
@ExceptionHandler
public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
// TODO GlobalExceptionAdvice 기능 추가 1
final ErrorResponse response = ErrorResponse.of(e.getExceptionCode());
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}
// TODO GlobalExceptionAdvice 기능 추가 2
@ExceptionHandler
public ResponseEntity handleHttpRequestMethodNotSupportedException
(HttpRequestMethodNotSupportedException e) {
final ErrorResponse response = ErrorResponse.of(e);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}
// TODO GlobalExceptionAdvice 기능 추가 3
@ExceptionHandler
public ResponseEntity handleException(NullPointerException e) {
final ErrorResponse response = ErrorResponse.of(e);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}
}
<추가 사항>
위의 2, 3번 기능 추가 메서드는 ErrorResponse 클래스에 of 메서드 + ExceptionCode 클래스 생성자 추가해야함.
<생성자>
private ErrorResponse (ExceptionCode exceptionCode) {
this.status = exceptionCode.getStatus();
this.message = exceptionCode.getMessage();
}
---------------------------------------------------------
<메서드>
public static ErrorResponse of
(HttpRequestMethodNotSupportedException httpRequestMethodNotSupportedException) {
return new ErrorResponse(ExceptionCode.METHOD_NOT_ALLOWED);
}
public static ErrorResponse of(NullPointerException nullPointerException){
return new ErrorResponse(ExceptionCode.ITERNAL_SERVER_ERROR);
}
[Service 계층의 서비스 메서드에서 예외 발생할 경우 지정한 코드가 작동하는 순서]
1. [서비스 계층]에 있는 서비스 메서드에서 발생하는 예외를 미리 작성해둔다.
public Member findMember(long memberId) {
// should business logic
throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
}
public List<Member> findMembers() {
throw new BusinessLogicException(ExceptionCode.TOO_LAZY);
2. 특정 서비스 메서드에서 예외시 BusinessLogicException 객체가 생성되며
Controller 핸들러 메서드에서 발생한 예외는
ErrorResponse 클래스로부터 나온다.
public class BusinessLogicException extends RuntimeException {
@Getter
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
3. BusinessLogicException 객체가 생성되면 exceptionCode.getMessage와 exceptionCode 객체가 호출된다.
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
TOO_LAZY(999, "I am too lazy to find all those info");
@Getter
private int status;
@Getter
private String message;
ExceptionCode(int code, String message) {
this.status = code;
this.message = message;
}
}
4. 이는 ErrorResponse 클래스에게 전달
2023.01.09 기준으로 아직 원리를 정확히 파악 못하겠다. 나중에 다시 확인하자
<핵심 포인트>
* [ErrorResponse 클래스]
예외 발생 시, 필요한 정보만 담아서 예외 응답 데이터를 전달할 수 있도록 필터링 하는 클래스.
* [GlobalExceptionAdvice 클래스]
모든 Controller 클래스에서 발생하는 예외들을 공통 처리 하는 클래스
* <BusinessLogicException 클래스>
커스텀 에외 클래스에서 원하는 데이터만 골라 응답으로 주는 역.
@ExceptionalHandler
Controller 클래스에서 예외 처리하는 메서드에서 사용.
@ResponseStatus(HttpStatus.~~)
해당 예외 메서드는 고정된 HttpStatus를 사용한다.
@RestControllerAdvice
여러 컨트롤러 클래스에서 발생하는 예외를 공통 처리하는 예외Controller 클래스임을 나타냄.
@ResponseEntity VS @ErrorResponse 차이.
@ResponseEntity의 경우
ErrorResponse 객체, HttpStatus를 같이 제공하여 HttpStatus를 유동적으로 전달한다.
@ErrorResponse의 경우
예외 처리 메서드명에 @ResponseStatus(HttpStatus.메시지_선택) 을 붙여서
HttpStatus를 고정적으로 전달한다.
'백엔드 학습 과정 > Section 3 [Spring MVC, JDBC, JPA, RestDo' 카테고리의 다른 글
Spring Data JDBC - Service 클래스 기능별 코드 (0) | 2022.12.31 |
---|---|
#4. Spring Data JDBC (0) | 2022.12.23 |
#2. Spring MVC [Service 계층] - 역할, 생성, 적용, 필요 애너테이션 (0) | 2022.12.20 |
** 참고 ** REST-API (0) | 2022.12.17 |
#1-1. Rest Client (0) | 2022.12.17 |