DTO vs VO vs Entity

503·2022년 9월 26일
1

발표스터디

목록 보기
1/4
post-thumbnail

안녕하세요, 503입니다.

저번 주부터 발표 스터디를 시작했는데요. 이번 주의 주제는 바로 DTO와 VO의 개념과 차이점에 대해 포스팅하려고 합니다.

필자는 항상 프로젝트를 시작할 때마다 어떻게 하면 더 좋고 깔끔한 코딩을 할 수 있을지 고민했던 것 같습니다. 그 중 계층 간 역할 분리를 위해 아키텍처를 어떻게 짤지 고민하게 되고, 그러면서 DTO, VO, DAO, Entity, Domain, repository.... 등등 많고 새로운 개념들을 접하게 됐습니다. 잘 구분해서 사용하려고는 하지만, 사실 정확한 개념을 설명하려고 하면 자신있게 말할 수 없었습니다. 그래서 이번 포스팅에서는 대체 이것들은 무엇이고 언제쓰이는지, 또 차이점은 무엇인지에 대해 정리해보겠습니다.


1. DTO란?

DTO(Data Transfer Object)는 데이터 전송 객체라는 의미를 가지고 있습니다.
'계층 간' 데이터 교환을 위한 객체(java beans)으로 데이터를 담아 전달하는 바구니라고 생각하면 됩니다.

  • DB에서 데이터를 얻어 service나 controller 등으로 보낼 때 사용하는 객체
  • 로직을 갖고 있지 않는 순수한 데이터 전달만을 위한 객체며 getter/setter 메서드 만을 갖음. 보내는 쪽에서 setter을 사용해 데이터를 DTO에 담아보내고 받는 쪽에서 getter을 사용해 데이터를 꺼냄

PersonDto.java

이름과 나이 필드를 가진 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;
    }
}

PersonService.java

데이터를 보내주는 Service 단에서 PersonDto 객체를 생성하고 setter를 사용해 데이터 값을 넣어줍니다.

public PersonDto createNewPerson(){
	String name = "오백삼";
    int age = 53;
    
    PersonDto personDto = new PersonDto();
    personDto.setName(name);
    personDto.setAge(age);
    
    return personDto;
}

PersonController.java

데이터를 받는 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)



2. Entity란?

Entity(엔티티)는 실제 DB 테이블과 매핑(mapping)되는 객체입니다.
데이터베이스의 테이블에 존재하는 컬럼들을 필드로 가지는 객체로 DB 테이블과 1:1로 매핑되며 테이블이 가지지 않는 컬럼을 필드로 가져서는 안됩니다.

  • 객체의 불변성을 보장해야 하기에 setter 메서드를 지양하고 생성자(constructor) 또는 Builder을 사용
  • 상속받거나 구현체이면 안됨

DTO와 무엇이 다른가?

Entity 클래스는 데이터베이스와 매핑되어 있는 핵심 클래스로 데이터베이스의 영속성(persistent)의 목적으로 사용하기 떄문에, 요청이나 응답 값을 전달하는 클래스로 사용하는 것에는 맞지 않습니다.
(*영속성(persistence) : 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성을 의미)

데이터 교환용으로 사용하면 안되는 이유?

Entity를 기준으로 테이블이 생성되고 스키마가 변경됩니다. 또한, 많은 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작하는데, 뷰가 변경될 때마다 Entity 클래스가 변경되면 여러 클래스에 영향을 주게 될 것입니다.

그렇기에 요청이나 응답 값을 전달하는 클래스로는 다른 클래스에 영향을 끼치지 않고 자유롭게 변경이 가능한 DTO를 사용하는 것이 좋습니다.



3. VO란?

