[Spring] Request로 Enum이 들어올 때

민찬기·2022년 10월 16일
0

1. 상황

사이드로 진행하고 있는 프로젝트에서, 프론트의 게시글 등록 요청 시 카테고리도 함께 받아야 하는 상황이었다.

@RequiredArgsConstructor
@RestController
public class PostController {

    private final PostService postService;

    @PostMapping("/post")
    public ResponsePost create(
            @RequestBody RequestPost requestPost
    ) {
        return postService.create(requestPost);
    }
}
@Data
@NoArgsConstructor
public class RequestPost {
    private String title;
    private String content;
    private String category;
}

처음엔 별 생각 없이 String으로 받으면 되겠다 싶었으나, 문제는 data.sql을 이용하여 DB에 Category를 초기화 해 둔 상태였다.


set foreign_key_checks = 0;
truncate table category;
truncate table subcategory;
set foreign_key_checks = 1;

INSERT INTO category set college='Humanities';
INSERT INTO category set college='SocialScience';
INSERT INTO category set college='Business';
INSERT INTO category set college='Natural';
INSERT INTO category set college='Engineering';
INSERT INTO category set college='Art';

들어온 요청이 테이블의 값과 일치하지 않는다면 꽤나 곤란한 상황이 생길 것이 분명한 상황이다.

따라서 이를 사전에 방지하기 위해 입력값에 대한 타당성 검사를 진행하기로 했다.


2. 두 가지 선택지

1) Enum으로 받기

첫 번째 방법은 RequestPost에 String으로 선언되어 있는 category를 Enum Category로 변경하는 것이다.

public enum Category {
    Humanities,
    Social,
    Business
}
@Data
@NoArgsConstructor
public class RequestPost {
    private String title;
    private String content;
    private Category category;
}

위와 같이 Enum 자료형으로 값을 받도록 할 때, Enum에 선언되지 않은 값으로 요청을 보내면 어떻게 될까?


{
  "title": "제목",
  "content": "내용",
  "category": "사회"
}

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type com.example.test.Category from String "사회": not one of the values accepted for Enum class: [Business, Social, Humanities]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type com.example.test.Category from String "사회": not one of the values accepted for Enum class: [Business, Social, Humanities] at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 4, column: 17] (through reference chain: com.example.test.RequestPost["category"])]

길긴 하지만, 요점은 String "사회":not one of the values accepted for Enum class 한 줄이면 충분하다. 즉, 입력으로 들어온 '사회'라고 하는 값은 Enum class에 속해있지 않으므로 Exception을 발생시킨다.


문제점

이 방법의 문제점은 Exception 관리가 쉽지 않다는 점이다. 발생한 HttpMessageNotReadableException은 JSON parse error로, 위의 예시처럼 Enum에 포함되지 않는 경우에도 발생할 수 있지만, 그 외의 사유로 발생하는 parse error에도 Exception이 발생한다. 즉, Error message를 깔끔하게 정제하기 어렵다는 문제점이 있다.


2) String으로 받기 + CustomValidator

요청으로 들어오는 값을 검증하기 위해 @Valid 어노테이션을 사용한 경험이 한 번쯤은 있을 것이다. 그러나 들어온 값이 Enum에 포함되는 지 여부를 검토하는 방법은 별도로 존재하지 않는다. 다만, 별도의 어노테이션을 만들어 검증하는 방법이 존재한다. 어노테이션을 만드는 구체적인 방법은 해당 링크에서 확인하면 된다.

간단하게 결과물만 확인해보자.

@PostMapping("/post")
    public ResponsePost create(
            @Valid @RequestBody RequestPost requestPost
    ) {
        return postService.create(requestPost);
    }
@Data
@NoArgsConstructor
public class RequestPost {
    private String title;
    private String content;
    @Enum(enumClass = Category.class, ignoreCase = true)
    private String category;
}

이렇게 사용을 하면, Enum에 포함되지 않는 값이 들어왔을 때 아래의 에러가 발생한다.

Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.qupp.config.dto.Response<com.qupp.post.dto.response.ResponsePost> com.qupp.post.controller.QuestionController.register(com.qupp.post.dto.request.RequestRegisterQuestion,java.util.List<org.springframework.web.multipart.MultipartFile>): [Field error in object 'requestRegisterQuestion' on field 'college': rejected value [Ar]; codes [Enum.requestRegisterQuestion.college,Enum.college,Enum.java.lang.String,Enum]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [requestRegisterQuestion.college,college]; arguments []; default message [college],class com.qupp.post.repository.College,true]; default message [Invalid value. This is not permitted.]]]


중요한 건 MethodArgumentNotValidException이 발생했다는 것이다. 해당 Exception을 별도의 ExceptionHandler에서 관리함으로써, 정제된 에러메세지를 프론트에게 응답해주는 것이 가능하다.

@RestControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler({
            MethodArgumentNotValidException.class
    })
    public Response methodArgumentNotValidException(final Exception e) {
        return Response.builder()
                .msg(e.getMessage())
                .build();
    }
}

3. 결론

위의 글을 모두 읽었다면 알겠지만, 본인은 두 번째 방법을 선택했다. 선택의 가장 큰 이유는 Exception 관리에 있었다. 별도의 어노테이션 설정 없이, Enum 자료형으로 받으면 코드야 간단하겠지만, Exception을 프론트에 전달하는 과정에 있어서 메세지 관리가 쉽지 않을 것이 자명했기 때문이다. 어떤 필드에서 에러가 발생했는지 정확히 전달해줄 필요가 있다는 관점에서 두 번째 방법을 선택하게 되었다.

사용하는 사람의 기호에 따라 취사선택하면 될 문제 같다. (JSON parse error를 Custom 할 방법이 있다면, 첫 번째도 좋은 선택이 될 것이라 생각한다.)

profile
https://github.com/devmizz

0개의 댓글