익명의 능력자분께 코드리뷰를 받았을 때 이러한 리뷰를 받았다.
1. DTO란?
DTOs or Data Transfer Objects are objects that carry data between processes in order to reduce the number of methods calls. Fowler explained that the pattern’s main purpose is to reduce roundtrips to the server by batching up multiple parameters in a single call. This reduces the network overhead in such remote operations.
DTO(Data Transfer Objects)는 프로세스 간에 데이터를 전달하는 데 사용되는 객체입니다.
DTO 패턴을 처음 소개하신 마틴 파울러는 한 번의 DTO 호출로 여러개의 매개 변수를 일괄 처리하여 클라이언트와 서버의 통신 횟수를 줄이는 것이 주된 목적이라고 하셨습니다.
@GetMapping("/example")
public String example(@RequestParam String name,
@RequestParam int age
@RequsetParam String address) {
// ...
}
@PostMapping("/example")
public String example(@RequestBody MyDto dto) {
// ...
}
public class MyDto {
private String name;
private int age;
// ...
}
두 코드를 보면 첫 번째 코드는 여러 개의 @RequestParam을 통해 값을 전송하고 있습니다. 하지만 두 번째 코드를 보면 MyDto라는 클래스를 생성해 값을 한 번에 전달받는 모습을 볼 수 있습니다.
이처럼 DTO는 서버와 클라이언트의 통신 횟수를 줄여주는 장점을 가지고 있습니다.
Another benefit is the encapsulation of the serialization’s logic (the mechanism that translates the object structure and data to a specific format that can be stored and transferred). It provides a single point of change in the serialization nuances. It also decouples the domain models from the presentation layer, allowing both to change independently.
또 다른 장점은 직렬화의 로직(객체 구조와 데이터를 저장 및 전송할 수 있는 특정 형식으로 변환하는 매커니즘)을 캡슐화 하는 것입니다.
그리고 도메인 모델들을 프레젠테이션 계층으로부터 분리하여 독립적으로 변경할 수 있습니다.
DTO에 @Getter와 @NoArgsConstructor 어노테이션을 추가하면 직렬화와 역직렬화가 가능해집니다.
[왜?] dto에 @getter 어노테이션이 없으면 안 돼?
(위 링크를 보시면 자세히 설명이 되어있습니다)
이러한 장점을 통해 데이터 포맷을 변경해야 할 때 DTO 클래스만 수정하면 된다는 장점을 가지고 있습니다.
쉽게 설명을 하자면 Client로부터 받는 값들 중에 새로운 타입의 값이 추가되면 DTO만 변경하면 됩니다.
* 이때 Entity에는 이미 해당 값이 존재해야 합니다.
기획팀에게 User의 정보에서 주소를 추가로 보여주자는 의견이 나왔을 때 DTO만 변경하면 됩니다.
* 이때 Entity에는 Address라는 정보가 있으며, 기존의 DTO에는 Address가 없어야 합니다.
도메인 모델을 프레젠테이션 계층으로부터 분리하여 독립적으로 변경할 수 있습니다.
*도메인 모델이란 도메인이 비즈니스 로직의 주도권을 가지고 있는 것을 의미하며, DDD의 핵심요소 중 하나입니다.
하지만 본 글은 DDD(Domain-Driven Design)이 아닌 핵심 로직이 Service에 위치한 트랜잭션 스크립트 패턴을 사용하고 있으므로 위 장점에 대한 설명은 하지 않겠습니다.
그래도 설명을 하자면...!
예를 들어 바지, 긴팔 옷, 반팔 옷, 패딩을 파는 쇼핑몰에서 메인 페이지에 긴팔 옷만 보여줬는데 겨울이 끝나 반팔 옷을 보여주려고 합니다. 현재 메인페이지의 옷들을 보여주는 로직은 도메인에 있기 때문에 반팔 옷으로 보여주는 로직으로 변경해야 합니다.
이렇게 UI가 비즈니스 로직에 침투하여 UI를 변경할 때마다 도메인 모델을 변경해야 한다는 단점이 생깁니다.
그러면 언제 사용해?
- DAO(Repository)에서 정보를 얻은 뒤, Service 계층으로 Entity를 전송하고 Presentation(Controller) 계층으로 보낼 때 DTO로 변환을 한 뒤, 보낸다.
- Client(사용자)에게 입력 값을 전달받은 뒤, Presentation(Controller) 계층에서 DTO로 전송을 받습니다.
- 주로 계층 간 정보 전달을 할 때 사용합니다.
지금부터는 필자의 개인적인 생각이 많이 포함되어 있습니다.
잘못된 정보가 있으면 댓글 부탁드리겠습니다.
2. DTO --> Entity 변환
DTO를 Entity로 변환을 하게 되면 메서드를 통해 Entity를 간단히 만들 수 있다는 장점이 있습니다.
또한 @Builder, 생성자등 다양한 방법으로 구현할 수 있습니다.
//변환 로직
Member member = memberRegistRequestDto.of();
//DTO
public record MemberRegistRequestDto(
@NotBlank String oauthId,
@NotBlank String oauthPlatform,
@NotBlank String name,
...
) {
public Member of() {
return Member.builder()
.oauthId(oauthId)
.oauthPlatform(getOAuthProviderFromString(oauthPlatform))
.name(name)
...
.build();
}
private OAuthProvider getOAuthProviderFromString(String oauthPlatform) {
return switch (oauthPlatform.toLowerCase()) {
case "kakao" -> OAuthProvider.KAKAO;
case "google" -> OAuthProvider.GOOGLE;
case "apple" -> OAuthProvider.APPLE;
default -> throw new UnknownOAuthPlatformException();
};
}
}
최소 지식 원칙이란?
객체 지향 구현시 모듈 간의 결합도를 최소화하여 설계한다는 원칙입니다.
모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 것을 의미합니다.
최소 지식 원칙이라는 개념을 위 코드에 대입시켜 본다면 DTO에서 Entity를 생성하므로 Entity에 대한 지식을 가지고 있는 것입니다. 이는 결합도를 증가시키며 Entity를 수정했을 때 DTO의 of 메서드도 수정을 해야 한다는 큰 단점을 가지게 됩니다.
또한 Entity의 멤버 변수 타입에 맞게 DTO에서 추가 변환을 해줘야 하는 단점도 생깁니다.
3. ENTITY --> DTO 변환
최소 지식의 개념을 위 방법에 대입을 시켜봐도 Entity가 DTO의 정보를 알아야 하므로 결합도가 높아지는 단점이 생깁니다.
추가적으로 Entity에 모든 DTO에 대한 변환 로직을 생성해야 하므로 Entity의 코드가 길어지는 단점도 생깁니다.
4. Mapper를 이용한 Entity <--> DTO 변환
다른 개발자분들은 어떻게 생각하시는지 궁금해서 개발자 커뮤니티인 StackOverFlow에 글을 작성해 보았다.
One approach to your problem is through a mapper class.
StackOverFlow에서는 mapper 클래스를 생성해서 구현하라는 답변을 받았다. (shout out to dani-vta)
Mapper Class를 사용하게 되면 Entity와 DTO가 서로 내부 구현에 대한 지식이 없으므로 결합도가 낮아지는 장점이 있습니다. 이러한 장점 덕분에 나는 Mapper를 사용해서 변환을 해보려고 한다.
public class MemberMapper {
public static MemberRegistRequestDto toMemberRegistRequestDto(Member m) {
MemberRegistRequestDto dto = new MemberRegistRequestDto();
/* mapping logic */
return dto;
}
public static Member toMember(MemberRegistRequestDto dto) {
Member m = new Member();
/* mapping logic */
return m;
}
}
참고 문서
- https://guntherpopp.blogspot.com/2010/09/to-dto-or-not-to-dto.html
- https://stackoverflow.com/questions/44884901/where-when-and-how-convert-dto-to-from-entity
- https://www.baeldung.com/java-dto-pattern