개발 도중 API 의 Request, Response 처리를 할 때나 다른 계층으로 데이터를 넘길 때 별도의 DTO 를 생성해야 하는 고민을 하기도 한다.
또한, 개발자마다 개발하는 과정에서 혹은 기존 소스에서 확인해보면 DTO 와 VO 는 다른 개념이지만 종종 같은 개념으로 사용하기도 합니다. 또한, Entity 와 DTO 도 한 클래스로 사용하기도 한다.
이것이 무조건? 틀렸다라곤 할 수는 없겠지만, 각각의 역할이 다르기 때문에 분리하여 각 역할에 맞게 사용하여야 하고 이것이 좋은 코드로 이어질 것이라고 생각한다.
이번 글에서는 각 Entity, DTO, VO 에 대해 정리를 해보고자 한다.
1. Entity
- 실제 DB 테이블과 매핑되는 핵심 클래스로, 데이터베이스의 테이블에 존재하는 컬럼들을 필드로 가지는 객체입니다.
상속을 받거나 구현체여서는 안되며, DB 테이블내에 존재하지 않는 컬럼을 가져서도 안됩니다.
- Entity 는 데이터베이스 영속성(persistent)의 목적으로 사용되는 객체이며, 요청(Request)이나 응답(Response) 값을 전달하는 클래스로 사용하는 것은 좋지 않습니다.
- 외부에서 Entity 클래스의 Data Field 에 접근하지 못하도록 제한해야 합니다.
- Entity 는 id 로 구분되며, 비지니스 로직을 포함할 수 있습니다.
@Entity
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
private String phoneNumber;
}
- Entity 클래스에서는 setter 메서드의 사용을 지양해야 합니다.
- 이유는 변경되면 안되는 인스턴스나 데이터에 대해서 setter 를 통해 접근이 가능해지기 때문에 객체의 일관성, 안전성을 보장하지 어렵습니다.
- 그렇기 때문에 Entity 에서는 Constructor(생성자) 또는 Builder 를 사용합니다.
2. DTO(Data Transfer Object)
- DTO 는 계층(Layer) 간 데이터 교환이 이루어질 수 있도록 하는 객체입니다.
데이터 교환만을 위해 사용하므로 비지니스 로직을 갖지 않고, getter/setter 메소드만 갖습니다.
- Controller 같은 클라이언트와 직접 마주하는 계층에서 DTO 를 사용하여 데이터를 요청/응답을 하며, 주로 View 와 Controller 사이에서 데이터를 주고받을 때 활용합니다.
@Getter
@Setter
public class MemberDto {
private String name;
private int age;
}
- 위 코드에서와 같이 setter 메소드를 가지는 경우 가변 객체로 활용할 수 있습니다.
- 만약, 불변 객체로 활용하고자 한다면 setter 대신 생성자를 이용해서 초기화하여 사용하실 수 있습니다.
불변 객체로 만들면 데이터를 전달하는 과정에서 데이터가 변조되지 않음을 보장할 수 있습니다.
@Getter
@AllArgsConstructor
public class MemberDto {
private String name;
private int age;
}
3. VO(Value Object)
- VO 는 직역과 같이 값 그 자체를 표현하는 객체라는 의미를 가지고 있습니다.
핵심 역할은 equals() 와 hashCode() 를 오버라이딩 하여 객체의 불변성을 보장합니다.
즉, 객체들의 주소 값이 달라도 데이터 값이 같으면 동일한 것으로 판단합니다.
- getter 메소드는 가질수 있지만, setter 메소드는 가지지 않습니다.(ReadOnly 특징을 가집니다)
- 비지니스 로직을 포함할 수 있습니다.
@Getter
@AllArgsConstructor
public class UserVo {
private String name;
private Integer age;
private String address;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserVo userVo = (UserVo) o;
return Objects.equals(name, userVo.name) && Objects.equals(age, userVo.age) && Objects.equals(address, userVo.address);
}
@Override
public int hashCode() {
return Objects.hash(name, age, address);
}
}
- 테스트 코드를 작성하여 확인해보면 동일한 값으로 확인하실 수 있습니다.
public class VoTest {
@DisplayName("VO 동등비교")
@Test
void isSameVo(){
UserVo vo1 = new UserVo("개발자", 20, "인천");
UserVo vo2 = new UserVo("개발자", 20, "인천");
Assert.assertEquals(vo1,vo2);
assertThat(vo1).isEqualTo(vo2);
assertThat(vo1).hasSameHashCodeAs(vo2);
}
}
- 만약 equals(), hashCode() 메소드가 없이 테스트를 실행하시면 테스트 통과에 실패하시는 것을 확인하실 수 있습니다.
4. 역할 분리를 해야 하는 이유
4-1. 관심사 분리
- 각각 분리를 하여 사용해야 하는 근본적인 이유는 관심사가 서로 다르기 때문입니다.
관심사의 분리(Separation of Concerns, SoC)
소프트웨어 분야의 오래된 원칙 중 하나로, 서로 다른 관심사들을 분리하여 변경 가능성을 최소화하고, 유연하며 확장 가능한 클린 아키텍처를 구축하도록 도와줍니다.
- DTO 는 Entity 객체와 달리 각 계층끼리 주고받는 데이터의 개념입니다.
- 데이터를 담고 있는 점에서 유사하지만, DTO의 목적 자체는 전달이므로 읽고, 쓰는 것이 가능하고 일회성으로 사용되는 성격이 강합니다.
- Entity 는 주로 JPA 에서 사용되는데 단순히 데이터를 담는 객체가 아니라 DB 와 관련된 중요한 역할을 하고, 데이터의 핵심 비지니스 로직을 담는 객체입니다.
4-2. Validation 로직 및 불필요한 코드의 분리
- 데이터 검증을 위한 @Valid 어노테이션이나, JPA를 사용하기 위한 Entity 에서도 @Id, @Column 등 어노테이션을 사용하여 각 객체를 만들게 됩니다.
- 만약 Entity, DTO 를 분리하지 않고 같이 사용한다면 해당 클래스의 코드가 상당히 복잡해집니다.
- 예를 들어, Entity 와 DTO를 같이 사용한다면 아래와 같이 알아보기 힘든 클래스가 생성되게 됩니다.
@Entity
@Table
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@NotNull
@Size(min = 0)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long id;
@NotBlank
@Column(nullable = false)
private String userId;
@NotNull
@Size(min = 0)
@Setter
@Column(nullable = false)
@ColumnDefault("0")
private Integer age;
@CreationTimestamp
@Column(nullable = false, length = 20, updatable = false)
private LocalDateTime createTime;
@UpdateTimestamp
@Column(length = 20)
private LocalDateTime updateTime;
}
- 위와 같이 어노테이션들이 잔뜩 있는 클래스는 보기만 해도 유지보수가 힘들어짐을 느낄 수 있습니다.
- 또한 위 Entity 클래스의 생성일자(createTime)나 수정 일자(updateTime)는 API 요청 및 응답 데이터로는 필요 없을 수 있습니다.
- 이렇게 한 클래스로 Entity 와 DTO 를 같이 사용하게 되면 핵심 비지니스 도메인 코드를 포함하여 요청/응답을 위한 값, 유효성 검증을 위한 값 등이 추가되면서 지나치게 비대해지고, 확장 및 유지보수 등에서 어려움을 겪게 될 것입니다.
4-3. API 스펙의 유지
- Entity 클래스를 통해서 API 를 응답한다고 예를 들어 본다면
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
public class Todo {
@Id
@GeneratedValue
@Column(name = "todo_id")
private Long id;
private String item;
private String date;
private boolean completed;
private String time;
private LocalDateTime writeDate;
private LocalDateTime updateDate;
@Column(columnDefinition = "varchar(1) default 'Y'")
private String useYn;
}
- API 호출 시 응답 값으로 아래와 같이 전달받게 됩니다.
{
"id" : "15",
"item" : "TODO 1",
"date" : "2022-10-15",
"completed" : false,
"time" : "13:33:05",
"createDate" : "20221015",
"updateDate" : "20221015",
"useYn" : "Y"
}
- 이때 만약 어떠한 이유로 한 파라미터의 이름을 변경해야 하는 상황이 있을 경우, API를 사용하고 있는 모든 사용자들이 장애를 겪게 되고 모두 수정 작업이 필요하게 될 것입니다.
- 물론 @JsonProperty 를 이용해 반환되는 이름의 값을 변경할 수도 있겠지만, 위 2번에서와 같이 클래스 자체를 무겁게 만들기 때문에 근본적인 해결책이라곤 할 수 없습니다.
- 그러므로 DTO 를 이용하여 분리해 독립성을 높이고 변경이 전파되는 것을 방지해야 합니다.
- DTO 를 사용한다면 내부에서 Entity 가 변경되어도 API 스펙이 변경되지 않으므로 안정성을 확보할 수 있습니다.
5. 정리
- 개발을 하다보면 시간에 쫓겨 역할 분리를 안하고 하나의 파일을 통해서 통신에서도 사용하고 DB 에서도 사용 하는 일이 있을 수 있습니다.
- 서비스는 동작하겠지만, 추후 유지보수 과정이나 변경사항이 있을 경우 엄청난 작업으로 이어질 수 있기 때문에 각 객체의 목적에 맞게 생성하여 사용하는 것이 좋다고 생각합니다.
- 이것이 무조건의 정답은 아니지만 조금더 좋은 아키텍처를 구성하는 하나의 방법으로 생각하시면 좋을 것 같습니다.
- 간단하게 표로 정리해보면 아래와 같습니다.
Entity | DTO | VO | |
목적(역할) | JPA 사용시 DB 와 직접적으로 매핑하여 컬럼의 필드를 가지는 객체 | 계층(Layer) 간 데이터 송/수신 용도의 객체 | 객체안의 값을 통해서 비교해야하는 중요 로직에서 사용하는 Data 객체 |
getter 사용 | O | O | O |
setter 사용 | X | O | X |
equals(), hashCode() 사용 | O | O | X |
Spring Tier 사용 범위 | Repository <-> Service | Controller <-> Service Client <-> Controller |
Controller <-> Service |
참고
- https://mangkyu.tistory.com/192
- https://tecoble.techcourse.co.kr/post/2021-05-16-dto-vs-vo-vs-entity/
- https://wildeveloperetrain.tistory.com/m/101
- https://velog.io/@boo105/Entitiy-%EC%99%80-DTO-%EC%9D%98-%EB%B6%84%EB%A6%AC
'Tool, 이론 및 기타 > 이론' 카테고리의 다른 글
Forward Proxy(포워드 프록시)와 Reverse Proxy(리버스 프록시) 정리 (0) | 2023.03.05 |
---|---|
sync, async, blocking, non-blocking 정리 (2) | 2023.02.05 |
Message Broker(메시지브로커) 및 Message Queue(메시지큐) 정리 (0) | 2022.10.14 |
[Nexacro] 넥사크로 메모장 (0) | 2022.06.01 |
[금융 IT] 마이데이터(MyData)란? (0) | 2022.06.01 |