ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • @RequestBody에 매핑되는 DTO는 왜 빈(empty) 생성자가 필요한가?
    Today I Learned 2022. 11. 16. 13:24

     

    임시 로그인을 구현하기 위해 백엔드 로직을 작성하던 중 한 가지 난관에 봉착했다. LoginRequestDto를 받아와 처리하는 Controller의 동작을 정의하기 위해 테스트 코드를 작성하던 중, @RequestBody에 매핑된 LoginRequestDto를 생성할 수 없다는 에러 메세지가 출력되면서 테스트가 실패하는 것을 확인했다.

     

    // SessionController.java
    
    @RestController
    @RequestMapping("session")
    public class SessionController {
        // Beans, Constructors
    
        @PostMapping
        @ResponseStatus(HttpStatus.CREATED)
        public LoginResultDto login(
            @Validated @RequestBody LoginRequestDto loginRequestDto,
            BindingResult bindingResult
        ) {
            // process
            // ...
            
            return new LoginResultDto(accessToken);
        }
    }
    // LoginRequestDto.java
    
    public class LoginRequestDto {
        @NotNull(message = "user Id를 입력해주세요.")
        private Long userId;
    
        public LoginRequestDto(Long userId) {
            this.userId = userId;
        }
    
        public Long getUserId() {
            return userId;
        }
    }
    // SessionControllerTest.java
    
    @WebMvcTest(SessionController.class)
    class SessionControllerTest {
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        void login() throws Exception {
            mockMvc.perform(MockMvcRequestBuilders.post("/session")
                    .accept(MediaType.APPLICATION_JSON)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{" +
                        "\"userId\":\"1\"" +
                        "}"))
                .andExpect(MockMvcResultMatchers.status().isCreated())
            ;
        }
    }
    2022-11-16 13:16:55.394 WARN 41110 --- [ Test worker] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `kr.megaptera.smash.dtos.LoginRequestDto` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `kr.megaptera.smash.dtos.LoginRequestDto` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)<EOL> at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]]

    Status expected:<201> but was:<400> Expected :201 Actual :400

     

    동료들과 3기 후배분들의 도움을 받아 LoginRequestDto에 빈 생성자가 없어 문제가 발생하고 있다는 점을 확인해 빈 생성자를 추가했다. 그 결과 다시 기대한 대로 요청 데이터를 잘 가져오는 것을 확인할 수 있었다.

     

    // LoginRequestDto.java를 다음과 같이 수정
    
    public class LoginRequestDto {
        @NotNull(message = "user Id를 입력해주세요.")
        private Long userId;
        
        public LoginRequestDto() {
        
        }
    
        public LoginRequestDto(Long userId) {
            this.userId = userId;
        }
    
        public Long getUserId() {
            return userId;
        }
    }

     

    그동안 DTO에 데이터들을 모두 포함하는 생성자와, getXX 방식의 네이밍을 갖는 getter 메서드만 사용해왔는데도 데이터 교환에 문제를 겪지 않고 있다가 갑자기 문제가 생기니 당황스러웠다. POJO, Java Bean과 DTO가 어떤 관련이 있는지 3기 분들이 물어보셨을 때 대답을 하지 못한 것도 마음에 걸렸다. 왜 요청으로 들어온 DTO에는 빈 생성자를 넣어줘야 Controller에서 DTO임을 인식할 수 있었는지 찾아 정리했다.

     

     

    웹 페이지에서 API 서버에 요청을 보낼 때 같이 전송해야 할 데이터가 있을 경우, Request Body에 데이터를 JSON 형식으로 담아 전송한다.

     

     

    Spring은 HTTP 요청을 수신하면 RequestResponseBodyMethodProcessor에서 readWithMessageConverters 메서드를 호출한다. 메서드를 처리하는 과정에서 JSON을 ContentType으로 읽을 수 있는 MappingJackson2HttpMessageConverter를 이용해 Request Body를 읽어들인다.

     

    이때 Jackson 라이브러리의 ObjectMapper를 통해 Body를 읽게 되는데, JSON 형식을 Deserialize해 Java Object 형태로 변환해 반환한다.

     

    Oracle의 설명에 따르면, ObjectMapper에서는 객체의 상태를 초기화하기 위해 빈 생성자를 사용하고, 빈 생성자가 선언되어 있지 않다면 런타임 에러가 발생한다. 따라서 HTTP 요청으로 JSON 형태의 Request Body를 받는 경우, DTO에 빈 생성자가 있지 않다면 DTO 객체를 생성하지 못해 에러가 발생한다.

     

    During deserialization, the fields of non-serializable classes will be initialized using the public or protected no-arg constructor of the class. A no-arg constructor must be accessible to the subclass that is serializable.
    The subtype may assume this responsibility only if the class it extends has an accessible no-arg constructor to initialize the class's state. It is an error to declare a class Serializable if this is not the case. The error will be detected at runtime.

     

    다음 글에서 ObjectMapper가 Serialization과 Deserialization을 전반적으로 어떻게 수행하는지 참고할 수 있었다.

     

    @RequestBody에 왜 기본 생성자는 필요하고, Setter는 필요 없을까? #2

    이전 글에서는 어떻게 @RequestBody를 처리하는지를 알아보기 위한 과정을 설명했습니다. 이번 글에서는 @RequestBody를 바인딩하는 ObjectMapper에 대해 알아보고, 결론을 짓겠습니다. 참고로 아래 사진

    velog.io

     

     

     

    References

    - POJO, Java Beans, DTO
    https://www.baeldung.com/java-pojo-javabeans-dto-vo


    - RequestResponseBodyMethodProcessor

    https://velog.io/@conatuseus/RequestBody 1


    - Serialization

    https://velog.io/@conatuseus/RequestBody 2
    https://docs.oracle.com/javase/10/docs/api/java/io/Serializable.html
    https://blusky10.tistory.com/415

    https://velog.io/@tsi0521/RequestBody


    - ObjectMapper
    https://interconnection.tistory.com/137

     

     

     

     

    댓글

Designed by Tistory.