
시작하기에 앞서
DTO(Data Transfer Obejct)와 VO(Value Object)의 구분은 단순한 코딩 컨벤션의 문제가 아니다.
이는 소프트웨어 아키텍쳐의 패러다임이 어떻게 변해왔는지에 대한 살아있는 화석과도 같다
이 논쟁의 역사를 추적하는 것은 '분산 객체' 시대에서 '도메인 주도 설계(DDD)' 시대로 그리고 최종적으로 '함수 지향 프로그래밍(FP)'의 영향을 받은 현대로
이어지는 기술 사상적 흐름을 이해하는 것과 같다.
둘은 모두 '값(Value) 중심의 객체'라는 점에서 비슷해보이지만, 설계 목적, 설계 방식에 따라서 나뉜다.
일반적으로 DTO는 데이터의 형태를 위한 것이고, VO는 데이터의 의미를 나누는데, 그렇다면 DTO와 VO는 무엇인지에 대해서 추적해보자.
그 전에 들어가기 앞서 필요한 기초지식을 조금 첨부하도록하겠다.
도메인 모델과 비즈니스 로직에 대한 것을 조금 설명해두는 편이 독자에게 편하기때문에 설명을 해두자면 다음과 같다.
필요한 사전 지식들
1. 도메인 모델(Domain Model)
도메인 모델 은 특정 문제 영역(도메인)을 표현하는 객체들의 집합
현실 세계의 개념을 코드로 추상화하여 구현한 것
예를 들어, 음악 스트리밍 서비스라면 "앨범", "아티스트", "트랙" 같은 개념이 도메인 모델에 포함될 수 있음
도메인 모델의 특징
객체 지향적: 클래스와 객체로 구성된다.
예: Album, Artist, Track 클래스
.
관계 중심: 도메인 객체 간에는 상호 관계가 존재한다.
예: 하나의 앨범(Album)은 여러 트랙(Track)을 가지고, 각 트랙은 여러 아티스트(Artist)와 연결될 수 있습다
비즈니스 규칙 반영: 도메인 모델은 해당 도메인의 핵심 규칙과 제약 조건을 반영한다.
예: "앨범에는 최소 한 개 이상의 트랙이 있어야 한다."
2. 비즈니스 로직(Business Logic)
비즈니스 로직 은 소프트웨어가 해결하려는 문제를 처리하기 위해 필요한 규칙과 절차
도메인 모델 위에서 동작하며, 실제 업무 요구사항을 구현
예를 들어, "앨범을 생성할 때 반드시 아티스트 정보가 있어야 한다"와 같은 규칙이 비즈니스 로직에 해당
비즈니스 로직의 특징
도메인 모델 기반: 비즈니스 로직은 도메인 모델을 활용하여 구현
핵심 기능: 애플리케이션의 핵심적인 동작을 정의
예: "앨범을 생성하고, 트랙을 추가하고, 아티스트를 연결한다."
유연성: 비즈니스 요구사항이 변경되면 비즈니스 로직도 수정되어야 한다.
도메인 모델은 현실 세계의 개념을 객체로 표현한 것이고, 비즈니스 로직은 도메인 모델 위에서 실제 업무 요구사항을 처리하는 규칙인 것이다.
도메인 모델은 데이터와 상태라는 좀 더 포괄적인 상위 개념을, 비즈니스 로직은 데이터를 활용하여 작업을 수행하는 구체적인 구현을 말한다.
3. 엔티티(Entity)
엔티티는 특정 도메인 내에서 고유한 식별자를 가지고, 시간이 지나도 동일성을 유지하는 객체
현실세계의 개체를 모델링해서, 데이터와 상태를 저장하고 관리
예시를 들자면, '사용자(User)'나 '상품(Produc)'는 엔티티로 표현될 수 있다.
엔티티의 핵심 특징
고유 식별자(ID):
각 엔티티는 고유한 식별자를 가지고 있어, 다른 객체와 구분됨
예: User 클래스의 userId, Product 클래스의 productId.
지속성(Persistence):
엔티티는 데이터베이스나 파일 시스템에 저장되어, 애플리케이션이 종료되더라도 상태를 유지한다.
동일성(Identity):
두 엔티티가 동일한 ID를 가지면, 데이터가 달라도 같은 객체로 간주된다.
예: User 객체 A와 B가 같은 userId를 가지면, A와 B는 동일한 사용자를 의미.
상태 변화(State Change):
엔티티는 시간이 지남에 따라 상태가 변할 수 있지만, 식별자는 변하지 않는다
예: 사용자가 이름을 변경하더라도 userId는 동일하게 유지됨
두 거인의 탄생(2002~2003)

