[Spring] DTO vs VO vs Entity

Spring에서 데이터를 다루는 객체인 DTO, VO, Entity의 차이점에 대해 알아보자.


DTO vs VO vs Entity

Entity는 DB와 매핑되는 핵심 객체, DTO는 데이터 전달용 객체, VO는 값 자체에 의미가 있는 불변 객체 Entity는 저장용, DTO는 전달용, VO는 표현용 객체

  • DTO : Data Transfer Object ➡️ 데이터 전달용 객체 (계층 간, 네트워크 등)

    • 🚐 손님에게 배달될 포장된 도시락
      • 음식점 → 배달기사 → 고객까지 전달하는 용도
        • 💡 클라이언트와 데이터 주고받는 운반용 객체! 💡
      • 메뉴명, 수량, 요청사항 등 담겨 있고, 전달 중에 수정될 수도 있음 (상황에 따라 포장을 다르게 담을 수 있다.)
    • 외부에 노출되는 API요청이나 응답은 Entity가 아닌 DTO를 통해 전달함으로써 보안성과 유연성을 확보

    🔴 DTO는 데이터를 “옮기는 상자”

  • VO : Value Object ➡️ 값을 표현하는 객체 (의미 있는 불변 값)

    • 🍱 도시락 자체
      • 만들어지면 바꿀 수 없음 (불변)
      • 메뉴가 같으면 같은 도시락 취급
      • 값 자체가 의미 있음 - 예: 좌표, 돈, 날짜, 주소 등

    🔴 VO는 의미 있는 값을 담은 “정체성 있는 객체”

    “무엇을 나타내는 값인지”가 중요한 객체

  • Entity ➡️ 실제 DB 테이블과 연결된 핵심 객체

    • 🍱 도시락 안의 구성 요소 (밥, 반찬, 소스 등)
      • 소중하기 때문에 주방 안에서만 써야 함
    • Entity는 VO를 포함할 수 있음
    • DB와 직접 연결된 객체이기 때문에 식별자가 존재하며 상태가 바뀔 수 있음

    🔴 Entity는 “누구인지”를 식별할 수 있는 객체

📦 DTO vs VO

구분DTOVO
목적데이터 전달값 표현
가변성가변(mutable)불변(immutable)
equals/hashCode 기준주소 (기본)값 기준으로 재정의
주 사용 위치Controller ↔ Service ↔ API도메인 내부, 로직 내 값 처리
예시회원 요청 객체, 응답 DTO 등Money, Address, Coordinate 등
생성 시언제든 생성 가능생성 후에는 값 변경 ❌

📦 Entity vs VO

항목EntityVO
의미DB 테이블과 1:1 매핑되는 객체의미 있는 작은 값 단위 객체
식별자 (ID)있음 (PK, 고유값)없음 (값 자체로 구별)
불변성보통 가변보통 불변 (final)
관리 위치DB와 연결되는 핵심 모델Entity 안의 필드나 계산용 값

📦 Entity와 DTO로 분리해야하는 이유

굳이 클래스를 2개로 나누지 않고 그냥 Entity 하나로 다 처리하면 안 되는 이유!

  • 구체적인 이유 5가지
    1. 보안
      • Entity에는 민감한 필드(비밀번호 등)가 있을 수 있음 ➡️ 그대로 외부에 노출하면 위험
    2. 유연성
      • API 요청/응답마다 필요한 필드가 다름 ➡️ DTO로 맞춤 설계 가능
    3. 엔티티 보호
      • DTO로 외부와 통신 ➡️ Entity는 내부에서만 안전하게 관리
    4. 유효성 검사 분리
      • @Valid, @NotNull등 검증 로직은 DTO에만 적용

        Entity는 DB와 연결된 순수한 모델이여야 함으로 비즈니스 룰, 요청 유효성 검증 같은 책임이 없어야 한다.

        JPA의 역할은 저장, 조회인데 검증 로직이 섞이면 책임이 뒤엉킴 (SRP(Single Responsibility Principle) 위반)

    5. 레이어 분리 원칙
      • Controller ↔ Service ↔ Repository 역할 구분이 명확해짐

        “Controller” : 클라이언트와 통신 (DTO 입출력) “Service” : 비즈니스 로직 (DTO 🔁 Entity 변환, 로직 처리) “Repository” : DB 접근 (Entity 전용)

        DTO와 Entity를 나누지 않고 Controller, Service에서 Entity를 직접 다루면

        • 한 객체가 너무 많은 계층을 넘나듬 (의존성 얽힘)
        • 책임이 명확하지 않음 (수정 시 어디를 고쳐야 할지 모름)
        • 보안 이슈 발생 가능 (불필요한 필드 노출)

코드 예제

✅ DTO (값 전달용, 가변 객체)

  •   public class MemberDTO {
          private String name;
          private int age;
    
          // 생성자
          public MemberDTO(String name, int age) {
              this.name = name;
              this.age = age;
          }
    
          // getter & setter (값 변경 가능!)
          public String getName() { return name; }
          public void setName(String name) { this.name = name; }
    
          public int getAge() { return age; }
          public void setAge(int age) { this.age = age; }
      }
    
  • setName(), setAge()처럼 값은 변경 가능
  • 주로 Controller 🔁 Service 🔁 Client 간 데이터 전달용

✅ VO (값 표현용, 불변 객체)

  •   public class Money {
          private final int amount;
    
          public Money(int amount) {
              this.amount = amount;
          }
    
          public int getAmount() { return amount; }
    
          // 값 기반 equals, hashCode
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (!(o instanceof Money)) return false;
              Money money = (Money) o;
              return amount == money.amount;
          }
    
          @Override
          public int hashCode() {
              return Integer.hashCode(amount);
          }
      }
    
  • 필드가 final, setter없음 → 불변 객체
  • equals() 재정의 → 값이 같으면 같은 객체로 간주
  • 주로 비즈니스 로직 내부에서 의미 있는 값 표현용

✅ Entity

  • @Entity
    public class Member {
        @Id @GeneratedValue
        private Long id;
    
        private String name;
        private String password;  // 노출되면 안 되는 정보
    }
    

💬 면접 시 설명 예시

“DTO는 계층 간 데이터를 전달할 때 사용하는 객체로, 보통 가변이고 네트워크나 컨트롤러에 노출됩니다. 반면 VO는 불변 객체로, 값 자체가 의미를 가지며 equals와 hashCode를 통해 같은 값을 같다고 간주해 도메인 모델 내에서 활용됩니다. Entity는 DB와 직접 연결된 객체로 식별자가 존재하며 상태가 바뀔 수 있습니다. 외부에 노출되는 API 요청이나 응답은 Entity가 아닌 DTO를 통해 전달함으로써 보안성과 유연성을 확보할 수 있습니다.”


🚀 Java 17부터는 record로 VO를 만들기 더 쉬워짐!

public record Coordinate(int x, int y) {}
  • final, 불변성, equals/hashCode 자동 구현!
  • 값 객체(VO)를 표현할 때 record는 아주 강력한 도구