본문 바로가기
Develop/Spring+JPA

Entity vs DTO vs VO

by 연로그 2021. 11. 23.
반응형

목차

1. 왜 이 글을 작성하게 되었는가?
2. 들어가기 전에... // 계층에 관하여
3. DTO, VO, Entity의 개념
4. DTO vs VO
5. DTO vs Entity
6. 정리


 


😲 왜 이 글을 작성하게 되었는가?

 

토이 프로젝트를 들어가기 앞서 어떤 식으로 프로젝트 구조를 잡을지 고민중이다.

GitHub에서 다른 사람들이 Spring으로 개발한 웹 서비스를 탐방했는데 구조가 아래와 크게 다르지 않았다.

  1. service 폴더
  2. domain 폴더: repository와 dto 포함
  3. web 폴더: controller 포함

 

그런데 여기서 domain 폴더명이 다 제각각이었다. 😱

데이터를 저장하는 객체인 것은 알겠는데 Entity, Domain, DTO 등 다양한 이름을 존재하였고 이에 대한 차이가 뭘까 궁금해져서 해당 글을 포스팅하게 되었다.

 

 


🤩 들어가기 전에...

 

각 차이점을 이해하기 앞서 Layer, 즉 '계층'에 대해 이해할 필요가 있다.

계층은 크게 4가지로 나눠져있다.

 

  1. Presentation Layer (=UI Layer)
    - 서비스와 관련된 정보 표시
    - 사용자가 직접 액세스할 수 있는 계층 (ex: 웹페이지, OS - GUI, ...)

  2. Application Layer (=Service Layer)
    - 특정 application 작업을 수행하는데 필요한 작업을 정의
    - 필요한 도메인 작업 위임하고 다른 서비스와 상호 작용

  3. Business Logic Layer (=Domain Layer)
    - 유효성 검사, 계산같은 로직 포함

  4. 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);
}
jUnit 테스트 결과 - 오류 화면

 

테스트가 실패한 원인은 두 객체의 주소값이 다르기 때문이다.

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() 오버라이드 후 테스트 결과

 

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);
}
hashcode_test - 실패 화면

 

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: 값 표현용

 

🔻 사람들은 왜 둘을 혼용할까?

더보기

많은 추측이 있지만 가장 유력한 후보는 아래 책 때문이다.

 

Core J2EE Patterns

 

초판에서는 데이터 전달용 객체를 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의 차이점에 대해 알아보겠다.

원래 본 글에 모두 담으려고 했으나 설계법에 대한 선행지식이 필요한 것 같아 더 조사해보고 따로 작성하려 한다.


참고

  1. 우아한 테크코스 - 10분 테크톡: 인비의 DTO vs VO
  2. 국윤창님 블로그 - DAO, DTO, Service
  3. matinFowler.com - PresentationDomainDataLayering
  4. StackExchange - Application layer vs domain layer?
  5. baeldung - java equals() and hashCode() Contracts
반응형