데이터 전송 객체 (DTO)
프로세스 간 데이터를 전달하기 위한 객체로서, 메서드 호출 횟수를 줄이기 위해 사용됩니다.
원격 인터페이스(예: Remote Facade (388))와 작업할 때, 각 호출은 비용이 많이 듭니다. 따라서 호출 횟수를 줄이고, 이는 한 번의 호출로 더 많은 데이터를 전송해야 함을 의미합니다. 이를 달성하는 방법 중 하나는 많은 매개변수를 사용하는 것입니다. 그러나 이는 종종 프로그래밍하기 어렵고, 특히 Java와 같은 언어에서는 단일 값만 반환할 수 있기 때문에 불가능할 수도 있습니다.
해결책은 모든 데이터를 포함할 수 있는 데이터 전송 객체(DTO)를 생성하는 것입니다. 이 객체는 연결을 통해 전송되려면 직렬화가 가능해야 합니다. 일반적으로 서버 측에서 DTO와 도메인 객체 간 데이터를 전송하기 위해 어셈블러가 사용됩니다.
많은 사람들이 Sun 커뮤니티에서는 이 패턴을 "Value Object"라고 부릅니다. 하지만 저는 이를 다른 의미로 사용합니다. 487페이지의 논의를 참조하세요.
DTO(Data Transfer Object)의 탄생

DTO는 Martin Fowler의 『Patterns of Enterprise Application Architecture』에서 알려져서 널리 퍼졌다.
핵심 논의는 다음과 같다.
'원격 호출을 최소화하기 위해서, 여러 번의 호출로 가져올 데이터를 하나의 멍청이(Dumb) 객체에 모두 담아, 단 한번의 호출로 전송하자.
이때의 DTO는 가변(mutable)객체로, 일반적으로 DTO를 가변 객체, VO를 불변 객체로 설명하던 것은 여기에서 시작된다.
그럼 왜 이런 논의가 이루어졌을까?
00년대 초반엔 EJB(Enterprise JavaBeans)와 같은 분산 컴포넌트 기술이 주류였다. 이 시대의 가장 기술적 제약은 무엇이었을까?
그것은 네트워크 호출 비용의 문제였다. 서버와 클라이언트, 그리고 서버와 서버간의 원격 호출의 비용은 지금과 달리 매우 비쌌다고 한다.
분산 컴포넌트 시스템이란?: 여러 컴퓨터를 노드로 두어, 독립적인 컴포넌트들이 분산되어 동작하고, 이것들이 전체적으로 통합된 시스템처럼 작동하는 아키텍쳐.
원격에 있는 객체의 정보를 가져오기 위해서(당시에는 분산 컴포넌트 시스템이므로, 원격 인터페이스를 통해서 가져옴), 이 호출 비용을 치루어야했는데
이 비용은 상당히 비쌌고, 그것을 줄이기 위한 객체가 바로 DTO였다.
이때 DTO는 오직 데이터 전송만을 위한 것이었다. 실제로 마틴 파울러가 제시한 예시를 아래에 언급하도록 하겠다.
우선 들어가기 앞서 코드의 목적은 다음과 같다.
도메인(Domain Model)에서 필요한 데이터를 추출하여, 간단한 구조로 변환, 이를 직렬화(Serialization) 가능한 형식으로 만들어 원격 호출에 사용한다.
그리고, 클라이언트로부터 받은 데이터를 다시 도메인 모델로 변환하는 예시를 보여준다.
도메인 모델: 도메인 모델은 애플리케이션의 핵심 비즈니스 로직과 데이터를 나타내는 객체 (ex: Album,Track,Artist 클래스)
이 클래스들은 복잡한 관계와 매서드를 가지고, 원격 호출에는 적합하지 않다
데이터 전송 객체(DTO)[Data Transfer Object]: 도메인 모델의 데이터를 단순화하고, 직렬화 가능한 방식으로 변환한 객체(ex: AlbumDTO,TrackDTO),
어셈블러(Assembler): 어셈블러는 도메인 모델과 DTO 간의 변환을 담당한다. (ex: AlbumAssembler 클래스), 이 클래스는 도메인 객체를 DTO로 변환하거나, DTO를 도메인객체로 변환한다. 현대 아키텍쳐에서는 어셈블러는 잘 쓰지 않고 일반적으로 Mapper로 쓰이는데, 이 부분은 왜 이렇게 변형되었는지는 추후 시간이 나면 쓸 예정이다.
사실 이렇게 하면 이해가 어렵지만
마틴 파울러의 Patterns of Enterprise Application Architecture(P of EAA)에서 예시를 통해서 도메인 모델을 설명하는데,
자 이제 핵심 개념이 이해되어 있으니, 아래의 코드블록을 보도록하자.
(1) DTO 작성 (Domain → DTO)

