DTO(Data Transfer Object)
란 계층간 데이터 교환을 위해 사용되는 객체이다. DB
에서 꺼낸 데이터를 저장하는 Entity
를 Controller
와 같은 클라이언트단과 직접 마주하는 계층에 직접 전달하는 대신 DTO
를 통해 데이터를 교환한다.
DTO
의 역할은 계층간 데이터 교환이 이루어질 수 있도록 하는 객체이기 때문에, 특별한 로직을 가지지 않는 순수한 객체여야 한다.
DB
에서 꺼낸 값을 DTO
에서 임의로 조작할 필요가 없기 때문에 DTO
에서는 Setter
를 만들 필요가 없고 생성자 또는 Builder
패턴을 통해 값을 할당한다.
관심사의 분리
Entity
는 주로 데이터베이스와 관련된 비즈니스 개체를 표현하는 데 사용된다. 반면에 DTO
는 클라이언트와의 데이터 교환을 위해 사용된다. Entity
는 데이터의 상태와 비즈니스 규칙을 포함하고 있으며, DTO
는 특정 작업을 수행하기 위해 필요한 데이터만 포함한다. Entity
와 DTO
를 분리함으로써 관심사를 분리하고 각각의 역할에 집중할 수 있다.
유연성과 확장성
Entity
의 값이 변경되는 경우 Repository
클래스의 Entity Manager
의 flush
가 호출될 때 DB
에 변경값이 반영된다. DTO
를 사용하여 Entity
의 변경으로 인한 영향을 최소화할 수 있고 클라이언트에게 전달되는 데이터를 쉽게 조정하고 확장할 수 있다.
데이터 은닉
Entity
는 데이터베이스와 직접적으로 관련되어 있으므로, 일부 데이터는 애플리케이션의 내부에서만 사용되는 경우가 있을 수 있다. DTO
를 사용하면 클라이언트에게 필요한 데이터만을 선택하여 전달하므로, 불필요한 정보 노출을 방지하고 데이터 은닉을 보장할 수 있다.
아래의 코드는 Post Entity
에 대한 DTO
클래스이다.
PostRequest
는 사용자의 게시글 조회 요청에 대한 DTO
이고,
PostResponse
는 조회된 게시판을 클라이언트로 리턴할때 사용되는 DTO
다.
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostRequest {
private String title;
private String content;
private String writer;
public Post toEntity() {
return Post.builder()
.title(title)
.content(content)
.writer(writer)
.build();
}
}
@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostResponse {
private Long id;
private String title;
private String content;
private String writer;
@Builder
public PostResponse(Post post) {
this.id = post.getId();
this.title = post.getTitle();
this.content = post.getContent();
this.writer = post.getWriter();
}
}
PostRequest
는 toEntity()
메서드를 통해 Entity
로 변환이 가능하다.
PostResponse
에서는 Builder
생성자를 통해 Entity
를 DTO
로 변환 가능하다.
이러한 DTO
들을 작성하고 난 후 Entity
- DTO
간의 변환 작업을 어느 레이어에서 처리해야 될지에 대한 궁금증이 생겼다.
@PostMapping
public ResponseEntity<Long> create(@RequestBody PostRequest PostRequest) {
return ResponseEntity.ok().body(postService.create(PostRequest.toEntity()));
}
@GetMapping("/{postId}")
public ResponseEntity<PostResponse> getPost(@PathVariable Long postId) {
Post post = postService.getPost(postId);
return ResponseEntity.ok().body(PostResponse.builder().post(post).build());
}
위의 두 코드는 Controller Layer
에서 Entity
- DTO
간 변환 작업을 처리하는 방식이다.
Presentation Logic 분리: DTO
는 주로 클라이언트에게 데이터를 전달하기 위한 용도로 사용되므로, Controller
에서 DTO
와의 변환 작업을 수행하면 Presentation Logic
을 Controller
내부로 격리시킬 수 있다.
Controller 레벨에서 유효성 검사: Controller
에서 DTO
와의 변환 작업을 수행하면, 변환 과정에서 유효성 검사를 수행할 수 있습니다. 이로써 더욱 엄격한 데이터 유효성을 적용할 수 있습니다.
Service의 독립성 유지: Service
레이어에서 Entity
에만 의존하고 DTO
변환 작업을 수행하지 않으면, Service
레이어의 독립성과 재사용성이 높아진다.
반복적인 작업: DTO
와 Entity
간의 변환 작업은 여러 Controller
메소드에서 반복적으로 수행되어 코드 중복이 발생한다.
비즈니스 로직과 혼재: DTO
와의 변환 작업을 Controller
에서 수행하면, 비즈니스 로직과 DTO
변환 로직이 혼재되어 코드의 가독성과 유지보수성이 저하된다.
@PostMapping
public ResponseEntity<Long> create(@RequestBody PostRequest PostRequest) {
return ResponseEntity.ok().body(postService.create(PostRequest));
}
@GetMapping("/{postId}")
public ResponseEntity<PostResponse> getPost(@PathVariable Long postId) {
return ResponseEntity.ok().body(postService.getPost(postId));
}
위의 두 코드는 Service Layer
에서 Entity
- DTO
간 변환 작업을 처리하는 방식이다.
단일 장소에서 변환 작업 관리: Service
는 비즈니스 로직을 관리하는 곳이므로, DTO
와 Entity
간의 변환 작업을 Service
내에서 수행하면 변환 작업을 단일한 장소에서 관리할 수 있다.
코드 중복 감소: Service
내에서 DTO
와 Entity
간의 변환 작업을 수행하면, 여러 Controller
메소드에서 동일한 변환 작업을 반복할 필요가 없어진다.
비즈니스 로직과 분리: DTO
와의 변환 작업을 Service
에서 수행하면, 비즈니스 로직과 변환 로직을 분리하여 코드를 관리할 수 있다.
Service의 복잡도 증가: Service
에 변환 작업까지 포함되면 역할이 복잡해질 수 있다.
코드 재사용성 하락: Service
에서 특정 DTO
에 의존하게 되면 여러 종류의 Controller
에서 해당 Service
를 이용할 수 없어 코드 재사용성이 떨어지게 된다.
Controller
에서 변환 작업을 수행하는 것과 Service
에서 변환 작업을 수행하는 것은 각각 장단점이 존재하여 프로젝트의 구조와 성격에 따라 선택을 하는 것이 필요하다.
하지만 일반적으로 Service
에서 변환작업을 수행하는 것이 다음과 같은 이점이 존재한다.
레이어 간의 역할 분리: Bussiness Logic
과 Presentation Logic
을 분리하여Controller
와 Service
간의 역할을 명확히 구분한다.
단일 책임 원칙: Service
는 주로 비즈니스 로직을 처리하는 컴포넌트로 간주되며, DTO
와 Entity
간의 변환 작업은 비즈니스 로직과 관련이 있다. 따라서 변환 작업을 Service
에서 처리함으로써 단일 책임 원칙을 준수할 수 있다.