Spring을 포함한 Java 기반 애플리케이션에서 **VO(Value Object)**와 **DTO(Data Transfer Object)**는 개념적으로 비슷해 보일 수 있지만, 그 목적과 사용하는 맥락에서 차이가 있습니다. 아래에 각각을 설명하고 차이를 정리할게요.
1. VO (Value Object)
- 의미: 값 자체를 표현하는 객체. 값의 동등성을 비교할 때, 객체의 참조가 아니라 값이 같은지로 판단합니다.
- 특징:
- **불변(Immutable)**하게 설계하는 것이 일반적입니다.
equals(),hashCode()를 재정의해서 값이 같으면 동일한 객체로 간주합니다.- 주로 도메인 모델에서 사용되며, 특정 개념(예: 주소, 돈, 이름 등)을 캡슐화합니다.
public class Address {
private final String city;
private final String street;
public Address(String city, String street) {
this.city = city;
this.street = street;
}
// equals, hashCode 반드시 재정의
}
2. DTO (Data Transfer Object)
- 의미: 계층 간 데이터 전달을 위해 사용하는 객체 (예: Controller → Service → Repository)
- 특징:
- 불변일 필요 없음, Getter/Setter 있음
- 단순히 데이터를 옮기는 목적 (로직이 없어야 함)
- 보통 입력(Request), **출력(Response)**용으로 많이 쓰임
public class UserResponseDto {
private String name;
private String email;
// 기본 생성자, getter/setter
}
정리: VO vs DTO
| 항목 | VO (Value Object) | DTO (Data Transfer Object) |
|---|---|---|
| 목적 | 값 자체를 표현 | 데이터 전달 |
| 불변성 | 보통 불변 | 보통 가변 |
| 사용 위치 | 도메인 모델 | 계층 간 데이터 전달 |
| equals/hashCode | 값 기준으로 재정의 | 일반적으로 재정의하지 않음 |
| 로직 포함 | 가능 | 없음 (순수 데이터만) |
사용 예시
간단한 Spring 기반 애플리케이션 구조에서 VO와 DTO의 차이를 실제 예제 코드입니다. 예시는 “사용자(User) 정보 처리”를 중심으로 구성합니다.
예제 시나리오: 회원가입 & 사용자 정보 조회
- **사용자 정보 (User)**를 DB에 저장하고,
- 클라이언트에서 입력한 정보를 기반으로 회원가입하며,
- 사용자 정보를 조회할 수 있게 합니다.
1. VO 예제: Email, Name 등을 값 객체로 설계
// Email.java (VO)
public class Email {
private final String value;
public Email(String value) {
if (!value.matches("^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$")) {
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 Objects.hash(value);
}
}
2. DTO 예제: 회원가입 요청/응답에 사용
// UserSignupRequestDto.java
public class UserSignupRequestDto {
private String email;
private String name;
public String getEmail() {
return email;
}
public String getName() {
return name;
}
}
// UserResponseDto.java
public class UserResponseDto {
private String email;
private String name;
public UserResponseDto(String email, String name) {
this.email = email;
this.name = name;
}
public String getEmail() {
return email;
}
public String getName() {
return name;
}
}
3. Entity 예제: VO 사용해서 불변성 강화
// User.java (Entity)
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@Embedded
private Email email;
private String name;
protected User() {}
public User(Email email, String name) {
this.email = email;
this.name = name;
}
public Email getEmail() {
return email;
}
public String getName() {
return name;
}
}
4. Controller/Service 사용 예
// UserService.java
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void signup(UserSignupRequestDto dto) {
Email email = new Email(dto.getEmail()); // VO 사용
User user = new User(email, dto.getName());
userRepository.save(user);
}
public UserResponseDto getUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
return new UserResponseDto(user.getEmail().getValue(), user.getName());
}
}
요약
- VO (Email): 데이터 무결성을 보장, equals/hashCode 재정의 → 값이 같으면 같다고 간주
- DTO (UserSignupRequestDto, UserResponseDto): 계층 간 데이터 전달, 입력/출력 전용
- Entity (User): VO를 내부에서 사용해 불변성 강화
이런 식으로 설계하면 도메인 모델이 더 견고해지고, 외부로부터 받는 데이터와 내부에서 사용하는 데이터의 역할이 분리되어 유지보수가 쉬워져.
Record나 Lombok을 이용한 간단한 버전
좋아! 그럼 위에서 보여준 VO와 DTO 예제를 record와 Lombok으로 각각 간단하게 리팩토링 해보겠습니다..
1. VO: Java 16+의 record 사용
값 객체(VO)는 불변이고, equals/hashCode를 잘 구현해야 하므로 record와 아주 잘 맞아.
// Email.java
public record Email(String value) {
public Email {
if (!value.matches("^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$")) {
throw new IllegalArgumentException("Invalid email format");
}
}
}
record는 생성자,equals(),hashCode(),toString()모두 자동 생성.- 불변이라 VO로 사용하기 적합.
2. DTO: Lombok으로 간단하게
DTO는 보통 Getter/Setter, 기본 생성자만 필요하므로 Lombok으로.
// UserSignupRequestDto.java
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
@Getter
@Setter
@NoArgsConstructor
public class UserSignupRequestDto {
private String email;
private String name;
}
// UserResponseDto.java
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class UserResponseDto {
private String email;
private String name;
}
Entity에서도 Lombok 활용
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id @GeneratedValue
private Long id;
@Embedded
private Email email;
private String name;
public User(Email email, String name) {
this.email = email;
this.name = name;
}
}
요약
| 목적 | 기술 | 장점 |
|---|---|---|
| VO | record |
코드 간결 + 불변성 유지 + equals/hashCode 자동 |
| DTO | Lombok (@Getter, @Setter, @NoArgsConstructor, @AllArgsConstructor) |
반복 코드 제거, 깔끔한 구조 |
| Entity | Lombok + VO 조합 | 도메인 모델은 깔끔하게 유지하면서도 무결성 보장 |