코드를 해석하면 다음과 같다
writeDTO: Album 객체를 AlbumDTO로 변환
앨범 제목(title)과 아티스트 이름(artist)을 DTO에 설정
트랙 정보는 writeTracks 메서드를 통해 처리
writeTracks: 앨범에 포함된 트랙들을 TrackDTO 배열로 변환
각 트랙의 제목과 연주자 정보를 추출하여 DTO에 저장
writePerformers: 트랙의 연주자 이름을 문자열 배열로 변환
(2) DTO를 이용한 도메인 생성 (DTO → Domain)

해석:
createAlbum: DTO를 기반으로 새로운 앨범 객체를 생성.
아티스트와 트랙 정보를 레지스트리(Registry)에서 찾아 연결
createTracks: DTO의 트랙 정보를 기반으로 새로운 트랙 객체를 생성
createPerformers: 트랙의 연주자 정보를 기반으로 아티스트 객체를 연결
(3) DTO를 이용한 도메인 업데이트

해석:
updateAlbum: 기존 앨범 객체를 DTO의 정보로 업데이트
제목, 아티스트, 트랙 정보를 비교하여 변경 사항을 반영
updateTracks: 트랙 정보를 업데이트하거나 새로 추가
VO (Value Object)의 탄생 - Eric Evans의 『DDD』 (2003)

