오류 처리는 프로그램을 개발하는데 있어서 매우 큰 부분을 차지한다. 오류를 예측해서 비정상적인 상황이 발생하지 않게 하는 것은 정말 중요하다.
1. @ControllerAdvice 란?
- @Controller나 @RestController에서 발생한 예외를 한 곳에서 관리하고 처리할 수 있게 도와주는 어노테이션이다.
2. @ControllerAdvice 예제 코드
- 동일한 유형의 Error Response 반환
- 확장성이 용이한 Custom Exception의 사용
1) ErrorCode.class
예외에 대한 정보를 담고있는 enum class 이다.
public enum ErrorCode {
INVALID_PARAMETER(400, null, "Invalid Request Data"),
COUPON_EXPIRATION(410, "C001", "Coupon Was Expired"),
COUPON_NOT_FOUND(404, "C002", "Coupon Not Found");
private final String code;
private final String message;
private final int status;
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
public int getStatus() {
return status;
}
ErrorCode(final int status, final String code, final String message) {
this.status = status;
this.message = message;
this.code = code;
}
}
2) ErrorResponse.class
예외에 대한 응답 정보를 저장하는 클래스이다.
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
public class ErrorResponse {
private LocalDateTime timestamp = LocalDateTime.now();
private String message; //예외 메시지 저장
private String code; // 예외를 세분화하기 위한 사용자 지정 코드,
private int status; // HTTP 상태 값 저장 400, 404, 500 등..
//@Valid의 Parameter 검증을 통과하지 못한 필드가 담긴다.
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonProperty("errors")
private List<CustomFieldError> customFieldErrors;
public ErrorResponse() {
}
static public ErrorResponse create() {
return new ErrorResponse();
}
public ErrorResponse code(String code) {
this.code = code;
return this;
}
public ErrorResponse status(int status) {
this.status = status;
return this;
}
public ErrorResponse message(String message) {
this.message = message;
return this;
}
public ErrorResponse errors(Errors errors) {
setCustomFieldErrors(errors.getFieldErrors());
return this;
}
/*
getter 생략
*/
//BindingResult.getFieldErrors() 메소드를 통해 전달받은 fieldErrors
public void setCustomFieldErrors(List<FieldError> fieldErrors) {
customFieldErrors = new ArrayList<>();
fieldErrors.forEach(error -> {
customFieldErrors.add(new CustomFieldError(
error.getCodes()[0],
error.getRejectedValue(),
error.getDefaultMessage()
));
});
}
//parameter 검증에 통과하지 못한 필드가 담긴 클래스이다.
public static class CustomFieldError {
private String field;
private Object value;
private String reason;
public CustomFieldError(String field, Object value, String reason) {
this.field = field;
this.value = value;
this.reason = reason;
}
public String getField() {
return field;
}
public Object getValue() {
return value;
}
public String getReason() {
return reason;
}
}
}
1) CustomException.class
쿠폰 만료 예외, 존재하지 않는 쿠폰 예외, 쿠폰 중복 참여 예외 등 여러가지 예외의 기본이 되는 클래스이다. 모든 예외 정보는 ErrorCode를 통해서 전달 받는다.
public class CustomException extends RuntimeException {
private static final long serialVersionUID = 1L;
private ErrorCode errorCode;
public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
2) CouponExpirationException.class
쿠폰이 만료 되었을 경우, 발생하는 예외 클래스이다.
public class CouponExpirationException extends CustomException {
private static final long serialVersionUID = -2116671122895194101L;
public CouponExpirationException() {
super(ErrorCode.COUPON_EXPIRATION);
}
}
3) CouponNotFoundException.class
입력한 쿠폰을 찾을 수 없을 경우, 발생하는 예외 클래스이다.
public class CouponNotFoundException extends CustomException {
private static final long serialVersionUID = -2116671122895194101L;
public CouponNotFoundException() {
super(ErrorCode.COUPON_NOT_FOUND);
}
}
4) InvalidParameterException.class
public class InvalidParameterException extends CustomException {
private static final long serialVersionUID = -2116671122895194101L;
private final Errors errors;
public InvalidParameterException(Errors errors) {
super(ErrorCode.INVALID_PARAMETER);
this.errors = errors;
}
public Errors getErrors() {
return this.errors;
}
}
5) ControllerExceptionHandler.class
@ControllerAdvice
public class ControllerExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
logger.error("handleHttpRequestMethodNotSupportedException", e);
final ErrorResponse response
= ErrorResponse
.create()
.status(HttpStatus.METHOD_NOT_ALLOWED.value())
.message(e.getMessage());
return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
}
//@Valid 검증 실패 시 Catch
@ExceptionHandler(InvalidParameterException.class)
protected ResponseEntity<ErrorResponse> handleInvalidParameterException(InvalidParameterException e) {
logger.error("handleInvalidParameterException", e);
ErrorCode errorCode = e.getErrorCode();
ErrorResponse response
= ErrorResponse
.create()
.status(errorCode.getStatus())
.message(e.toString())
.errors(e.getErrors());
return new ResponseEntity<>(response, HttpStatus.resolve(errorCode.getStatus()));
}
//CustomException을 상속받은 클래스가 예외를 발생 시킬 시, Catch하여 ErrorResponse를 반환한다.
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
logger.error("handleAllException", e);
ErrorCode errorCode = e.getErrorCode();
ErrorResponse response
= ErrorResponse
.create()
.status(errorCode.getStatus())
.code(errorCode.getCode())
.message(e.toString());
return new ResponseEntity<>(response, HttpStatus.resolve(errorCode.getStatus()));
}
//모든 예외를 핸들링하여 ErrorResponse 형식으로 반환한다.
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e) {
logger.error("handleException", e);
ErrorResponse response
= ErrorResponse
.create()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.message(e.toString());
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
- @ControllerAdvice : Controller에서 발생한 예외를 Catch하여 처리하는 클래스이다.
basePackageClasses, basePackages 속성을 이용하여 특정 클래스에서 발생하는 예외만 받아서 처리할 수 있다. - @ExceptionHandler : 메소드 위에 선언되며, 등록한 예외를 Catch하여 처리할 수 있다.
- ResponseEntity : Header와 Body에 응답 객체를 저장하여 반환할 수 있는 클래스이다.
6) Member.class
public class Member {
@NotBlank(message = "name은 필수 값입니다.")
private String name;
@NotBlank(message = "email은 필수 값입니다.")
private String email;
@Min(value = 18, message = "18살 이상만 입장 가능합니다.")
private int age;
@NotNull(message = "금액은 필수 값 입니다.")
@Min(value = 10000, message = "10000원 이상 소지해야합니다.")
private Integer money;
//getter,setter 생략
}
7) HomeController.class
@Controller
@RequestMapping(value = "/")
public class HomeController {
@GetMapping("/member")
public String memberException(@Valid Member dto, BindingResult result) {
if (result.hasErrors()) {
throw new InvalidParameterException(result);
}
return "page/home";
}
@GetMapping("/exception")
public String exceptionTest(String code) {
switch (code) {
case "1":
throw new CouponExpirationException();
case "2":
throw new CouponNotFoundException();
case "3":
int a = 3 / 0;
break;
}
return "page/home";
}
}
3. 테스트
1) GET http://localhost:8080/SpringBootException/member
에러 응답
{
"timestamp": "2020-06-23T17:42:35.51",
"message": "com.example.bamdule.exception.InvalidParameterException: Invalid Request Data",
"status": 400,
"fieldErrors": [
{
"field": "NotBlank.member.name",
"value": null,
"reason": "name은 필수 값입니다."
},
{
"field": "Min.member.age",
"value": 0,
"reason": "18살 이상만 입장 가능합니다."
},
{
"field": "NotBlank.member.email",
"value": null,
"reason": "email은 필수 값입니다."
},
{
"field": "NotNull.member.money",
"value": null,
"reason": "금액은 필수 값 입니다."
}
]
}
2) GET http://localhost:8080/SpringBootException/exception?code=2
에러 응답
{
"timestamp": "2020-06-23T17:46:25.628",
"message": "com.example.bamdule.exception.CouponNotFoundException: Coupon Not Found",
"status": 404,
"code": "C002"
}
'IT > Spring' 카테고리의 다른 글
[Spring] Ajax를 이용해 데이터를 서버로 보내는 몇 가지 방법 (0) | 2020.07.01 |
---|---|
[Spring Boot] 클라이언트 디바이스 정보 확인하기 (0) | 2020.06.17 |
[Spring Boot] spring boot devtools (0) | 2020.02.27 |
[Spring Boot] Spring Security 적용하기 (5) | 2020.02.09 |
[Spring Boot] Spring Security의 동작 (0) | 2020.02.07 |