본문 바로가기
프로젝트/PongGame

Account_exceptionHandler

by 히포파타마스 2022. 5. 11.

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

댓글