최근 예외처리에 대해 많이 여러가지 이유로 공부하게 된것 같다. 백기선님 자바 스터디 및 스프링 Q&A 를 통해서, 예전에 코드스쿼드 채팅에서 Dan 님의 예외처리 코드를 봤던 기억이 났는데, 오늘 그 기억이 나서 예외처리에 대한 코드를 짜보았다.
오늘 야자를 하고 있는데 Cooper 가 물어봤던 질문에 대한 답이 되는 코드일 수도 있다. 일단 Event 라는 하나의 클래스를 만들자.
@Getter
@Setter
public class Event {
private Integer id;
@NotBlank
private String name;
@Min(value = 0)
private Integer limit;
}
@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 를 리턴해줄 것이다. 그게 우리의 목표다.
그렇담 먼져, 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 는 오류가 발생한 필드명, 그리고 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());
}
}
@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
로치 감사합니다!