티스토리 뷰

반응형

요즘 프로젝트 리팩토링을 하면서, DTO에 작성하는 여러개의 lombok 어노테이션의 사용 이유를 정확히 알지 못하고

사용하는 것 같아서 하나하나 고찰(?) 해보기로 했다.

 

일단, DTO는 Data Transfer Object로, REST API 작성 시에 엔티티 대신에 DTO를 사용하여 컨트롤러에서 데이터를 주고받는 용도로 사용한다. DTO를 사용하면 엔티티에 변질을 막을 수 있고, 로직에 맞춰 필요한 필드만 주고받을 수 있어 DTO를 사용하는 것이 좋다.

 

컨트롤러에서 DTO를 주고받기 때문에 JSON 직렬화와 역직렬화가 일어난다.

- 직렬화(serialization) : Java Object 가 JSON으로 변환되는 것으로, ResponseBody를 사용할 때 일어난다. (서버 -> 클라이언트)

- 역직렬화(deserialization) : JSON이 Java 오브젝트로 변환되는 것으로, RequestBody를 사용할 때 일어난다. (클라이언트 -> 서버)

 

 

DTO 직렬화/역직렬화 시에 필드에 어떻게 접근할 것인가?

Spring Boot에서 DTO를 직렬화/역직렬화하기 위해서는 Jackson이 동작하는데, Jackson 내부에서는 ObjectMapper가 리플렉션(Java Reflection)을 사용한다.

이 때 이 로직에서는 기본생성자와 getter/setter가 필요하다.

이전에는 무조건 필드에 값을 세팅해주기 위해서 setter를 사용해서 setter가 꼭 필요한 줄 알았다. 그런데 아니었다.

이번에 공부를 하면서 알게 된 것을 정리하면 이렇다.

 

- 직렬화(Java Object -> JSON) : getter 필요 (setter 불가능), 기본 생성자 필요 없음

- 역직렬화(JSON -> Java Object) : getter나 setter 중에 하나만 있어도됨, 기본 생성자 필요

 

 

DTO 역직렬화에서는 기본 생성자가 필요하다. 왜일까?

ObjectMapper의 동작 방식을 보면 역직렬화 시에 기본 생성자를 이용하여 객체를 생성한 후에

getter나 setter를 이용하여를 필드를 가져온다. 이 때 필드를 가져오기 위해서는 위에서 말했던 reflection을 이용한다.

그래서 setter가 필요하지 않은 이상 setter보다는 getter를 선언하는 것이 좋을 듯하다. (setter는 필드 값이 바뀔 가능성을 높여주기 때문)

 

 

이 때 DTO에서 기본 생성자의 접근 제한자는?

위에서 말한것처럼 DTO의 기본 생성자는 ObjectMapper의 내부 동작에서 리플렉션으로 사용된다.

결국 이 기본 생성자가 다른 용도로 외부에서 사용할 것이 아니라면, 앞서 말한 리플렉션에선만 사용되기 때문에 private이어도 상관이 없다.

오히려 불변성을 위해서, 불필요한 객체 생성을 막기 위해서 private으로 선언하는 것이 좋을 것 같다.

 

 

기본 생성자가 필요하지 않을 때는 없을까?

기본 생성자와 getter를 필수로 하지 않을 때는 프로퍼티와 생성자가 위임된 경우에 해당한다.

위임 어노테이션은 @JsonProperty, @JsonAutoDetect, @JsonCreator 이 있다.

이 어노테이션을 사용해 객체를 직렬화/역직렬화할 때 쓰일 정보를 직접 선언할 수 있다.

 

 

그런데 놀랍게도, Spring Boot에서 DTO 역직렬화 시에
@NoArgsConstructor가 없어도 오류가 나지 않고 잘 수행된다.
왜일까?

리팩토링 중인 프로젝터의 Request DTO에서 기본 생성자가 없는데도 잘 수행되는 것을 확인할 수 있었다.

이에 대해 의문을 가지게 되었고, 관련해서 수많은 검색을 하면서 궁금증을 해결할 수 있었다.

 

앞서 말한대로, 기본 생성자가 필요 없을 때는 프로퍼티나 생성자가 위임된 경우라고 했는데, 

스프링 부트에는 위임을 자동으로 해주는 라이브러리인 jackson-datatype-jdk8이 있다.

