안녕하세요, 503입니다.
저번 주부터 발표 스터디를 시작했는데요. 이번 주의 주제는 바로 DTO와 VO의 개념과 차이점에 대해 포스팅하려고 합니다.
필자는 항상 프로젝트를 시작할 때마다 어떻게 하면 더 좋고 깔끔한 코딩을 할 수 있을지 고민했던 것 같습니다. 그 중 계층 간 역할 분리를 위해 아키텍처를 어떻게 짤지 고민하게 되고, 그러면서 DTO, VO, DAO, Entity, Domain, repository.... 등등 많고 새로운 개념들을 접하게 됐습니다. 잘 구분해서 사용하려고는 하지만, 사실 정확한 개념을 설명하려고 하면 자신있게 말할 수 없었습니다. 그래서 이번 포스팅에서는 대체 이것들은 무엇이고 언제쓰이는지, 또 차이점은 무엇인지에 대해 정리해보겠습니다.
DTO(Data Transfer Object)는 데이터 전송 객체라는 의미를 가지고 있습니다.
'계층 간' 데이터 교환을 위한 객체(java beans)으로 데이터를 담아 전달하는 바구니라고 생각하면 됩니다.
getter/setter
메서드 만을 갖음. 보내는 쪽에서 setter
을 사용해 데이터를 DTO에 담아보내고 받는 쪽에서 getter
을 사용해 데이터를 꺼냄이름과 나이 필드를 가진 PersonDto
를 정의해보았습니다.
public class PersonDto {
private String name;
private int age;
public String getName() {
return name;
}
public String setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public String setAge(String age) {
this.age = age;
}
}
데이터를 보내주는 Service 단에서 PersonDto
객체를 생성하고 setter
를 사용해 데이터 값을 넣어줍니다.
public PersonDto createNewPerson(){
String name = "오백삼";
int age = 53;
PersonDto personDto = new PersonDto();
personDto.setName(name);
personDto.setAge(age);
return personDto;
}
데이터를 받는 Controller 단에서 PersonDto
객체를 받환받아 getter
을 사용해 전달받은 데이터 값을 꺼내 사용합니다.
public String createNewPerson(){
PersonDto person = PersonService.createNewPerson();
String name = person.getName();
int age = person.getAge();
return name + age;
}
위처럼 setter
메소드를 가질 경우, 객체의 정보를 변경할 수 있기 때문에 데이터가 가변적입니다. 이러한 가변객체를 불변객체로 바꾸고 싶다면 아래와 같이 setter 메소드를 지우고, 생성자(constructor)를 통해 속성 값들을 초기화하도록 수정합니다.
public class PersonDto {
private String name;
private int age;
public PersonDto(String name, int age) { // 생성자
this.name = name;
this.age = age;
}
// getter
public String getName() {
return name;
}
public String getAge() {
return age;
}
}
이렇게 하면 오직 생성자를 통해서만 속성 값들을 초기화할 수 있기 때문에, 전달과정 중에 데이터가 변조되지 않음을 보장할 수 있습니다.
DTO는 위처럼 service와 controller 같은 계층간 데이터 교환을 위해 사용하며, 한 화면에서 보여지는 데이터 값들을 객체화시키고 어떤 타입의 데이터를 받아올 것인지 정의하기 위해서도 사용됩니다. (ex. QueryParam DTO)
Entity(엔티티)는 실제 DB 테이블과 매핑(mapping)되는 객체입니다.
데이터베이스의 테이블에 존재하는 컬럼들을 필드로 가지는 객체로 DB 테이블과 1:1로 매핑되며 테이블이 가지지 않는 컬럼을 필드로 가져서는 안됩니다.
Entity 클래스는 데이터베이스와 매핑되어 있는 핵심 클래스로 데이터베이스의 영속성(persistent)의 목적으로 사용하기 떄문에, 요청이나 응답 값을 전달하는 클래스로 사용하는 것에는 맞지 않습니다.
(*영속성(persistence) : 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성을 의미)
Entity를 기준으로 테이블이 생성되고 스키마가 변경됩니다. 또한, 많은 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작하는데, 뷰가 변경될 때마다 Entity 클래스가 변경되면 여러 클래스에 영향을 주게 될 것입니다.
그렇기에 요청이나 응답 값을 전달하는 클래스로는 다른 클래스에 영향을 끼치지 않고 자유롭게 변경이 가능한 DTO
를 사용하는 것이 좋습니다.
VO(Value Oject)는 값 그 자체를 표현하는 객체입니다. 객체의 정보가 변경되지 않는 '불변성'을 보장합니다.
VO 내부에 선언된 속성(필드)의 모든 값들이 VO 객체마다 값이 같아야 똑같은 객체라고 판별함.
서로 다른 이름을 갖는 VO 인스턴스라도 모든 속성 값이 같다면 두 인스턴스는 같은 객체로 인식
VO에서 객체를 속성 값만으로 비교하도록 Object 클래스의 equals()
와 hashcode()
를 오버라이딩해 구현
getter
와 setter
을 가질 수 있으며, 그 외의 로직도 가질 수 있음(테이블 내 있는 속성 외 추가적인 속성을 가질 수 있음)
대표적인 예시로는 돈이 있는데요. 여러 만원짜리의 지폐의 고유번호가 달라도 같은 만원으로 보는 것처럼 "값" 자체로만 비교를 합니다.
public class Money{
private final int value;
public Money(int value) { // 생성자
this.value = value;
}
public int getHalfValue(){ // setter/getter 외의 로직 추가 가능
return value/2;
}
// equals()와 hashcode()를 오버라이딩
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(!(o instanceof Money)) retrun false;
Money money = (Money) o;
return value == money.value;
}
@Override
public boolean hashCode() {
return Objects.hash(value);
}
}
둘 다 데이터를 담고있는 객체지만 아래와 같은 차이점이 있습니다.
예시를 들자면, 웹 화면에서 로그인하는 정보는 DTO
로 처리하고, 테이블과 관련된 데이터는 VO
로 처리합니다.
DTO와 VO는 데이터를 담고, 다른 계층 또는 다른 컴포넌트들로 데이터를 넘겨주기 위한 자료구조(Data Structure)이기에 어떠한 기능 및 동작도 없어야 합니다.
반면에 Entity는 핵심 비지니스 로직을 담는 비지니스 도메인의 영역의 일부입니다. 그러므로 Entity 또는 domain 객체에는 비지니스 로직이 추가될 수 있습니다. 따라서 다른 계층이나 컴포넌트들 사이에서 데이터 전달을 위한 객체로 사용하지 않습니다.
예시를 들어보겠습니다.
동일한 속성을 가진 두 개의 Entity가 있습니다.
- Entity는 id로 구별합니다. 따라서 이 두 객체는 두 개의 다른 id를 가지고 있기 때문에 동일하지 않다고 봅니다.
- 그러나 동일한 속성을 가진 두 개의 VO는 같은 객체입니다.
하나의 Entity가 있습니다.
- Entity는 존재하는 동안 속성들이 계속 바뀔 수 있지만, 속성이 바뀌어도 같은 Entity입니다.
- 그러나 VO는 값이 바뀌면 다른 객체가 됩니다.
ex. 돈이라는 객체가 있고, 얼마를 사용해서 거스름돈을 받았습니다. 그러면거스름돈 != 처음 갖고있던 돈
이기 때문에처음 갖고있던
돈과사용한 후
의 돈은 다른 값의 객체가 됩니다..
Spring에 JPA를 쓰는 경우에는 Entity라고 표현하고, MyBatis를 쓰는 경우에는 주로 VO(ValueObject)라고 표현합니다.
그 이유는 JPA는 ORM
이고 MyBatis는 SQL-Mapper
이기 때문입니다. ORM은 SQL문이 아닌 RDB 객체를 자바 객체로 매핑하며 객체간 관계나 식별자를 가질 수 있습니다. 반면에 SQL-Mapper는 SQL문으로 RDB에 접근하고 데이터를 객체로 매핑하며 객체간 관계나 식별자는 가질 수 없습니다.
때문에 JPA에서는 식별자를 가지는 Entity
, MyBatis에서는 값 객체를 의미하는 VO
라는 명칭을 사용합니다.
여기까지 읽었다면 DTO와 VO, Entity의 차이점을 알 수 있을 것입니다.
실제로 프로젝트(Spring MyBatis기준)를 진행할 때 어떤식으로 적용하는지 계층화 아키텍쳐로 정리해보면 아래와 같습니다.
Controller, Service단에서 데이터를 교환할 때 DTO
를 사용하고, DB와 매핑하기 위해서 Entity
클래스를 사용합니다.
계층을 Presentation, Business, Pesistence 3계층으로 나눈 것을 볼 수 있는데요. 어떤 계층이고 각 계층간 무슨 역할을 하는지 등은 다음 포스팅에서 계층화 아키텍쳐(Layered Architecture)를 소개하며 알아보겠습니다.
계층(Layer) 간의 데이터 교환, 매핑을 위해 사용하는 객체들이 DTO와 Entity입니다.
DTO와 VO는 데이터를 넘겨주기 위한 자료구조로 계층 간 데이터 전달을 위해 사용됩니다.
그러나 Entity는 핵심 비즈니스 로직을 담는 비지니스 도메인 영역의 일부로 Entity 및 Domain 객체에는 비즈니스 로직이 추가될 수 있으며 계층 간 데이터 전달용으로 사용하지 않습니다.
안녕하세요, 잘 읽었습니다. 중간에 제목이 잘못되어있는거 같아 공유드려요~
Spring의 MyBatis(Entity) vs JPA(VO)
-> Spring의 MyBatis(VO) vs JPA(Entity)