DTO (Data Transfer Object)란?
DTO는 시스템 내부 또는 외부 계층 간에 데이터를 효율적이고 안전하게 전달하기 위한 객체입니다.
주로 계층 간의 경계(boundary)에서 사용되며, 비즈니스 로직을 포함하지 않고 순수 데이터만을 담는 객체입니다
주요 특징
| 항목 | 설명 |
|---|
| 역할 | 데이터 캡슐화 및 전달 |
| 포함 내용 | 필드 + Getter/Setter, toString(), equals(), hashCode() 등 |
| 비즈니스 로직 | ❌ 없음 (단순 데이터 구조체) |
| 불변성(Immutable) | 가능하면 적용 권장 (record, final 등) |
| 계층 간 전달 | Controller ↔ Service ↔ Repository 등에서 사용 |
Java Class로 DTO 정의
public class UserDTO {
private String name;
private String email;
public UserDTO() {}
public UserDTO(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
public void setName(String name) { this.name = name; }
public void setEmail(String email) { this.email = email; }
}
Java 16+ record로 불변 DTO 정의
public record UserDTO(String name, String email) {}
final, getter, 생성자, toString, equals, hashCode 자동 생성
- DTO에 가장 적합한 구조 중 하나
사용 예시 (Spring MVC 기반)
Controller → Service로 전달
@PostMapping("/users")
public ResponseEntity<Void> createUser(@RequestBody UserDTO userDto) {
userService.create(userDto);
return ResponseEntity.ok().build();
}
사용 이유
| 이유 | 설명 |
|---|
| 보안 | Entity 전체를 외부에 노출하면 민감 정보 유출 가능 (ex. 비밀번호, 내부 ID 등) |
| API 명세 분리 | 클라이언트가 원하는 필드 구조로 구성 가능 (Entity와 무관하게) |
| 계층 분리 | Controller, Service, Repository 간의 책임 분리 |
| 성능 최적화 | 필요한 필드만 전송하여 네트워크 비용 절감 |
잘못된 사용 예시
@GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll();
}
→ ✅ DTO로 변환 후 반환해야 함.
정리
| 항목 | 요약 |
|---|
| DTO란? | 데이터 전달 전용 객체 (계층/시스템 간 전송 목적) |
| 포함 내용 | 필드, getter/setter, toString 등 (비즈니스 로직 없음) |
| 사용 목적 | 보안, 성능, API 명세 분리, 계층 분리 |
| 추천 방식 | Java 16+에서는 record 사용으로 불변 DTO 작성 권장 |
VO (Value Object)란?
VO는 객체의 고유 식별자(ID)보다는 속성 값 자체가 객체의 동일성을 결정하는 객체입니다.
주로 작고 변경되지 않는 값(불변 객체)을 표현하며, 도메인 모델링에서 핵심적인 개념입니다.
주요 특징
| 항목 | 설명 |
|---|
| 역할 | 값 자체로 동일성을 판단하는 객체 |
| 식별자 | ❌ 없음 (ID가 아니라 값으로 동등성 비교) |
| 불변성 | ✅ 필수 (값이 변하면 객체 자체가 달라진 것으로 간주) |
| 비즈니스 로직 | ✅ 포함 가능 (자기 완결적 연산) |
| 사용 위치 | 도메인 모델, JPA 임베디드 타입, 계산 결과, 설정값 등 |
Java Class로 VO 구현
public final class Email {
private final String value;
public Email(String value) {
if (!value.matches("^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$")) {
throw new IllegalArgumentException("Invalid email format");
}
this.value = value;
}
public String getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Email)) return false;
Email email = (Email) o;
return value.equals(email.value);
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public String toString() {
return value;
}
}
Java 16+ record로 불변 VO
public record Money(int amount, String currency) {
public Money {
if (amount < 0) throw new IllegalArgumentException("Amount must be non-negative");
}
}
record는 VO에 매우 적합: 자동 final, equals, hashCode, toString 생성
실무 예시 (Address VO)
public record Address(String street, String city, String zip) {
public Address {
if (zip.length() != 5) throw new IllegalArgumentException("Invalid zip code");
}
}
→ Order 엔티티에 포함되는 임베디드 값 객체로 사용
사용 이유
| 이유 | 설명 |
|---|
| 불변성 보장 | 참조 공유로 인한 의도치 않은 변경 방지 |
| 도메인 안정성 | Email, Money, Address 등 의미 있는 값으로 모델링 가능 |
| equals/hashCode 기반 동일성 판단 | 값이 같으면 같은 객체로 취급 (ID 불필요) |
| 캡슐화와 검증 | 유효성 검사를 생성자나 팩토리에서 처리 가능 |
주의 사항
| 항목 | 설명 |
|---|
equals/hashCode 반드시 override | VO의 핵심: 값이 같으면 같은 객체 |
| setter 절대 금지 | 불변 객체 원칙 위반 |
JPA에서는 @Embeddable 활용 | VO를 Entity 내부에 내장 타입으로 사용 가능 |
정리
| 항목 | 요약 |
|---|
| VO란? | 값 자체로 객체를 정의하는 불변 객체 |
| 불변성 | 필수 (생성 후 값 변경 불가) |
| 동일성 | 식별자(X) → 값 기반 equals/hashCode |
| 사용 예시 | 이메일, 금액, 주소, 전화번호 등 |
| 추천 구현 | final class, record, 생성자 검증, equals/hashCode 구현 |
DTO vs VO
| 항목 | DTO (Data Transfer Object) | VO (Value Object) |
|---|
| 목적 | 계층/시스템 간 데이터 전달 | 값 자체의 의미 표현 및 불변성 유지 |
| 동일성 기준 | 동일성 없음 (그저 데이터 컨테이너) | 필드 값(value) 기반 equals()/hashCode() |
| 불변성 | 선택 사항 (보통 가변) | 필수 (불변 객체로 설계) |
| 포함 정보 | 순수 데이터 (getter/setter만) | 도메인에서 의미 있는 값 + 간단한 로직 |
| 비즈니스 로직 | 없음 (비즈니스 로직 금지) | 간단한 유효성 검증/계산 포함 가능 |
| 캡슐화 | 주로 단순 구조 (필드 노출) | 내부 필드 보호, 유효성 검사 포함 |
| 사용 위치 | Controller ↔ Service ↔ Repository 사이 | 도메인 모델 내부 (Entity 필드 등) |
| 직렬화 목적 | 직렬화 대상 (JSON/XML) | 보통 사용하지 않음 |
| JPA 사용 | Entity로 직접 매핑하지 않음 | @Embeddable로 내장 사용 가능 |
| Setter 제공 | 보통 있음 (가변 구조) | 없음 (불변 원칙) |
| 적합한 구현 방식 | 일반 class, 가끔 record | final class 또는 record |
VO 신분증처럼 값 자체가 의미 있고 고유한 구조체 (이메일, 돈, 주소 등)
DTO 택배 상자처럼 데이터를 옮기기 위한 포장재 (값을 담기만 함)