값 객체 (VALUE OBJECTS)
많은 객체들은 개념적인 식별성(conceptual identity)을 갖지 않습니다. 이러한 객체들은 어떤 사물의 특성을 묘사합니다.
한 아이가 그림을 그리고 있다면, 아이는 자신이 선택한 마커의 색상에는 신경을 씁니다. 팁의 날카로움에도 신경을 쓸 수 있습니다. 하지만 같은 색상과 모양의 마커가 두 개 있다면, 아이는 어느 것을 사용하든 신경 쓰지 않을 것입니다. 만약 마커 하나를 잃어버리고 새 포장 상자에서 나온 같은 색의 다른 마커로 교체하더라도, 아이는 그 교체에 개의치 않고 작업을 재개할 수 있습니다.
냉장고에 붙어있는 여러 그림에 대해 아이에게 물어보면, 아이는 자신이 만든 것과 여동생이 만든 것을 빠르게 구별할 것입니다. 아이와 여동생은 유용한 식별성을 가지고 있으며, 그들이 완성한 그림들도 마찬가지입니다. 하지만 그림 속의 각 선이 어떤 마커로 그려졌는지까지 추적해야 한다면 얼마나 복잡해질지 상상해 보십시오. 그림 그리기는 더 이상 어린아이의 놀이가 아닐 것입니다.
모델에서 가장 눈에 띄는 객체들은 보통 엔티티(ENTITIES)이며, 그들의 식별성을 추적하는 것은 매우 중요하기에, 모든 도메인 객체에 식별성을 부여하려는 생각을 자연스럽게 시작하게 됩니다. 때로는 모든 객체에 고유 ID를 할당하는 프레임워크가 만들어지기도 합니다. 분산 시스템과 데이터베이스 저장소 전반에 걸쳐 객체를 완벽하게 추적하는 방법을 고안하기 위해 추가적인 분석적 노력이 들어갑니다.
이는 시스템이 그 모든 추적을 감당해야 하고, 많은 잠재적인 성능 최적화가 배제되기 때문에 시스템 성능 면에서 비용이 많이 들 수 있습니다. 똑같이 중요한 것은, 이것이 모든 객체를 같은 틀에 억지로 집어넣고, 오해의 소지가 있는 인위적인 식별성을 덧붙여 모델을 혼란스럽게 만든다는 점입니다.
엔티티의 식별성을 추적하는 것은 필수적이지만, 다른 객체들에 식별성을 부여하는 것은 시스템 성능을 해치고, 분석 작업을 추가하며, 모든 객체를 똑같이 보이게 만들어 모델을 혼란스럽게 할 수 있습니다. 소프트웨어 설계는 복잡성과의 끊임없는 싸움입니다. 우리는 구별을 통해, 어떤 특별한 처리가 필요한 곳에만 적용되도록 해야 합니다.
하지만 우리가 이 범주의 객체를 단지 식별성이 없는 것으로만 생각한다면, 우리의 도구 상자나 어휘에 많은 것을 추가하지 못한 것입니다. 사실, 이러한 객체들은 그들만의 특징과 모델에 대한 그들만의 중요성을 가집니다. 이것들이 바로 사물의 본질을 묘사하는 객체들입니다.
개념적 식별성이 없는 도메인의 서술적 측면을 나타내는 객체를 **값 객체(VALUE OBJECT)**라고 부릅니다. 값 객체는 우리가 그것이 '누구'인지가 아니라 '무엇'인지에만 관심을 갖는 설계의 요소를 나타내기 위해 인스턴스화됩니다.

프로그래밍이 점점 복잡해짐에 따라서,
비즈니스 로직또한 복잡해졌다. 그러한 과정에서 비즈니스 로직을 '우아하게' 표현하고자 하는 과정에서 나온 것이 VO(Value Object)이다.
그리고 이 VO라는 개념은 Domain Driven Design 이라는 소위 DDD라는 명저중의 명저에서 소개되어 널리 퍼진 개념으로
'값과 그 값에 대한 비즈니스 로직을 하나의 Object로 캡슐화하고, 이 객체는 ID가 아닌, 가진 값(Value)로 동등성을 비교하며, 한 번 생성되면 변하지 않는
불변(Immuatable)값이어야한다.'

(DDD 책의 VO 예시)
(1) Address Value Object

(자바 14 이전 VO를 구현한 코드, 자바에서는 14이후 record 를 쓰는게 더 안전하다고 하지만 이 글은 시대상에 따른 변화를 보여주기위해 쓰는것)
Address는 주소 정보를 담는 Value Object로, street, city, state 등의 속성을 포함.
Value Object는 일반적으로 불변 객체이며, 동일한 값이면 동일한 객체로 간주된다.
초기에는 이렇게 두 개념의 역할은 명확했다.
과거
항목 | DTO | VO |
---|
목적 | 네트워크 전송용 | 도메인 모델 표현 |
구조 | 가변 객체 (get; set; ) | 불변 객체 (final , val , readonly ) |
로직 | 없음 | 유효성 검사, 동등성 등 존재 |
위치 | API 경계 | 도메인 계층 |
-> 이때는 구분이 명확했음.
DTO는 직렬화 용이성, VO는 의미 표현을 중시했음.
DTO는 원격 통신을 하기 위한 모든 데이터가 들어간 상자,
VO는 도메인을 설계할때 필요한 의미를 가지는 값
하지만 하드웨어의 성능이 좋아짐에 따라서 이 경계가 무너지기 시작했다.
어째서 DTO VS VO를 가지고 이야기하기 시작했는가?

