spring application를 layerd architecture 로 개발할 때 interface의 requestBody 를 dto class 로 만들어 구현한다.
이때 멤버변수가 많은 경우, testcode 작성이 용이하려고 @Builder
어노테이션을 class 위에 선언하곤 한다.
이 때, Json Data를 Dto 로 Deserialize 될 때 문제가 발생하는데, 단순히 구글링을 하여 @NoArgsConstructor
, @AllArgsConstructor
를 같이 선언했던 자신을 반성하며 문제의 원인을 파악하고 근본적인 문제를 해결하기 위한 방법이 담긴 글이다.
직렬화된 파일이나 Data Stream을 객체로 변환하는 것을 의미한다.
HTTP API 에서는 JSON 형태의 RequestBody 를 Object로 파싱하는 작업이 된다.
@Getter
@Builder
public static class RegisterUserRequest {
@NotBlank
private String email;
}
@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]
@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
가 선언된 경우이다.@Builder
는 default no-args constructor 를 생성하지 않는다.@Builder
에 default constructor 를 생성하고자 @NoArgsConstructor
를 추가하게 되면 Build가 되지 않고 다음 에러가 발생한다.RegisterUserRequest cannot be applied to given types;
required: no arguments
@Builder
와@NoArgsConstuctor
를 함께 사용하려면,@AllArgsConstructor
를 함께 써야 한다.
@Builder
,@NoArgsConstuctor
,@AllArgsConstructor
를 모두 사용한다.물론 private 지정자를 선언해서 불변성을 막을 수는 있지만, 쓸데없이 constructor가 생성된 것은 맞다.
답은 의외로 공식문서(https://github.com/FasterXML/jackson-databind/) 에서 찾을 수 있었다.
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 { }
}
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 된 코드를 봐도 사실 이해하기 어려운 부분이 많다. 😥
어렵지만 차근차근 해석하다 보면 지식이 차곡차곡 쌓이지 않을까?