예외처리

roach·2021년 3월 31일
0

스프링

목록 보기
2/2

서론

최근 예외처리에 대해 많이 여러가지 이유로 공부하게 된것 같다. 백기선님 자바 스터디 및 스프링 Q&A 를 통해서, 예전에 코드스쿼드 채팅에서 Dan 님의 예외처리 코드를 봤던 기억이 났는데, 오늘 그 기억이 나서 예외처리에 대한 코드를 짜보았다.

예외 Response 를 리턴하자.

오늘 야자를 하고 있는데 Cooper 가 물어봤던 질문에 대한 답이 되는 코드일 수도 있다. 일단 Event 라는 하나의 클래스를 만들자.

Event class

@Getter
@Setter
public class Event {

    private Integer id;

    @NotBlank
    private String name;

    @Min(value = 0)
    private Integer limit;
    
}
  • 일단 뭐 간단하게 name 은 빌수가 없고, limit 는 0 이하로 줄어들 수 없다. 나중에 개인적으로 Validation 을 직접 구현하는 글도 하나 적어볼려고 한다. 어려운 부분은 아니라, 별로 안걸릴것 같다. 잡담은 그만하고 이제 Controller 를 간단하게 만들자.

SampleController

  @PostMapping("/events")
    @ResponseBody
    public ResponseEntity<?> getEvent(@Validated @ModelAttribute Event event, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResponseEntity
                    .badRequest()
                    .body(ErrorResponse.of(ErrorCode.BAD_REQUEST, bindingResult, UUID.randomUUID()));
        }
        return ResponseEntity.ok().body(event);
    }

위와 같이 이제 Event 가 @ModelAttribute 를 통해서 생성되는 도중 @Validation 에 걸리는 내용이 있다면, bindingResult 에 에러 정보가 담긴다.

음 BindingResult 에 대해서 그냥 가볍게 설명하면 DataBinder 가 값을 Binding 할때, 바인딩의 결과는 BindingResult 를 통해서 검증될수 있는데, 뭐 이때 필드상의 Validate 에 문제가 있으면, 뭐 BindException 이 나는것 같은데 그걸 BindingErrorProcessor 의 startegy 로 처리하는데 그때 아래코드처럼 진행하는것 같다.

아래는 기본 전략인 DefaultBindingErrorProcessor 의 코드다.

public void processMissingFieldError(String missingField, BindingResult bindingResult) {
        String fixedField = bindingResult.getNestedPath() + missingField;
        String[] codes = bindingResult.resolveMessageCodes("required", missingField);
        Object[] arguments = this.getArgumentsForBindError(bindingResult.getObjectName(), fixedField);
        FieldError error = new FieldError(bindingResult.getObjectName(), fixedField, "", true, codes, arguments, "Field '" + fixedField + "' is required");
        bindingResult.addError(error);
    }

뭐 더 자세히 보려면 시간이 조금 더 걸릴것 같아서 귀찮으니 패스!
뭐 근데 이렇게 공식문서를 보면서 공부하는게 참 도움이 많이 된다. 타고 타고 들어가다보면 대략적으로 구조를 이해할 수 있고, 에러를 잡는데 많은 도움이 된다.

여튼 저런 이유로 BindingResult 를 우린 사용할 것이다. 아래가 SampleController 의 코드이다.

   @PostMapping("/api/events")
    @ResponseBody
    public ResponseEntity<?> getEvent(@Validated @ModelAttribute Event event, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return ResponseEntity
                    .badRequest()
                    .body(ErrorResponse.of(ErrorCode.BAD_REQUEST, bindingResult, UUID.randomUUID()));
        }
        return ResponseEntity.ok().body(event);
    }

간단히 설명하면 우리는 Response로 정상적인 요청일시 event 를 Response body 로 넘겨줄 것이고, 정상적이지 않은 값일시 ErrorResponse 를 리턴해줄 것이다. 그게 우리의 목표다.

ErrorCode

그렇담 먼져, Error를 분류해줄 ErrorCode enum 부터 만들어보자.

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

    BAD_REQUEST("1000", HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
    UNAUTHORIZED("1001", HttpStatus.UNAUTHORIZED, "인증에 실패했습니다."),
    FORBIDDEN("1002", HttpStatus.FORBIDDEN, "권한이 없습니다."),
    NOT_FOUND("1003", HttpStatus.NOT_FOUND, "찾을 수 없습니다."),
    METHOD_NOT_ALLOWED("1004", HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 메소드입니다."),
    UNKNOWN("1005", HttpStatus.INTERNAL_SERVER_ERROR, "알수 없는 서버에러");

    private final String code;
    private final HttpStatus httpStatus;
    private final String reason;

}

뭐 간단히 만들었다. 구조적으로는 특정 구분값을 지닌 코드와, Http상태코드, 그리고 발생이유를 적었다.

FieldError

FieldError 는 오류가 발생한 필드명, 그리고 value, reason 을 알기 위해서 만들어졌다. 코드는 아래와 같다.

@Getter
@AllArgsConstructor
public class FieldError {

    private final String field;
    private final String value;
    private final String reason;

    public static FieldError of(String field, String value, String reason) {
        return new FieldError(field, value, reason);
    }

    public static FieldError of(org.springframework.validation.FieldError fieldError) {
        return FieldError.of(
                fieldError.getField(),
                (fieldError.getRejectedValue() == null) ? "" : fieldError.getRejectedValue().toString(),
                fieldError.getDefaultMessage());
    }

    public static List<FieldError> of(BindingResult bindingResult) {
        return bindingResult.getFieldErrors().stream()
                .map(FieldError::of)
                .collect(Collectors.toList());
    }

}

ErrorResponse

@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
public class ErrorResponse {

    private String code;
    private String message;
    private LocalDateTime time;
    private List<FieldError> errors;
    private UUID logId;

    public static ErrorResponse of(ErrorCode errorCode, BindingResult bindingResult, UUID logId) {
        return ErrorResponse.builder()
                .code(errorCode.name())
                .message(errorCode.getReason())
                .time(LocalDateTime.now())
                .errors(FieldError.of(bindingResult))
                .logId(logId)
                .build();
    }

}

Builder 를 통해 구현했으며, code, message, time 등등 여러가지 정보들이 담길 수 있도록 했다. 이제 PostMan 으로 우리가 만든 컨트롤러에 에러정보가 담기는지 확인해보자!

{
    "code": "BAD_REQUEST",
    "message": "잘못된 요청입니다.",
    "time": "2021-03-31T21:08:03.025667",
    "errors": [
        {
            "field": "limit",
            "value": "-1",
            "reason": "0 이상이어야 합니다"
        }
    ],
    "logId": "6cd8f5e4-6e64-4e7b-bc72-aa745ebfb52f"
}

오 위와 같이 잘담겼다! 이런식으로 Error 를 컨트롤 할 수 있다는게 신기하다.

후기

  • 단의 코드를 보면서 열심히 공부했고, 따라했기에 단의 링크를 아래에 남겼다.
    Dan Git-Hub

  • 예외 처리를 프론트랑 협업할때 어떻게 해야할지가 그냥 궁금했었는데, 이런 방식으로 할 수 있다는 사실을 알게 됬다. 한번쯤 해보는게 다들 공부가 잘된다고 생각한다.

  • 아래는 예제의 코드들이 담긴 깃 리포지토리이다.
    StudyException

profile
모든 기술에는 고민을

1개의 댓글

comment-user-thumbnail
2021년 4월 5일

로치 감사합니다!

답글 달기