분산 시스템과 EJB 시대를 지나, 강력한 프레임워크 스프링이 만들어진 것이다.
과거에는 분산 시스템과 EJB 시대와 스프링 프레임워크 시대의 어떤 차이가 있는지 예시를 들
면 끝도 없지만
간단한 비유를 통해서 이야기해보겠다
옛날의 집은 시설이 좋지 않아서, 집안에 주방이 없었다(EJB 시대)
결국 침실은 집에 있고,
침실, 주방,화장실이 외부에 있어서(별개의 서버)에 있어서, 침실에 일어나서 밖에 있는 주방으로 아침을 먹으려면 밖을 나가야했으며
화장실에 가기 위해서도 역시 외부에 호출을 해야만 했고
(네트워크 호출),
외부에서 주방에서 화장실등을 자유롭게 이동하기위해서 가기위해서 모든 가재도구(똥싸고 닦을 휴지, 식사를 위한 칼,나이프)를 들고 나가야만했다.
하지만 스프링을 위시한 강력한 웹 프레임워크가 등장하고
분산 시스템의 경계가 사라지고, 단일 애플리케이션 내에서 계층 간 데이터
(Controller ↔ Service ↔ Repository) 간 데이터 전달이 주된 관심사가 되었다.
즉 집안에 모든 방이 다 있게 됨에 따라서,굳이 비싼 네트워크 호출 비용이 사라짐에 따라
DTO가 원격 통신용 객체에서, 계층간 데이터 전달을 위한 객체라는 의미로 사용되기 시작했고,
"어차피 같은 집안에 있는데, 굳이 모든 가재도구(DTO)를 들고 다녀야하나? 그때 그때 필요한 물건(VO)을 꺼내 쓰면 안되나?"
따라서 DTO는 전송 객체, VO는 도메인 모델링 객체라는 구분이 흐려졌고, 왜 꼭 DTO를 써야만 하느냐는 질문이 설득력을 갖기 시작되었다.
DTO는 필요 없어졌는가?

(클린코드 8장 경계)
결론부터 말하자면 DTO의 역할은 약간 다른식으로 변화하기 시작했다.
엉클밥의 명저 클린 코드에서 말했듯 '시스템은 외부 코드와 명확한 경계를 설정해야한다"
라는 말이 있다.여기서 외부 코드와 경계를 설정하는 경계 객체로써, DTO가 사용되기 시작한것이다.
과거의
DTO는 외부와의 경계에서 사용되고, API,DB, 메시지 큐,파일 등에 사용되고
VO는 내부 모델에서 사용된다.
즉 DTO는 외부 API를 감싸는 경계 객체(Boundary Adapter)로써 기능하기 시작한 것이다.
즉 DTO의 역할은 외부와의 의사 소통을 위한 번역 도구,
VO는 내부 도메인에서의 언어 자체가 된 것이다.
여기에 더해서 멀티 쓰레딩이 주류화 됨에 따라서, DTO에서도 새로운 변화가 생긴다.

