Spring

[Jackson] 왜 field가 1개인 DTO는 기본 생성자가 필요할까?(1)

hanrabong 2022. 12. 15. 21:01

 

 

 

 API server를 개발하면서 DTO를 이용해서 client으로부터 request를 받거나 server내의 다른 layer로 넘기곤 합니다. Spring boot를 이용하여 프로젝트를 할 때도 DTO를 이용하여 데이터를 주고받곤 하였습니다.

 

 client가 Content-Typeapplication/json 형태로 요청을 할 경우, Node.js 기반의 express 와 Nest.js와는 다르게 Spring boot는 Json을 객체로 변환시켜주는 과정이 필요합니다. 이 때, jackson library가 json을 객체로, 객체를 json으로 변환해줍니다. 

 

 Jackson library는 별도로 설치할 필요가 없고 spring web mvc dependency를 설치하면 자동으로 설치가 됩니다. spring web mvc를 사용하면 간편하게 설치가 되기에, Jackson이 정확히 어떻게 작동하는지 모르고 사용을 해왔습니다. 그러다가 다음과 같은 오류에 부딪히게 되었습니다.

 

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.example.test.dto.TestOneRequestDto` (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:

 

 위의 오류를 간략하게 말하면 jackson이 TestOneRequestDto instance를 만들 수가 없어서 deserialize를 할 수 가 없다는 warning 메세지 입니다. deserialize란 json을 object로 변환을 말합니다.

 위의 오류는 신기하게도 제목처럼 field가 1개인 DTO에서만 오류가 발생합니다. field가 2개 이상이면 오류가 발생하지 않습니다.

 

 

결론부터 말하면, field가 1개인 경우 기본 생성자가 있어야만 오류가 발생하지 않습니다. 

 

 

Jackson이 어떻게 json을 object로 변환을 시키는지, 또 이런 warning이 왜 발생하는지 이야기해보겠습니다. 

 

 

다음은 개발 환경 및 테스트 코드입니다.

 

개발 환경 


  • Gradle
  • Spring Boot 2.7.6
  • Java 11
  • spring-webmvc 5.3.24
  • Jackson 2.13.4
  • API test: Talend API Tester

 

테스트 코드 및 결과


// TestContoller.java
@RestController
@RequestMapping("/test")
public class testController {

    @PostMapping
    public void JacksonTest1(@RequestBody TestOneRequestDto testOneRequestDto) {
        System.out.println(testOneRequestDto);
    }

    @PostMapping("/1")
    public void JackSonTest2(@RequestBody TestTwoRequestDto testTwoRequestDto) {
        System.out.println(testTwoRequestDto);
    }
}

// TestOneRequestDto.java
public class TestOneRequestDto {
    private String test;

    public String getTest() {
        return test;
    }

    public void setTest(String test) {
        this.test = test;
    }

    public TestOneRequestDto(String test) {
        this.test = test;
    }
}

// TestTwoRequestDto.java
public class TestTwoRequestDto {
    private String test1;

    private String test2;

    public TestTwoRequestDto(String test1, String test2) {
        this.test1 = test1;
        this.test2 = test2;
    }

    public void setTest1(String test1) {
        this.test1 = test1;
    }

    public void setTest2(String test2) {
        this.test2 = test2;
    }

    public String getTest1() {
        return test1;
    }

    public String getTest2() {
        return test2;
    }
}

 

 테스트를 좀 더 확실하게 하기 위해 Lombok을 사용하지 않고, 코드를 직접 작성하였습니다. TestOneRequestDto와 TestTwoRequestDto 둘다 setter, getter, constructor(모든 field)가 있습니다. 차이점은 오직 field의 갯수입니다. 

 

 

field가 1개인 경우(좌), field가 2개인 경우(우)

 

field가 1개인 경우 결과(좌), field가 2개인 경우(우)

 

 Talend API Tester를 이용하여 테스트를 해 본 결과, field가 1개인 경우는 400 error가 발생하였고, field가 2개인 경우 성공을 의미하는 200 status code를 받았습니다.

 

 

 

Jackson


 Jackson은 json을 parsing하고 생성하는 라이브러리입니다. 내부적으로 Object Mapper class가 있어서 json file을 parsing해서 java 객체로 deserialize하거나, java 객체로부터 json을 생성(serialize)할 수 있습니다.

 

 

Serialize & Deserialize

 

public class JacksonTest {

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    public void testDeserializeJackson() throws JsonProcessingException {
        String jsonString = "{\"test\": \"It's a test\"}";

        TestOneRequestDto testOneRequestDto = objectMapper.readValue(jsonString, TestOneRequestDto.class);
        System.out.println(testOneRequestDto.getTest());
    }

    @Test
    public void testSerializeJackson() throws JsonProcessingException {
        TestOneRequestDto testOneRequestDto = new TestOneRequestDto("It's a test");

        String jsonString = objectMapper.writeValueAsString(testOneRequestDto);
        System.out.println(jsonString);
    }
}

 

 위의 코드처럼 Jackson은 Object Mapper를 이용하여 serialize와 deserialize를 진행합니다. 실제로는 String이 아닌 Stream을 읽어냅니다. 정확한 코드는 AbstractJackson2HttpMessageConverter.java (writeInternal method, readInternal method)를 참고하세요.

 위의 코드를 실행하면 당연히 처음에 언급했던 오류가 나옵니다. 기본 생성자를 추가해주면 바로 test가 성공하는 것을 볼 수 있습니다. 

 

 

그럼 왜 field가 1개일 때 기본 생성자가 필요할까요??

 

 

 

다음 글에서 이어서 적겠습니다!!

 

 

※ 궁금한 사항이나 잘못된 점 댓글 부탁드립니다.