우리는 데이터를 표현하기 위해서 Entity, VO, DTO라는 녀석들을 사용한다. 기본적인 작성 방법이 비슷해서 대충 섞어서 잘못 사용하는 경향이 있는데 이들의 정확한 차이점에 대해서 살펴보자.
Entity vs VO vs DTO
Entity
Entity는 실제 데이터베이스의 테이블과 매핑되어 영속성을 가지는 데이터를 표현하기 위해 사용된다.
- Entity는 테이블의 기본키(Primary Key)에 해당하는 고유한 식별자(ID)를 가지며 이를 기준으로 동일성을 판단한다. 따라서 Entity 객체 하나가 테이블의 한 행(row)에 대응 된다.
- Entity는 식별자는 불변이지만 시간에 따라 상태가 변할 수 있는 가변성을 갖는다.
- Entity는 필요 시 비지니스 로직을 포함할 수 있으며 데이터와 관련된 로직을 내부적으로 처리할 수 있다.
일반적으로 DB <--> DAO(Repository) 간의 데이터 전달에 사용될 수 있다. (DAO <-- --> Service 간에도 사용 되기도 한다.)
Entity는 주로 JPA에서 사용하는 개념이다.
@Entity
@Data
public class Board {
@Id // 식별자 필요
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer bno;
private String title;
private String content;
private String writer;
}
VO
VO(Value Object)는 값 자체로써의 객체를 표현하기 위해 사용되며 주로 도메인의 개념을 표현하는데 사용된다.
- VO는 Entity와 달리 데이터베이스와 연관 없을 수도 있으며 관련이 있을 때 여러개의 VO가 테이블의 한 행(row)에 대응될 수 있다. VO는 생성되는 시점의 값을 스냅샷으로 사진찍어서 사용하는 형태로 식별자의 의미가 없다.
- VO는 값 자체를 나타내기 때문에 가장 큰 특징으로 불변성(Immutable) 특성을 가진다. 따라서 setter가 없다.
- VO는 동일성(==)이 아닌 동등성(equals)을 기준으로 비교된다. 즉 VO의 모든 속성 값이 같다면 두 VO는 동등하다고 간주된다.
- VO는 Entity와 마찬가지로 데이터와 관련된 비지니스 로직을 포함할 수 있다.
예를 들어 BoardVO는 P.K에 해당하는 bno가 있지만 이것이 객체를 구분하는 값은 아니다.
@Getter // setter는 제공하지 않는다.
@EqualsAndHashCode // ==이 아니라 equals로 객체를 구분한다.
public class BoardVO {
private Integer bno; // 값이 있지만 구분자는 아니다.
private String title;
private String content;
private String writer;
}
또 다른 예로 오픈API로 현재의 환율 정보를 가져왔다면 이것은 DB와 상관 없는 값이므로 Entity는 아니고 현재 시점의 환율을 변경하지 않고 써야하므로 VO로 작성하는 것이 적합하다.
DTO
DTO(Data Transfer Object)는 서로 다른 계층(레이어) 간에 데이터를 교환하기 위해 사용되는 객체이다.
- DTO는 같은 도메인이라도 필요에 따라(클라이언트-> 서버, 서비스 계층->프레젠테이션 계층) 다양하게 만들어질 수 있다. 이를 통해 계층간 결합도를 낮추고 시스템 유지보수성을 향상시킨다.
- DTO은 특정 작업에 최적화된 형태로 데이터를 전달하기 위해 다양한 형태로 변형될 수 있다. 즉 필요한 데이터만 선별해서 전송함으로써 효율성이 높아지고 불필요한 데이터가 노출될 위험이 낮아진다.
- DTO는 단순히 값을 전달하는 목적으로 생성하기 때문에 비지니스 로직을 포함하지는 않는다. 다만 데이터 변환 로직은 포함하기도 한다.
예를 들어 Entity의 입장에서 Board는 no, title, content, writer(로그인한사람)가 있을 수 있는데 작성된 글을 서버에서 받을 때는 title, content만 필요할 수 있다.(no는 자동생성, writer는 세션값) 이때의 BoardDtoToInsert(title, content) 형태면 충분하다. 목록을 화면에 뿌려줄 때는 BoardDtoToList(no, title, writer)가 필요하고 상세보기를 위해서는 BoardDtoToDetail(no, title, writer, content)가 필요하다. 이처럼 DTO는 상황에 따라 다양하게 만들어질 수 있다.
@Data // getter/setter 모두 필요
public class BoardDtoToInsert {
private String title;
private String content;
}
@Data
public class BoardDtoToList {
private Integer bno;
private String title;
private String writer;
}
@Data
public class BoardDtoToDetail {
private Integer bno;
private String title;
private String content;
private String writer;
}
record 클래스
record는 JDK 14에서 처음 소개되었는데 16에서 정식으로 사용할 수 있게 되었다. record 클래스는 불변성의 데이터를 갖는 객체를 만들기 위해서 사용된다. 이 말인 즉 VO 작성에 아주 적합한 클래스라는 이야기이다. 그리고 DTO도 값을 변경하는 일이 거의 없기 때문에 사용하는데 문제는 없다.
이전에는 이런 목적을 위해서 final 키워드를 사용했다. 그런데 blank final을 초기화 하기 위해 생성자에서 초기화가 필요하고 getter가 필수적이다.
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
하지만 record가 적용되면 아래와 같이 간단히 처리할 수 있다. record 클래스는 심지어 toString, hashCode, equals 메서드까지 내부적으로 override가 완료된 상태이다. 물론 필요하다면 메서드 재정의나 비지니스 로직의 추가도 가능하다.
public record Person(String name, int age) {
@Override
public String toString(){
return String.format("이름은 %s, 나이는 %d%n", name, age);
}
}
요약비교
항목 |
Entity |
VO |
DTO |
정의 |
시스템에서 식별이 필요한 객체 |
값 자체가 의미가 있는 객체 |
계층 간 데이터 교환을 위한 객체 |
목적 |
도메인의 핵심 개념을 표현 |
도메인에서 값의 개념을 표현 |
계층 간 데이터 전송 최적화 |
사용 |
식별자를 통한 지속적인 상태 관리 |
값의 불변성을 통한 도메인 규칙 표현 |
계층 간 데이터 전송 및 통신 효율성 증대 |
특징 |
식별자(ID)를 가지며 다른 속성은 변경 가능 |
불변성을 가지며 동등성 기반 비교 |
계층 간의 결합도 감소 및 유지 보수성 향상 |