목차
1. 왜 이 글을 작성하게 되었는가?
2. 들어가기 전에... // 계층에 관하여
3. DTO, VO, Entity의 개념
4. DTO vs VO
5. DTO vs Entity
6. 정리
😲 왜 이 글을 작성하게 되었는가?
토이 프로젝트를 들어가기 앞서 어떤 식으로 프로젝트 구조를 잡을지 고민중이다.
GitHub에서 다른 사람들이 Spring으로 개발한 웹 서비스를 탐방했는데 구조가 아래와 크게 다르지 않았다.
- service 폴더
- domain 폴더: repository와 dto 포함
- web 폴더: controller 포함
그런데 여기서 domain 폴더명이 다 제각각이었다. 😱
데이터를 저장하는 객체인 것은 알겠는데 Entity, Domain, DTO 등 다양한 이름을 존재하였고 이에 대한 차이가 뭘까 궁금해져서 해당 글을 포스팅하게 되었다.
🤩 들어가기 전에...
각 차이점을 이해하기 앞서 Layer, 즉 '계층'에 대해 이해할 필요가 있다.
계층은 크게 4가지로 나눠져있다.
- Presentation Layer (=UI Layer)
- 서비스와 관련된 정보 표시
- 사용자가 직접 액세스할 수 있는 계층 (ex: 웹페이지, OS - GUI, ...) - Application Layer (=Service Layer)
- 특정 application 작업을 수행하는데 필요한 작업을 정의
- 필요한 도메인 작업 위임하고 다른 서비스와 상호 작용 - Business Logic Layer (=Domain Layer)
- 유효성 검사, 계산같은 로직 포함 - Data Access Layer (=Persistence Layer)
- 데이터에 영속성을 부여해주는 계층
(영속성이란? 데이터를 생성한 프로그램이 종료되어도 데이터는 유지되는 특성)
😤 개념 알아보기
DTO; Data Transfer Object
- 계층간 데이터 교환을 위한 객체
- 로직을 가지지 않은 getter/setter 메소드만 갖는 객체
- request와 response용 DTO는 view를 위한 클래스
VO; Value Object
- 특정 비즈니스 값을 담는 객체
- DTO와 유사하나 read only 속성을 가짐
- 모든 속성 값이 같다면 같은 객체임을 의미
(ex: 지폐에는 각 고유번호가 존재한다. 하지만 우리는 만원짜리 두 장을 보면 만원이 2장 있다고 인식하지 고유번호 A인 지폐 하나, 고유번호 B인 지폐 하나로 인식하지 않는다.) - 생성자 외에 setter 속성을 띄는 메소드 선언 금지
🔻 VO 생성하기
VO는 모든 속성 값이 같다면 같은 객체임을 증명할 수 있어야 한다.
이 말을 코드를 통해 살펴보기 위해 아래와 같은 Money 클래스를 생성했다.
public class Money {
private final int price;
public Money(final int price) {
this.price = price;
}
}
Money 클래스에 같은 값을 넣어 생성한 다음 비교해보자.
@Test
void equals() {
Money money1 = new Money(1000);
Money money2 = new Money(1000);
assertThat(money1).isEqualTo(money2);
}
테스트가 실패한 원인은 두 객체의 주소값이 다르기 때문이다.
equals 메소드는 주소값이 다른 객체는 서로 다른 객체로 판단하기 때문에 equals()와 hashcode()를 오버라이드 해야한다.
먼저 equals()를 오버라이드 해보자.
public class Money {
private final int price;
public Money(final int price) {
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money money = (Money)o;
return price == money.price;
}
}
equals()를 재정의한 것만으로도 테스트가 통과되는 것 같다.
그러면 hashcode()를 재정의하라는 이유는 뭘까?
다음과 같은 테스트 코드를 작성해보자.
@Test
void hashcode_test() {
Map<Money, Integer> moneyCnt = new HashMap<>();
moneyCnt.put(new Money(1000), 10);
Money money = new Money(1000);
assertThat(moneyCnt.get(money)).isEqualTo(10);
}
moneyCnt에 put한 Money 객체와 변수명 money인 객체는 값이 둘 다 1000원이다.
위에서 equals()를 오버라이드했으니 두 객체를 비교하면 당연히 같다고 뜰 것이다.
하지만 HashMap의 key값으로 Money 클래스를 이용했을 때에는 Money 클래스의 hashCode()를 이용하게 되므로 재정의가 필수적이다.
public class Money {
private final int price;
public Money(final int price) { ... }
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() {
return Objects.hash(price);
}
}
Entity
- 실제 DB 테이블과 매칭되는 클래스
- 외부에서 getter 메소드를 이용하지 않도록 필요한 로직 구현
- DB 테이블 내에 존재하는 컬럼만을 속성으로 가지는 클래스
- request, response 클래스로 사용 X
🔻 Entity 예제
+ @Entity는 JPA 사용 시 이용하는 어노테이션으로 MyBatis 이용 시 사용하지 않는다.
@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@Column
private String picture;
@Builder
public User(String name, String email, String picture) {
this.name = name;
this.email = email;
this.picture = picture;
}
public User update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
}
👊 DTO vs VO 👊
아직 헷갈릴 수 있지만 둘은 엄연히 다른 개념이다.
위의 개념에서 언급했다시피 둘의 가장 큰 차이점은 값 변경 가능 여부이다.
DTO | VO | |
속성값이 같은 경우 | 다른 객체 | 같은 객체로 취급 |
값의 가변성 | setter가 존재하면 변경 가능 | 불변 |
로직 | getter/setter 외 로직 X | 다양한 로직 추가 가능 |
아래와 같이 기능으로 기억하면 위 특징들에 대해 이해하기 편할 것이다.
- DTO: 데이터 전달용
- VO: 값 표현용
🔻 사람들은 왜 둘을 혼용할까?
많은 추측이 있지만 가장 유력한 후보는 아래 책 때문이다.
초판에서는 데이터 전달용 객체를 VO로 정의했으나 이후에는 TO로 정정했다.
하지만 이미 많은 사람들이 초판을 읽었기 때문에 혼용해서 사용하는 경우가 많다고 추측하고 있다.
👊 DTO vs Entity 👊
Entity를 잘 설계했어도 View 내에서 getter만 이용해 원하는 정보를 표시하기 어려울 수 있다.
이런 경우 Entity 내에서 Presentation을 위한 필드나 로직을 추가해야하는데 이는 모델링의 순수성을 깨고 여러 클래스에 영향을 끼칠 수 있다.
수많은 서비스들이 Entity 클래스를 기준으로 동작하고, 가장 core한 클래스이므로 잦은 변경은 부담이 간다.
이런 상황에서 등장한 것이 DTO로, 도메인 모델을 복사한 형태이나 Presentaion를 위한 필드 등을 추가할 수 있다.
- View Layer와 DB Layer와의 분리를 위해 구분
- Entity: DB 테이블과 매핑되므로 변경 시 여러 클래스에 영향 (DB Layer)
- DTO: View와 통신하며 request, response의 변경이 유동적 (View Layer)
💡 정리
- DTO: 계층간 데이터 전송
- VO: 의미있는 값 표현할 때 사용
- Entity: DB 테이블과 매핑되는 클래스
DTO | VO | Entity | |
값 변경 | O | X | O |
로직 포함 | X | O | O |
다음에는 DAO와 Repository의 차이점에 대해 알아보겠다.
원래 본 글에 모두 담으려고 했으나 설계법에 대한 선행지식이 필요한 것 같아 더 조사해보고 따로 작성하려 한다.
참고
- 우아한 테크코스 - 10분 테크톡: 인비의 DTO vs VO
- 국윤창님 블로그 - DAO, DTO, Service
- matinFowler.com - PresentationDomainDataLayering
- StackExchange - Application layer vs domain layer?
- baeldung - java equals() and hashCode() Contracts
'Develop > Spring+JPA' 카테고리의 다른 글
PATCH 메소드는 언제 사용하는가? (3) | 2022.01.25 |
---|---|
[Spring/MariaDB] 연동 시 자주 발생하는 오류 (0) | 2021.12.26 |
[Thymeleaf] 타임리프란? (+기본적인 사용법) (2) | 2021.10.15 |
[Spring] HTTP 메시지 컨버터 (0) | 2021.10.10 |
[Spring] HTTP 응답 (0) | 2021.08.11 |