본문 바로가기
스프링

[스프링] @ConstructorProperties 활용하기 (feat. Jackson 라이브러리)

by kdohyeon (김대니) 2023. 3. 5.
반응형

POST 형식의 REST API 를 작성할 때, 요청 값을 주로 JSON 형태로 전달받는다. 이 데이터를 내부에서 사용할 수 있도록 맵핑 DTO 가 필요하고 주로 @RequestBody 어노테이션을 활용하여 전달받게 된다. 다음과 같이 코드를 작성할 수 있다.

@Getter
public class SampleRequestBody {
    private final String sampleName;
    private final Long sampleNumber;

    public SampleRequestBody(String sampleName, Long sampleNumber) {
        this.sampleName = sampleName;
        this.sampleNumber = sampleNumber;
    }
}

@PostMapping("/api/v1/sample")
public ResultResponse<Boolean> sample(
        @RequestBody SampleRequestBody requestBody
) {
    ...
}

 

SampleRequestBody 에는 2개의 필드가 포함되어 있고 @Getter 어노테이션만 부여되어 있다. POST 형식으로 API 를 호출할 수 있고, @RequestBody 로 맵핑해두었다. POSTMAN 등 API 를 호출할 수 있는 툴을 사용하여 /api/v1/sample API 를 호출하면 다음과 같이 에러가 발생한다.

Cannot construct instance of...

에러 메시지

Type definition error: [simple type, class sample.kdohyeon.blog.controller.blog.request.SampleRequestBody]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `sample.kdohyeon.blog.controller.blog.request.SampleRequestBody` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 2, column: 5]

전달받은 JSON 데이터를 스프링 내부 객체로 변환하는 과정에서 발생하는 에러이다. 즉, 데이터 바인딩을 하는 과정에서 발생하는 에러인데 이에 대해 좀 더 살펴보자.

스프링 데이터 바인딩에 대한 이해

스프링부트를 활용하면 내부적으로 jackson 라이브러리 (JSON 데이터구조를 처리해주는 라이브러리)를 활용하여 JSON 데이터를 내부 객체로 데이터 바인딩을 시켜준다. 좀 더 자세히 들어가면, @RequestBody 어노테이션을 사용하게 되면 스프링은 입력받는 데이터의 형식에 맞는 메시지 컨버터를 골라 변환해준다. 여기서 Jackson 라이브러리는 JSON 데이터 처리를 위해 MappingJackson2HttpMessageConverter 를 제공하는데 스프링은 이를 활용하여 컨버팅을 한다. 그 과정에서 ObjectMapper 를 활용하여 JSON 데이터와 오브젝트 사이의 변환을 지원한다. 

Jackson 라이브러리의 특성

스프링은 JSON 데이터 바인딩을 위해 MappingJackson2HttpMessageConverter 를 사용한다고 했는데 어떻게 동작하는걸까? JSON 데이터를 내부 객체로 어떻게 맵핑해주는걸까?

Jackson 은 기본적으로 프로퍼티 기반으로 동작하는데 프로퍼티는 GetterSetter 기반으로 정해진다. Getter 또는 Setter 메소드 명의 prefix 를 지우고, 나머지 문자의 첫 문자를 소문자로 변환한 문자열을 참조한다. 위 예시에서는 SampleRequestBody 의 @Getter 어노테이션이 있으므로 getSampleName() 메소드가 생성되고 jackson 은 여기서 get 을 지우고 (SampleName) 첫 문자를 소문자로 변환한다 (sampleName). 따라서 SampleName 과 SampleNumber 가 그 프로퍼티이다. 

그런데 잠깐 다시 위 예시로 돌아가보면 분명 @Getter 어노테이션으로 Getter 를 설정해주었다. 그럼 동작해야 하는게 아닐까? 왜 Cannot construct instance of... 에러가 나는걸까? 주의해야 할 점은 별도의 생성자를 직접 만들게 되면 jackson 에게 이를 알려줘야 한다. 위 예시에서는 SampleRequestBody 를 immutable 객체로 만들기 위해 final 키워드를 사용했는데 이렇게 되면 별도 생성자를 만들게 된다. jackson 에게 별도로 생성한 생성자를 활용하라고 알려줘야 하는데, 그 방법으로는 @JsonCreator + @JsonProperty 를 활용하는 방법과 @ConstructorProperties 를 활용하는 방법이 있다 (여기 참조). 

@ConstructorProperties 의 동작 방식

먼저, @ConstructorProperties 어노테이션을 별도로 생성한 생성자 위에 부여해준다. 그리고 외부에서 요청할 파라미터 이름을 속성으로 입력해준다. 아래 예시에서는 name 과 number 를 입력받아 sampleName, sampleNumber 에 각각 할당하려고 한다. 따라서 1) 에서는 name 과 number 로 값을 입력받고, 그대로 @ConstructorProperties 를 활용해 그 값을 전달받는다. 그리고 2) 에서는 그 순서에 맞춰 name 과 number 를 생성자로 주입한다. 

따라서, 2) 과정에서는 순서에 기반하여 주입을 하기 때문에 @ConstructorProperties 에 입력되는 프로퍼티 명과 생성자에 입력되는 변수 명이 꼭 동일하지 않아도 된다. 아래 처럼 @ConstructorProperties 에서는 name 을, 생성자에는 sampleName 으로 해도 정상 작동한다. 

그렇기 때문에 @ConstructorProperties 로 입력받는 데이터와 주입하는 데이터의 순서가 꼬이지 않게 조심해야 한다. 아래처럼  @ConstructorProperties 에 number 와 name 의 순서를 바꿔두게 되면 number -> sampleName 으로 주입, name -> sampleNumber 로 주입하게 동작한다. 만약 이 때, 데이터의 유형이 맞지 않으면 에러가 발생한다.

참고로, 별도의 생성자를 만들지 않으면 (아래 예시처럼) jackson 은 잘 동작한다.

@Getter
public class SampleRequestBody {
    private String sampleName;
    private Long sampleNumber;
}

참고 자료

반응형

댓글