[Spring] DTO vs VO vs Entity
Spring에서 데이터를 다루는 객체인 DTO, VO, Entity의 차이점에 대해 알아보자.
- DTO vs VO vs Entity
- 📦 DTO vs VO
- 📦 Entity vs VO
- 📦 Entity와 DTO로 분리해야하는 이유
- 코드 예제
- 💬 면접 시 설명 예시
- 🚀 Java 17부터는
record
로 VO를 만들기 더 쉬워짐!
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
구분 | DTO | VO |
---|---|---|
목적 | 데이터 전달 | 값 표현 |
가변성 | 가변(mutable) | 불변(immutable) |
equals/hashCode 기준 | 주소 (기본) | 값 기준으로 재정의 |
주 사용 위치 | Controller ↔ Service ↔ API | 도메인 내부, 로직 내 값 처리 |
예시 | 회원 요청 객체, 응답 DTO 등 | Money, Address, Coordinate 등 |
생성 시 | 언제든 생성 가능 | 생성 후에는 값 변경 ❌ |
📦 Entity vs VO
항목 | Entity | VO |
---|---|---|
의미 | DB 테이블과 1:1 매핑되는 객체 | 의미 있는 작은 값 단위 객체 |
식별자 (ID) | 있음 (PK, 고유값) | 없음 (값 자체로 구별) |
불변성 | 보통 가변 | 보통 불변 (final ) |
관리 위치 | DB와 연결되는 핵심 모델 | Entity 안의 필드나 계산용 값 |
📦 Entity와 DTO로 분리해야하는 이유
굳이 클래스를 2개로 나누지 않고 그냥 Entity 하나로 다 처리하면 안 되는 이유!
- 구체적인 이유 5가지
- 보안
- Entity에는 민감한 필드(비밀번호 등)가 있을 수 있음 ➡️ 그대로 외부에 노출하면 위험
- 유연성
- API 요청/응답마다 필요한 필드가 다름 ➡️ DTO로 맞춤 설계 가능
- 엔티티 보호
- DTO로 외부와 통신 ➡️ Entity는 내부에서만 안전하게 관리
- 유효성 검사 분리
@Valid
,@NotNull
등 검증 로직은 DTO에만 적용Entity는 DB와 연결된 순수한 모델이여야 함으로 비즈니스 룰, 요청 유효성 검증 같은 책임이 없어야 한다.
JPA의 역할은 저장, 조회인데 검증 로직이 섞이면 책임이 뒤엉킴 (SRP(Single Responsibility Principle) 위반)
- 레이어 분리 원칙
- Controller ↔ Service ↔ Repository 역할 구분이 명확해짐
“Controller” : 클라이언트와 통신 (DTO 입출력) “Service” : 비즈니스 로직 (DTO 🔁 Entity 변환, 로직 처리) “Repository” : DB 접근 (Entity 전용)
DTO와 Entity를 나누지 않고 Controller, Service에서 Entity를 직접 다루면
- 한 객체가 너무 많은 계층을 넘나듬 (의존성 얽힘)
- 책임이 명확하지 않음 (수정 시 어디를 고쳐야 할지 모름)
- 보안 이슈 발생 가능 (불필요한 필드 노출)
- Controller ↔ Service ↔ Repository 역할 구분이 명확해짐
- 보안
코드 예제
✅ 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는 아주 강력한 도구