VO(Value Oject)는 값 그 자체를 표현하는 객체입니다. 객체의 정보가 변경되지 않는 '불변성'을 보장합니다.

  • VO 내부에 선언된 속성(필드)의 모든 값들이 VO 객체마다 값이 같아야 똑같은 객체라고 판별함.

  • 서로 다른 이름을 갖는 VO 인스턴스라도 모든 속성 값이 같다면 두 인스턴스는 같은 객체로 인식

  • VO에서 객체를 속성 값만으로 비교하도록 Object 클래스의 equals()hashcode()를 오버라이딩해 구현

    [추가설명] HashSet, HashMap, HashTable 의 동등 비교방식
    1. hashcode() 리턴값을 비교 후, 같다면 equals() 리턴 값 비교
    2. 그 결과가 true면 동등 객체로 보며, 둘 중 하나라도 false라면 다른 객체로 봄
  • gettersetter을 가질 수 있으며, 그 외의 로직도 가질 수 있음(테이블 내 있는 속성 외 추가적인 속성을 가질 수 있음)

대표적인 예시로는 돈이 있는데요. 여러 만원짜리의 지폐의 고유번호가 달라도 같은 만원으로 보는 것처럼 "값" 자체로만 비교를 합니다.

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);
    }
}

VO vs DTO

둘 다 데이터를 담고있는 객체지만 아래와 같은 차이점이 있습니다.

  • VO의 경우 Read Only의 목적이 강하고 데이터 자체도 불변(immutable)하게 설계하는 것이 정석입니다.
  • DTO는 주로 데이터 수집의 용도가 좀 더 강합니다.

예시를 들자면, 웹 화면에서 로그인하는 정보는 DTO로 처리하고, 테이블과 관련된 데이터는 VO로 처리합니다.

VO vs Entity?

DTO와 VO는 데이터를 담고, 다른 계층 또는 다른 컴포넌트들로 데이터를 넘겨주기 위한 자료구조(Data Structure)이기에 어떠한 기능 및 동작도 없어야 합니다.

반면에 Entity는 핵심 비지니스 로직을 담는 비지니스 도메인의 영역의 일부입니다. 그러므로 Entity 또는 domain 객체에는 비지니스 로직이 추가될 수 있습니다. 따라서 다른 계층이나 컴포넌트들 사이에서 데이터 전달을 위한 객체로 사용하지 않습니다.

예시를 들어보겠습니다.

동일한 속성을 가진 두 개의 Entity가 있습니다.

  • Entity는 id로 구별합니다. 따라서 이 두 객체는 두 개의 다른 id를 가지고 있기 때문에 동일하지 않다고 봅니다.
  • 그러나 동일한 속성을 가진 두 개의 VO는 같은 객체입니다.

하나의 Entity가 있습니다.

  • Entity는 존재하는 동안 속성들이 계속 바뀔 수 있지만, 속성이 바뀌어도 같은 Entity입니다.
  • 그러나 VO는 값이 바뀌면 다른 객체가 됩니다.
    ex. 돈이라는 객체가 있고, 얼마를 사용해서 거스름돈을 받았습니다. 그러면 거스름돈 != 처음 갖고있던 돈이기 때문에 처음 갖고있던 돈과 사용한 후의 돈은 다른 값의 객체가 됩니다..

Spring의 MyBatis(Entity) vs JPA(VO)

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)를 소개하며 알아보겠습니다.


정리

  • DTO: Layer 간 데이터 교환, 데이터 전달용
  • VO : 값을 갖는 순수한 도메인, 값 표현용
  • Entity : DAO와 DB 테이블과 매핑하는 객체
  • DAO : Repository

계층(Layer) 간의 데이터 교환, 매핑을 위해 사용하는 객체들이 DTO와 Entity입니다.
DTO와 VO는 데이터를 넘겨주기 위한 자료구조로 계층 간 데이터 전달을 위해 사용됩니다.
그러나 Entity는 핵심 비즈니스 로직을 담는 비지니스 도메인 영역의 일부로 Entity 및 Domain 객체에는 비즈니스 로직이 추가될 수 있으며 계층 간 데이터 전달용으로 사용하지 않습니다.



🔗 참고

profile
얼레벌레 개발자로 살아가기. 개발하면서 만났던 이슈를 기록합니다.

0개의 댓글