DTO와 VO의 개념은 Java와 Spring 애플리케이션에서 자주 혼동되는 부분이다. 나 또한 두 객체의 차이점을 구별하기 힘들어 이 글을 작성하면서 공부해보려고 한다. 여러 자료를 찾아보니 나처럼 헷갈려하는 사람들이 많더라..
더 이상 헷갈려하지 않기 위해 DTO(Data Transfer Object)와 VO(Value Object)의 차이를 명확히 구분하고, 각각이 어떻게 사용되는지에 대해 정리해보려고 한다.
DTO는 계층 간 데이터를 전달하기 위한 객체
Spring 애플리케이션에서 주로 Controller ↔ Service ↔ Repository 간에 데이터를 주고받을 때 사용
@RestController에서 API 응답으로 활용하거나, @RequestBody로 API 요청을 처리하는 데 사용됨.public class UserDTO {
private String name;
private int age;
public UserDTO(String name, int age) {
this.name = name;
this.age = age;
}
// Getter
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<UserDTO> createUser(@RequestBody UserDTO userDTO) {
// DTO를 받아서 처리 후, 다시 DTO로 응답
return ResponseEntity.ok(userDTO);
}
}
DTO는 HTTP 요청을 처리할 때
@RequestBody로 요청 데이터를 받거나,@ResponseBody로 JSON 응답을 보낼 때 사용된다.
DTO를 불변 객체로 설계하면, 객체가 생성된 후 데이터 변경을 방지할 수 있어 데이터의 무결성을 보장한다.
final 필드와 생성자를 사용하여 불변 객체를 선언할 수 있다.
public class UserDTO {
private final String name;
private final int age;
public UserDTO(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
VO는 값 객체로, 값에 의미를 두고 불변(Immutable)하도록 설계
Spring에서 VO는 주로 비즈니스 로직에서 값을 표현하는 객체로 사용
@Embeddable로 다른 엔티티에 포함되거나, @Value 어노테이션으로 다룰 수 있음.@Embeddable
public class Address {
private final String city;
private final String street;
protected Address() {} // JPA에서 VO를 사용할 때 기본 생성자 필요
public Address(String city, String street) {
this.city = city;
this.street = street;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Address address = (Address) obj;
return Objects.equals(city, address.city) &&
Objects.equals(street, address.street);
}
@Override
public int hashCode() {
return Objects.hash(city, street);
}
}
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address address; // VO 사용
public User(String name, Address address) {
this.name = name;
this.address = address;
}
}
VO는 비즈니스 로직에서 값 객체를 표현하는 데 사용되며, 값이 동일하면 동일한 객체로 취급된다.
Address, Money 등)| 구분 | DTO (Data Transfer Object) | VO (Value Object) |
|---|---|---|
| 주요 목적 | 계층 간 데이터 전달 | 값 객체 표현 |
| 불변성 | 가변/불변 가능 | 불변 (Immutable) |
| 동등성 판단 | 객체 참조 비교 (ID 기준) | 값 비교 (equals()와 hashCode() 기준) |
| 식별자 | 없음 (주로 전달용) | 없음 (값 자체가 중요) |
| Spring에서 사용 | 요청/응답 데이터 처리 | 엔티티의 일부로 사용 (값 객체) |
| 저장 여부 | 데이터베이스 저장 안 함 | 엔티티 내부에서 사용, DB에 저장되지 않음 |
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@RequestBody OrderDTO orderDTO) {
// OrderDTO를 받아서 주문 생성 로직 처리 후 응답
return ResponseEntity.ok(orderDTO);
}
}
OrderDTO는 API 요청/응답을 처리하는 데 사용된다.@RequestBody로 요청을 받고,@ResponseBody로 응답을 보낸다.
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private Money totalPrice; // VO 사용
public Order(Money totalPrice) {
this.totalPrice = totalPrice;
}
}
@Embeddable
public class Money {
private final int amount;
public Money(int amount) {
this.amount = amount;
}
public int getAmount() {
return amount;
}
}
MoneyVO는Order엔티티의 일부로@Embedded어노테이션을 통해 값 객체로 사용되며, 엔티티의 비즈니스 로직을 깔끔하게 처리한다.
| 사용처 | DTO | VO |
|---|---|---|
| Controller ↔ Service | ✅ 사용 (요청/응답) | ❌ 사용 안 함 |
| Service ↔ Repository | ✅ 사용 가능 | ❌ 잘 사용 안 함 |
| 도메인 모델 (Entity 내부) | ❌ 사용 안 함 | ✅ 사용 (값 객체) |
| API 응답용 데이터 | ✅ 사용 | ❌ 사용 안 함 |
DTO는 HTTP 요청/응답 처리와 계층 간 데이터 전달을 위한 객체로, 가변일 수 있지만 불변으로 설계하는 것이 좋다.
@RequestBody나 @ResponseBody와 함께 JSON 형식으로 데이터를 주고받을 때 유용하다.VO는 불변 객체로 설계되어 값을 표현하는 데 사용되며, 동일한 값이면 동일한 객체로 취급한다.
Address나 Money와 같은 값 객체는 불변해야 하며, 값이 동일하면 동일한 객체로 취급된다.@Embeddable 어노테이션을 통해 엔티티에 포함될 수 있으며, 값 객체가 변경되지 않도록 보장된다. 또한, 값을 비교하는 equals()와 hashCode() 메서드를 통해 동등성을 판단한다.DTO는 계층 간 데이터 전송 용도, VO는 도메인 모델의 값 표현 용도