Account_exceptionHandler
API 호출 시 exception이 발생할 경우 BasicErrorController에 의해 에러에 대한 정보가 Json으로 반환된다.
exception에 의해 기본적으로 반환되는 Json은 다음과 같이 그 형태가 정해져 있다.
[BasicErrorController_exception 문구]
{
"timestamp": "2021-04-28T00:00:00.000+00:00", "status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"trace": "java.lang.RuntimeException: 잘못된 사용자\n\tat
hello.exception.web.api.ApiExceptionController.getMember(ApiExceptionController .java:19...,
"message": "잘못된 사용자", "path": "/api/members/ex"
}
API의 경우 각 오류 상황에 맞는 오류 응답 스펙을 정하고 Json으로 데이터를 내려줘야 한다.
물론 BasicErrorController를 확장하면 반환되는 Json의 형태를 변경할 수 있지만 각 API마다 오류 메시지, 형식 등을 다르게 처리하기 어렵다.
따라서 위 문제를 해결하기 위해 스프링이 제공하는 ExceptionHandlerExceptionResolver의 @ExceptionHanler 어노테이션을 사용해서 각 Exception에 대한 처리를 한다.
1. ErrorDto
에러 발생 시, 발생한 에러에 대한 정보를 담고 반환될 DTO를 만든다.
■ BasicErrorResult
일반적인 excetion이 발생했을 때, 반환되는 DTO
[BasicErrorResult]
@Data
@AllArgsConstructor
public class BasicErrorResult {
private int state;
private String exception;
private String message;
}
상태 코드값, exception의 이름, exception이 갖고 있는 message를 필드로 갖고 있다.
■ FormErrorResult
@Valid에 의해 에러가 발생했을 때 반환되는 DTO
[FormErrorResult]
@Data
@AllArgsConstructor
public class FormErrorResult {
private int state;
private String exception;
private ArrayList<FieldErrorDto> fieldErrorList;
private ArrayList<GlobalErrorDto> globalErrorList;
@Data
@AllArgsConstructor
static public class FieldErrorDto {
private String field;
private String code;
private String message;
}
@Data
@AllArgsConstructor
static public class GlobalErrorDto {
private String code;
private String message;
}
}
상태 코드, exception의 이름, 그리고 FileldErrorDTO와 GlobalErorrDTO의 리스트를 필드로 갖고 있다.
□ FieldErrorDto
[FieldErrorDto]
@Data
@AllArgsConstructor
static public class FieldErrorDto {
private String field;
private String code;
private String message;
}
FieldError가 갖고 있는 field와 code, message를 필드로 갖고 있다.
□ GlobalErrorDto
[GlobalErrorDto]
@Data
@AllArgsConstructor
static public class GlobalErrorDto {
private String code;
private String message;
}
GlobalError는 field가 없기 때문에 code와 messge만 필드로 갖고 있다.
2. @ExceptionHandler
본 프로젝트에서 Exception에 대한 처리는 모든 컨트롤러에 대해 공통적으로 처리되도록 상정했다.
따라서 Exception을 처리하는 @ExceptionHandler과 관련된 메서드는 ControllerAdvice를 사용해서 컨트롤러에 전역적으로 적용한다.
■ ExceptionControllerAdvice
[ExceptionControllerAdvice]
@RestControllerAdvice(annotations = RestController.class)
@Component
@RequiredArgsConstructor
public class ExceptionControllerAdvice {
private final MessageSource messageSource;
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler
public BasicErrorResult NonExistResourceExceptionHandler(NonExistResourceException exception) {
return new BasicErrorResult(HttpStatus.NOT_FOUND.value(), exception.getClass().getSimpleName(),
exception.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public FormErrorResult IllegalFormExceptionHandler(BindException exception) {
List<FieldError> fieldErrors = exception.getFieldErrors();
List<ObjectError> globalErrors = exception.getGlobalErrors();
return new FormErrorResult(HttpStatus.BAD_REQUEST.value(), exception.getClass().getSimpleName(),
getFieldErrorList(fieldErrors), getGlobalErrorList(globalErrors));
}
private ArrayList<FieldErrorDto> getFieldErrorList(List<FieldError> fieldErrors) {
if (fieldErrors == null) {
return new ArrayList<>();
}
return fieldErrors.stream()
.map(error -> new FieldErrorDto(error.getField(), error.getCode(), getErrorMessage(error)))
.collect(Collectors.toCollection(ArrayList::new));
}
private ArrayList<GlobalErrorDto> getGlobalErrorList(List<ObjectError> globalErrors) {
if (globalErrors == null) {
return new ArrayList<>();
}
return globalErrors.stream()
.map(error -> new GlobalErrorDto(error.getCode(), getErrorMessage(error)))
.collect(Collectors.toCollection(ArrayList::new));
}
private String getErrorMessage(ObjectError error) {
String[] codes = error.getCodes();
for (String code : codes) {
try {
return messageSource.getMessage(code, null, Locale.KOREA);
} catch (NoSuchMessageException ignored) {}
}
return error.getDefaultMessage();
}
}
□ NonExistResourceExceptionHandler
NonExistResourceException을 처리하는 Handler
[NonExistResourceExceptionHandler]
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler //[1]
public BasicErrorResult NonExistResourceExceptionHandler(NonExistResourceException exception) {
return new BasicErrorResult(HttpStatus.NOT_FOUND.value(), exception.getClass().getSimpleName(),
exception.getMessage()); //[2]
}
컨트롤러에서 Exception이 발생했을 경우 해당 메서드가 실행되고 설정한 값이 반환된다.
● [1] : @ExceptionHandler가 붙은 메서드가 매개변수로 받는 Exception이 대상이 된다.
● [2] : BasicErrorResult가 반환되며, 발생한 exception에서 exception의 이름과 message를 찾아 생성된다.
▣ NonExistResourceException
[NonExistResourceException]
public class NonExistResourceException extends RuntimeException{
public NonExistResourceException() {
}
public NonExistResourceException(String message) {
super(message);
}
public NonExistResourceException(String message, Throwable cause) {
super(message, cause);
}
public NonExistResourceException(Throwable cause) {
super(cause);
}
public NonExistResourceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
자원을 조회할 때 자원이 없을 때 발생하는 Exception
RuntimeException을 상속받고 메서드를 overrid 해서 생성.
□ BindExceptionHandler
@Valid에 의한 검증 시, 에러가 존재하면 BindException이 발생한다.
이 경우 BindException을 처리하는 Handler.
[BindExceptionHandler]
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public FormErrorResult BindExceptionHandler(BindException exception) {
List<FieldError> fieldErrors = exception.getFieldErrors(); //[1]
List<ObjectError> globalErrors = exception.getGlobalErrors();
//[2]
return new FormErrorResult(HttpStatus.BAD_REQUEST.value(), exception.getClass().getSimpleName(),
getFieldErrorList(fieldErrors), getGlobalErrorList(globalErrors)); //[3]
}
● [1] : BindException은 fieldError와 globalError의 List를 갖고 있다.
● [2] : 반환 값은 FormErrorResult
● [3] : getFieldErrorList()와 getGlobalErrorList()로 filedErrors와 globalError를 FieldErorrDTO와 GlobalErrorDTO의 리스트로 변환하고, 해당 값들을 FormErrorResult의 생성 초기값으로 설정해준다.
▣ getFieldErrorList
[getFieldErrorList]
private ArrayList<FieldErrorDto> getFieldErrorList(List<FieldError> fieldErrors) {
if (fieldErrors == null) {
return new ArrayList<>();
}
return fieldErrors.stream()
.map(error -> new FieldErrorDto(error.getField(), error.getCode(), getErrorMessage(error)))
.collect(Collectors.toCollection(ArrayList::new));
}
List<FieldError>를 ArrayList<FieldErrorDto>로 변환하는 메서드
▣ getGlobalErrorList
[getGlobalErrorList]
private ArrayList<GlobalErrorDto> getGlobalErrorList(List<ObjectError> globalErrors) {
if (globalErrors == null) {
return new ArrayList<>();
}
return globalErrors.stream()
.map(error -> new GlobalErrorDto(error.getCode(), getErrorMessage(error)))
.collect(Collectors.toCollection(ArrayList::new));
}
List<ObjectError>를 ArrayList<GlobalErrorDto>로 변환하는 메서드
⊙ getErrormessage
[getErrormessage]
private String getErrorMessage(ObjectError error) {
String[] codes = error.getCodes();
for (String code : codes) {
try {
return messageSource.getMessage(code, null, Locale.KOREA);
} catch (NoSuchMessageException ignored) {}
}
return error.getDefaultMessage();
}
error(ObjectError)의 code에 맞는 message가 errors.properties에 정의되어있으면 해당 message를 반환한다.
정의돼있지 않다면 erorr의 defaultMessage를 반환한다.
3. errors.properties
@Valid에 의해 생성되는 code패턴은 다음과 같다.
[code 생성 규칙]
1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code
순서는 우선도 순이다.
따라서 위의 규칙에 따라 errors.properties에 code에 따른 메시지를 정의하면 error의 code에 따라 정의된 메시지를 사용할 수 있다.
code값을 직접 넣지 않는 Bean Validation 같은 경우는 어노테이션의 문구가 code가 된다.
ex) AccountForm의 nickname에 @NotBlack가 붙을 경우 NotBlank.accountForm.nickname과 같은 코드가 생성된다.
■ errors.properties
[errors.properties]
# username
Email.accountForm.username = 이메일 형식이 아닙니다.
NotBlank.accountForm.username = 아이디가 비어있습니다.
UsernameDuplication = 이미 사용중인 아이디입니다.
# password
NotBlank.accountForm.password = 비밀번호가 비어있습니다.
Length.accountForm.password = 비밀번호는 4자리 이상 30자리 이하이여야 합니다.
WrongPassword = 비밀번호가 맞지 않습니다.
SamePassword = 현재 비밀번호와 변경할 비밀번호가 같을 수 없습니다.
Discord = 새 비밀번호와 확인 항목이 일치하지 않습니다.
# nickname
Length.nickname = 닉네임은 1자리 이상 30자리 이하이여야 합니다.
Pattern.nickname = 닉네임에 특수문자나 공백을 기입할 수 없습니다.
NotBlank.nickname = 닉네임이 비어있습니다.
NicknameDuplication = 이미 사용중인 닉네임입니다.
# profileFile
BlankFileName = 파일 이름이 공백이거나 비어있습니다.
NonExtractFileName = 확장자가 없습니다.
# birthDate
typeMismatch.accountForm.birthDate = 생년월일은 'yyyy-MM-dd'과 같은 형식 이여야 합니다.
FutureBirthDate = 생년월일이 당일보다 늦을 수 없습니다.
#
typeMismatch = "잘못된 형식입니다."
NotNull = null값이 들어갈 수 없습니다.
typeMismatch는 Form이 입력될 때 잘못된 타입이나 형태가 다를 경우 생성되는 code이다.
ex) gender에 숫자를 넣거나 생년월일에 19999-999 같은 값을 넣었을 때 발생
'프로젝트 > PongGame' 카테고리의 다른 글
Account Controller (0) | 2022.05.11 |
---|---|
Account Service (0) | 2022.05.11 |
Account Validation (0) | 2022.05.10 |
Account Repository (0) | 2022.05.10 |
Account API 명세 (0) | 2022.05.05 |
댓글