이 라이브러리에서 jackson-module-parameter-names라는 모듈을 지원하고 있으며,

이 모듈은 기본 생성자가 없어도 다른 생성자로 대체하여 역직렬화를 수행할 수 있게한다.

 

스프링 부트의 ObjectMapper에 위 모듈이 기본적으로 등록되어 있어서,

DTO 역직렬화를 할 때 기본 생성자가 없어도 오류 없이 수행이 가능했던 것이다.

 

여기서 주의할 점은 Gradle 빌드의 스프링 부트에서 인텔리제이 설정에 따라 오류가 날 수도 있다.

Gradle 빌드 옵션(IntelliJ > Settings > Build, Execution, Deployment > Build Tools > Gradle)을 

Gradle(default)가 아닌 IntelliJ로 설정하게 되면 기본 생성자가 없다면 오류가 나고 역직렬화가 불가능하다.

Gradle(defulat) 일 경우에는 리플렉션을 통해 인자가 있는 생성자 정보로도 역직렬화가 가능하므로, Gradle 빌드 옵션을 바꾸지 말자.

 

 

 

생성자 인자가 하나일 경우에는 위임이 안된다?

앞서 말한 것들이 생성자 인자가 하나일 경우에는 동작하지 않는다..!

이 때는 @JsonCreator을 꼭 사용해줘야만 한다.

public class UserDTO {
	private final String name;

	@JsonCreator
	public User(String name) {
		this.name = name;
	}
}

 

 

역직렬화는 알겠는데, 직렬화는 어떻게 되는걸까?

직렬화는 기본 생성자는 필요없고, getter는 필수로 한다고 위에서 언급했다.

역직렬화는 위 내용처럼 스프링 부트에서 모듈을 지원해주기도 하는데, 직렬화는 어떻게 되는걸까?

많은 내용을 검색해보고 문서를 찾아봐도 직렬화에 관한 내용은 잘 찾을 수 없었다.

 

내가 실험을 해본 결과, 직렬화 즉 response DTO에서는 getter가 있어야만 오류가 나지 않음을 확인했다.

정확한 fact는 찾아보지 못했지만, 아마 역직렬화 시에 기본 생성자를 다른 인자 생성자로 대체할 수 있는

jackson-module-parameter-names 모듈은 말 그대로 생성자에 관해서만 지원을 해주기 때문에 직렬화와는 관련이 없는 것 같다.

 

직렬화는 기본 생성자 여부와 관계없이 getter만 필요하기 때문이다.

 

 

 

그럼 대체 어떻게 이 복잡한 생성자와 getter, setter을 작성해야할까?

사실 이 과정들을 검색하고 공부하다보니, 굉장히 복잡하고 또, 많은 경우의 수가 있다는 것을 느꼈다.(인자가 하나일 경우 등..)

위의 내용들을 공부하면서 궁금한 점도 많이 풀리고, 많은 것들을 알게 되어 좋지만 이런 내용들을 모두 알면서 적용하긴 힘들 것 같다.

 

DTO에는 @Builder도 많이 사용하기 때문에 빌더를 사용할 경우에는 그냥 @Builder, @Getter, @NoArgsConstructor(private), @AllArgsConstructor를 무조건 명시해주는 것이 편할 것 같다.

 

빌더를 사용하지 않을 경우에는 직렬화 dto에는 @Getter, 역직렬화 dto에는 @Getter와 @NoArgsConstructor(private)로 기본 생성자를 무조건 명시해주는게 좋을 것 같다.

 

 

 

 

 


복잡하다 복잡해..

문서, 블로그, 검색, 여러가지 실험을 통해 작성한 글이다.

 

궁금증이 해결되면서 재밌기도 했고 이제 알고서 사용할 수 있을 것 같아서 뿌듯하다.

앞으로도 코드를 작성하든, 기술을 사용하든 사용하는 이유와 동작방식 등을 꼭 공부하고 사용하는 습관을 들이는 것이 좋을듯하다.

그러면 더 효과적으로 사용할 수도 있고, 코드의 동작을 이해하기도 쉽기 때문이다.

공부하는 과정 속에서도 깊게 파고 파고들다보면 생각보다 궁금해지는 것도 많고 배우는 것도 많아 재밌는 것 같다 !!

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday