
Spring Boot를 사용하며 우리는 DTO를 통해 유저에게 반응을 전달하고, 레이어 간에 데이터를 이동시킵니다.
우선 DTO에 대해서 자세히 알아보고, 평소에 우리는 어떻게 사용하는지 살펴봅시다.
DTO(Data Transfer Object, 데이터 전송 객체)란?
DTO는 말 그대로 데이터를 임시로 담고 있는 객체입니다. 계층과 계층 사이에서 데이터를 전송하기 위해서 사용됩니다.
Client-Server 프로젝트에서는 각자 서로 다른 구조로 구성되는 경우가 있습니다. 각 영역에서는 중점으로 하는 점이 다른데요. Client Side에서는 사용자 친화적인 표현을 하기 위한 구조를 지향하고, 서버에서는 DB의 테이블 구조와 유사하거나, 성능이 뛰어난 방식을 선호하게 됩니다. 이러한 이유로 DTO는 같은 단어이지만, 두 개의 서로 다른 계층(Client-Server)에서 다른 구조를 사용합니다.

글로만 설명하기에는 이해가 어려울 수 있으니 게시판을 사용하는 예제 코드로 설명하겠습니다.
DTO 예제 코드
public class CreatePostRequest {
private String title;
private String content;
// 생성자, Getter, Setter는 생략했습니다.
}
public class PostResponse {
private final Long id;
private final String title;
private final String content;
// 생성자, Getter, Setter는 생략했습니다.
}
@GetMapping("/api/v1/posts")
public ResponseEntity<PostResponse> getPost(
@RequestParam("postId") Long postId
) {
// Service 계층에서 반환하는 DTO를 그대로 Controller에서 반환
return ResponseEntity.ok(postService.getPost(postId));
}
위와 같은 형태의 게시판을 반환하는 구조가 있습니다. 현재 CreatePostRequest, PostResponse 두 가지가 위 그림에서 Server와 Client 사이에서 데이터를 전달하는 DTO 입니다.
하지만 위의 코드에서 불편한 점을 찾을 수 있으신가요?
...
현재 구조를 보면 service 계층에서 반환하는 DTO를 그대로 Controller에서 API로 반환하고 있습니다. 그렇다면 이러한 코드는 어떠한 문제가 발생할 수 있을까요?
제가 생각한 두 가지 문제에 대해서 설명해보겠습니다.
1. Service 메서드의 범용성 부족
위의 코드처럼 단순한 Response DTO를 반환하는 경우 말고, 모듈에서 해당 메서드를 사용하는 곳이 많다고 가정해봅시다.
만약 해당 메서드의 반환 객체(DTO)에 대한 필드가 변경된다면, 이 메서드와 연결된 모든 Controller 코드와 테스트 코드를 수정해야 할 것입니다.
즉, Service가 Controller(Presentation) 계층의 스펙에 의존하게 된다면 계층 간의 분리가 모호해집니다.
2. API 스키마 변경을 알기 어려움
가장 문제가 되는 지점이라고 생각합니다. 실무에서는 v1, v2 등 다양한 API 버전을 지원해야하는 경우가 있습니다.
Service 계층의 로직은 하나인데, API 버전마다 반환해야 하는 필드가 다르면 어떻게 할까요? Service가 반환하는 DTO를 그대로 API 응답 DTO로 사용하게 된다면, API 스펙이 변경될 때마다 비즈니스 로직까지 바꿔야하는 상황이 발생할 수 있습니다. 또한, 테스트 코드를 통해서 예측할 수 있지만, 테스트 코드에서 DTO 클래스로 반환 객체를 검증하고 있다면 테스트에서도 통과할 수 있습니다.
더 나은 방법은 무엇일까?
각 계층에서 DTO를 구분하는 것입니다. 아래의 User를 반환하는 예제를 살펴보겠습니다.
@GetMapping("/api/v1/users")
public ResponseEntity<SuccessResponse<UserResponse>> getUser(
@RequestParam("userId") Long userId
) {
// 1. Service는 비즈니스 로직의 결과(UserInfo)만 반환
UserInfo userInfo = userService.getUser(userId);
// 2. Controller에서는 API 스펙에 해당하는 DTO(UserResponse)만 반환
UserResponse response = UserResponse.of(userInfo);
return ResponseEntity.ok(new SuccessResponse<UserResponse>(200, response));
}
Service 계층에서 반환하는 DTO(UserInfo)를 바로 Controller에서 반환하는 것이 아니라, Controller 계층에서 API 계층으로 반환할때 새로운 DTO(UserResponse)로 변환하는 것입니다.
위와 같은 형태를 통해서 getUser 메서드의 반환 파라미터가 변경되더라도, 해당 API 엔드포인트는 항상 같은 스키마를 반환할 수 있습니다. 반대로 API에서 원하는 필드가 추가되더라도 UserResponse의 매핑 로직만 변경하면 됩니다.
또한, UserInfo 클래스(Service DTO)에 대한 구현을 record 등을 활용해 불변 객체로 구현하여 책임을 명확히 하고, 안정성을 높일수 있습니다.
끝내며..
위와 같은 구조의 변경을 통해서 Service 메서드의 범용성 향상과 API 스키마 변경을 알기 어렵다는 단점을 보완할 수 있습니다.
물론 같은 내용의 DTO를 여러개 만들고, 반환하여 보일러 플레이트 코드가 증가하는 점이 단점이 있을 수 있습니다. 하지만 스키마 변경으로 시스템 전체에 문제가 발생하는 상황보다는 낫다고 생각합니다.
서버 개발을 공부하는 사람들이 많이 놓치는 부분이라고 생각하여 글을 작성해보았습니다. 감사합니다.
'Code > Spring' 카테고리의 다른 글
| HttpServletRequest Body를 여러번 읽을 수 없을까? - ContentCachingRequestWrapper (0) | 2025.12.17 |
|---|