(이펙티브 자바 17장 가변 객체를 최소화하라)
10년대 초반 멀티 쓰레딩이 다시금 화두가 된 시점에서 함수 지향의 장점인 데이터 불변성을 도입하게 되는데,
이때 시대의 조류에 따라 DTO또한 데이터의 불변성을 고려해서 가변(Mutable)객체에서
record 키워드등을 도입하여, 불변 객체(Immutable)가 되어간다.
(이후 로버트 마틴은 《클린 아키텍처》에서 UseCase Output DTO라는 개념을 통해, DTO는 외부 세계와 Use Case 계층 사이의 단방향 인터페이스로 정의해야 한다고 강조한다. 즉, DTO는 항상 '경계의 밖'에 있어야 하며, 도메인 모델은 그 경계 안에서만 존재해야 한다는 원칙이다 하지만 이걸 쓰면 글의 구조가 흐트러지기때문에 단순 그런게 있다라는 식으로만 이해해도 된다.)
그리고 이에따라서 DTO와 VO 모두 '불변성'을 기본값으로 가지게 됨에 따라서, 둘의 차이는 더욱 미묘 해졌고,
현재의 논쟁은 도메인 로직 없는 불변 객체(DTO) VS 도메인 로직 있는 불변 객체(VO)로 바뀌어갔다
현재
항목 | DTO | VO |
---|
불변성 | 권장됨(반 필수) | 필수 |
도메인 로직 포함 여부 | 없음 (단순 컨테이너) | 존재 (동등성, 검증 등 도메인 로직) |
의미 기반 동등성 | ID, 키 기반 또는 무관 | equals , value-based identity 필수 |
위치 | API/입출력 계층 | 도메인 계층 |
의존성 | 프레임워크 기반 (Jackson, gRPC 등) | 독립적, 의미 중심 |
물론 반드시 DTO에 로직이 없다는 것은 아니다.
일례에서 DTO에서도 데이터 검증 로직을 포함하는 경우가 있다. 즉 여기서 말하는 로직은 일반적으로 '도메인 로직을 포함하지 않는다'라는 개념으로 봐야한다.
글을 마치며...

(마틴 파울러)
The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design
-이 안티패턴(빈혈 도메인 모델)의 근본적인 문제점은 객체지향의 디자인과 상반된다.
저렇게 점차 차이가 없어져가는데 우리는 왜 이 둘을 구분하려고 두는가? DTO만 사용하면 안되는가?
그것은 기술적 차이를 넘어 설계 의도(Design Intent)를 코드 수준에서 명확히 표현하려는 노력 때문이다.
DTO를 사용한다는 것: "이 객체는 A 계층에서 B 계층으로 데이터를 전달하는 역할만 할 뿐, 데이터의 의미나 비즈니스 규칙에 대해서는 아무것도 모른다."라는 책임의 경계를 선언하는 것
VO를 사용한다는 것: "이 객체는 '값' 자체로 완전한 의미를 가지며, 내 값에 대한 유효성 검사나 관련 행위는 내가 직접 책임진다."라는 도메인 지식의 캡슐화를 선언하는 것
마틴 파울러는 DTO라는 개념을 널리 알린 사람이지만, 동시에 J2EE 모델 같이 DTO와 같은 데이터만 가진 객체가 도메인 로직의 핵심부에 사용되는 것을 반박한다.
DTO만 사용하는 그것이 안티패턴이며, VO는 단순히 값을 담는 통이 아니라, 값과 관련된 행위를 함께 캡슐화하기때문에 DTO만 존재해서는 안된다.
하지만 VO만 존재한다면 어떻게 될까? 복잡한 비즈니스 로직을 직접 외부와 노출 시켜야하는데, 다르게 말해서, 계층간의 경계를 파괴하여, 외부 요청으로 인해서 도메인의 계층이 침범되고 변형될 수 있다는 것이다. 즉 도메인 객체가 직렬화와 API 계약까지 떠맡게 된다면 도메인 설계가 오염된다.
결국 어떤 것에서도 완전한 정답은 없다
그렇기에 우리는 DTO와 VO사이에서 여전히 줄 다리기를 하고 있으며, 그 사이에서 설계를 고민하는 것이다
댓글 영역
획득법
① NFT 발행
작성한 게시물을 NFT로 발행하면 일주일 동안 사용할 수 있습니다. (최초 1회)
② NFT 구매
다른 이용자의 NFT를 구매하면 한 달 동안 사용할 수 있습니다. (구매 시마다 갱신)
사용법
디시콘에서지갑연결시 바로 사용 가능합니다.