최근 Spring MVC 강의를 듣고 JPA를 공부하고 있다. 그런데 공부 중 나오는 Entity, DTO 등의 개념에 대해서 공부가 부족함을 느껴서 글로 정리해보려고 한다.
예를 들어서 Book 테이블 내에 author, isbn, name을 컬럼으로 가지고 있다면, Entity 클래스의 속성도 author, isbn, name만 가져야 한다.
예시 코드
@Entity
public class Book {
private String name;
private String author;
private int isbn;
}
JPA를 사용하게 될 경우 엔티티 클래스는 @Entity 어노테이션을 명시해야 한다. @Entity는 엔티티 클래스임을 지정하고 테이블과 1 : 1로 매핑된다.
최대한 외부에서 Entity Class의 Data Field에 접근하지 못하도록 제한해서 해달 Class 안에서 접근을 허용할 데이터들을 제한하며 로직 메소드를 구현해야한다. 또한 domain logic만 가지며 presentation logic을 가지고 있어서는 안된다.
Entity 클래스를 작성할 때 Setter를 무분별하게 사용하면 객체(Entity)의 값을 쉽게 변경할 수 있게 되어서 객체의 일관성을 보장할 수 없다. 객체의 일관성을 유지할 수 있어야 유지 보수성이 올라가기 때문에 Setter의 사용을 지양해야한다.
그 방법으로는 객체의 생성자에 값들을 넣어주는 것이다.
예시 코드
public Book(String name, String author, int isbn){
this.name = name;
this.author = author;
this.isbn = isbn;
}
그러나 생성자 주입 또한 좋은 방법은 아니다. 생성자에 현재 넣는 값이 어떤 필드인지 명확히 알 수가 없고, 매개변수끼리의 순서가 바뀌더라도 코드가 모두 실행되기 전에는 문제를 파악할 수 없다는 단점이 있다.
그렇다면 해결책은 무엇일까?
예시 코드
@Getter
@Entity
@NoArgsConstructor
public class Book book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private String name;
private String author;
@Column(length = 13, nullable = false)
private int isbn;
@Builder
public Member(String name, String author, int isbn) {
this.name = name;
this.author = author;
this.isbn = isbn;
}
}
// 사용 방법
Book book = new book.builder()
.name("velog_초보자")
.author("pp8817")
.isbn(145643156)
.build();
Builder 패턴을 사용하면 된다. Builder 패턴을 사용하면 멤버 변수가 많아지더라도 어떤 값을 어떤 필드에 넣는지 코드를 통해 확인할 수 있고, 필요한 값만 집어 넣는 것이 가능해진다.
DB에서 꺼낸 데이터를 저장하는 Entity를 가지고 만드는 일종의 Wrapper라고 볼 수 있는데, Entity를 Controller 같은 클라이언트단과 직접 마주하는 계층에 직접 전달하는 대신 DTO를 사용해 데이터를 교환한다.
DTO는 그저 계층간 데이터 교환이 이루어 질 수 있도록 하는 객체이기 때문에, 특별한 로직을 가지지 않는 순수한 데이터 객체여야 한다. 또한 DB에서 꺼낸 값을 DTO에서 임의로 조작할 필요가 없기 때문에 DTO에는 Setter를 만들 필요가 없고 생성자에서 값을 할당한다.
사용자는 자신이 필요한 Interface를 DAO에게 던지고 DAO는 Interface를 구현한 객체를 사용자에게 편리하게 사용할 수 있도록 반환한다.
엄밀히 따지면 Repository가 DAO를 대체한다고 보는 것이 맞다. DAO와 Repository 모두 DB에 직접 쿼리를 날려 CRUD 기능을 하는 것은 동일하지만, 개념적인 측면에서 차이가 있다.
예제 코드
@Getter @Setter
@Alias("book")
class BookVO {
private String name;
private String author;
private int isbn;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(id, book.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
VO는 Getter, Setter을 가질 수 있다. 또한, 테이블 내에 있는 속성 외에 추가적인 속성을 가질 수 있으며, 여러 테이블(A, B, C)에 대한 공통 속성을 모아서 만든 BaseVO 클래스를 상속받아서 사용할 수도 있다.
Entity의 값이 변경되면 Repository 클래스의 Entity Manager의 flush가 호출될 때 DB에 값이 반영되고, 이는 다른 로직들에게도 영향을 준다. 따라서 뷰와 통신하면서 필연적으로 데이터의 변경이 많은 DTO 클래스를 분리함으로써 문제를 해결해야 한다.
또한, 도메인 설계가 아무리 잘 되어있어도 Getter만을 이용해서 원하는 데이터를 표시하기 어려운 경우가 발생할 수 있다. 이때 Entity와 DTO가 분리되어 있지 않은 경우 Entity 내의 Presentation을 위한 필드나 로직이 추가되게 되어서 객체 설계가 망가진다.
따라서, 이런 경우에는 분리시킨 DTO에 Presentation 로직 정도를 추가해서 사용하고, Entity에는 추가하지 않기 때문에 도메인 모델링이 깨지지 않는다.
VO(Value Object)도 DTO와 Data를 전달하는 객체로 동일한 개념이다. 다만 DTO와 VO의 차이는, DTO는 데이터를 계층간 교환(Transfer)하는데 의미가 있고, VO는 읽기만 가능한 read-only 속성을 가진 객체로서 데이터 그 자체에 의미를 두고 있다는 점이다.
DTO는 데이터를 계층간 교환(Transfer)하는데 의미가 있고 VO는 조금 더 특정한 Business Logic의 결과 값을 담는 객체이다.
equals, hashCode Method를 구현하여 특정 중요한 Data를 전달할 때는 VO를 생성하여 이를 동일한 객체 비교까지 필요한 Logic 내에서 주로 사용하게 된다.
DTO는 단순 통신 용도로 오가는 Data 전달 객체이기에 포괄적으로 사용할 수 있는 객체이므로, 민감하지 않거나 해당 객체 안의 값들을 통해 동일한 객체임을 비교하는 로직에 사용되지 않을 때는 DTO를 사용하면된다.
출처
Entity, DAO, DTO가 무엇이며 왜 사용할까?
[JAVA] DAO, DTO, VO 개념, 차이
Entity, VO, DTO