Pink Spider/Vo와 Dto

Created Sun, 30 Mar 2025 17:14:05 +0900 Modified Mon, 08 Dec 2025 08:41:47 +0900
1474 Words 7 min

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 조합 도메인 모델은 깔끔하게 유지하면서도 무결성 보장