@Builder 클래스 Deserialize 하기

Sera Lee·2022년 2월 28일
1

Lombok 잘 사용하기

목록 보기
2/2

개요

spring application를 layerd architecture 로 개발할 때 interface의 requestBody 를 dto class 로 만들어 구현한다.

이때 멤버변수가 많은 경우, testcode 작성이 용이하려고 @Builder 어노테이션을 class 위에 선언하곤 한다.

이 때, Json Data를 Dto 로 Deserialize 될 때 문제가 발생하는데, 단순히 구글링을 하여 @NoArgsConstructor, @AllArgsConstructor 를 같이 선언했던 자신을 반성하며 문제의 원인을 파악하고 근본적인 문제를 해결하기 위한 방법이 담긴 글이다.

Deserialize 란?

직렬화된 파일이나 Data Stream을 객체로 변환하는 것을 의미한다.

HTTP API 에서는 JSON 형태의 RequestBody 를 Object로 파싱하는 작업이 된다.

Jackson ObjectMapper를 이용하여 Deserialize 하기

  • Deserialize 할 Dto
@Getter
@Builder
public static class RegisterUserRequest {
	@NotBlank
  private String email;
}
  • ObjectMapper를 이용하여 Deserialize 하기
@Test
void deserialize() throws Exception {
		RegisterUserRequest userdto =  UserDto.RegisterUserRequest.builder().email("abcd@email.com").build();
    String jsonStr =  objectMapper.writeValueAsString(userdto);
    RegisterUserRequest request = objectMapper.readValue(jsonStr, RegisterUserRequest.class);
    System.out.println(request);
}
  • 에러발생!!!!!!!!!!!!!!!!!!!!!!!!!!!!

(although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"abcd@email.com"}"; line: 1, column: 2]

(although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)

  • 이 에러가 나는 이유는 default constructor가 없어서 그렇다고 한다.

해결1. constructor 를 추가해 주자.

@Builder 를 사용하게 되면 @NoArgsConstuctor, @AllAgsConstuctor 두 개가 모두 필요하다. 직접 생성자를 추가해주거나 위처럼 롬복으로 추가할 수 있다.

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class RegisterUserRequest {

	@NotBlank
  private String email;
}
  • 위의 @NoArgsConstuctor@AllArgsConstructor 가 모두 선언됐을 때와, @Builder 만 존재했을 때의 compile 된 class 파일을 비교해보자. 좌측이 @Builder 만, 우측이 @NoArgsConstuctor@AllArgsConstructor , @Builder 가 선언된 경우이다.

  • 해결한 방법에 대한 결론!!
    • Jackson은 default no-args constructor 가 없으면 객체를 구성하는 방법을 찾을 수 없다.
    • @Builderdefault no-args constructor 를 생성하지 않는다.
    • @Builder 에 default constructor 를 생성하고자 @NoArgsConstructor 를 추가하게 되면 Build가 되지 않고 다음 에러가 발생한다.

      RegisterUserRequest cannot be applied to given types;
      required: no arguments

      • @Builder@NoArgsConstuctor를 함께 사용하려면, @AllArgsConstructor 를 함께 써야 한다.
    • @Builder,@NoArgsConstuctor,@AllArgsConstructor 를 모두 사용한다.

❓︎ 과연 deserialize 를 위한 default no-args constructor 가 옳은 방식일까?

물론 private 지정자를 선언해서 불변성을 막을 수는 있지만, 쓸데없이 constructor가 생성된 것은 맞다.

답은 의외로 공식문서(https://github.com/FasterXML/jackson-databind/) 에서 찾을 수 있었다.

GitHub - FasterXML/jackson-databind: General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)

Builder 패턴과 Jackson 을 함께 사용 할 수 있는 방법에 대해 Tutoral을 명시해 놓았다.

Dto 클래스에 @JsonDeserialize 를 선언하고, Builder 클래스에 @JsonPOJOBuilder 를 선언한다.

@Getter
@Builder(builderClassName = "DtoBuilder", toBuilder = true)
@JsonDeserialize(builder = RegisterUserRequest.DtoBuilder.class)
public static class RegisterUserRequest {

	@NotBlank
  private String email;

  @JsonPOJOBuilder
  public static class DtoBuilder { }

}
  • 이 때, 기본 빌더가 아닌 Builder class(DtoBuilder) 를 따로 만들어주고
    • public static class DtoBuilder{}
    • @Builder(builerClassName = “DtoBuilder”, toBuilder= true)
  • @JsonDeserialize 로 만들어준 Builer(RegisterUserRequest.DtoBuilder.class) 를 명시적으로 선언하여 해당 빌더를 Deserialize 할 수 있도록 해준다.
  • compile 된 RegisterUserRequest.class는 다음과 같다.
public class UserDto {
    public UserDto() {
    }

    @JsonDeserialize(
        builder = UserDto.RegisterUserRequest.DtoBuilder.class
    )
    public static class RegisterUserRequest {
        @NotBlank
        private String email;

        RegisterUserRequest(String email) {
            this.email = email;
        }

        public static UserDto.RegisterUserRequest.DtoBuilder builder() {
            return new UserDto.RegisterUserRequest.DtoBuilder();
        }

        public UserDto.RegisterUserRequest.DtoBuilder toBuilder() {
            return (new UserDto.RegisterUserRequest.DtoBuilder()).email(this.email);
        }

        public String getEmail() {
            return this.email;
        }

        @JsonPOJOBuilder
        public static class DtoBuilder {
            private String email;

            DtoBuilder() {
            }

            public UserDto.RegisterUserRequest.DtoBuilder email(String email) {
                this.email = email;
                return this;
            }

            public UserDto.RegisterUserRequest build() {
                return new UserDto.RegisterUserRequest(this.email);
            }

            public String toString() {
                return "UserDto.RegisterUserRequest.DtoBuilder(email=" + this.email + ")";
            }
        }
    }
}

❗느낀점

역시 해답은 공식 문서에 있었다. 공식 문서를 항상 첫번째로 확인하자.

블로그글을 맹신하지 말자. 생각보다 검색해서 본 블로그에는 뭔가 애매한 해석, 잘못된 해석들이 많았다. 무작정 구글링 해서 나온 해결법으로 문제를 해결하기만 급급했던 자신에 반성한다.

방법이 구글링을 하면 기본적으로 나오는 @NoArgsConstructor, @AllArgsConstuctor 이고, 물론 이방법 도 맞다. 하지만 어노테이션은 의도와 일치하게 작성되는 것이 좋다고 생각한다.

@Builder에 두개의 생성자 어노테이션을 붙이는 것보다 Builder를 Jackson이 Deserialize 할 수 있게 선언해준다고 표현된 어노테이션이 좀 더 의도가 잘 나타난다고 생각이 되었다.

Lombok 사용할 때는 꼭 generate 된 class 를 확인해보자. 나 자신이 @Builder 에 대해 잘 모르고 썼던 부분도 있었고, 실제 compile 을 해서 generate 된 class 를 직접 확인해보니, 더 명확해진 부분이 있었다.

generate 된 코드를 봐도 사실 이해하기 어려운 부분이 많다. 😥

어렵지만 차근차근 해석하다 보면 지식이 차곡차곡 쌓이지 않을까?

0개의 댓글