왜 계층간에 데이터를 주고 받을 때 Entity 객체를 주고받으면 편할 것 같은데 왜? DTO라는 객체를 만들어서 주고받는 걸까? 그 이유가 궁금했다. 찾아보니 VO객체에 대한 개념도 같이 등장하면서 각 개념의 차이점과 왜 해당 개념을 사용하는 것인지 비교하여 정리해볼 필요성을 느꼈다.
DTO(Data Transfer Object)는 getter와 setter 메소드를 제외한 비즈니스 로직이 없는 순수한 데이터 전달을 위한 객체다. DTO는 데이터를 전송하기 위한 용도로만 사용되기 때문에, 데이터를 담는 용도 외에 다른 기능을 추가하지 않는다.
주로 View와 Controller 사이에서 데이터를 주고 받을 때 활용한다.
다음과 같이 setter를 가지는 경우 가변 객체로 활용할 수 있으며,
@Getter
@Setter
public class UserDTO {
private String name;
private String email;
private String password;
}
혹은 setter가 아닌 생성자를 이용해서 초기화 하는 경우, 불변 객체로 활용할 수 있다.
DTO를 불변 객체로 만들 경우 데이터를 전달하는 과정에서 데이터가 변동되지 않음을 보장할 수 있다.
@Getter
public class UserDTO {
private final String name;
private final String email;
private final String password
public UserDto(String name, String email, String password) {
this.name = name;
this.email = email;
this.password = password;
}
}
VO(Value Object)는 값을 표현하는 객체이다. 동일한 값을 가지면 동일한 객체로 간주하는 객체이다.
주로 데이터 전달을 위해 사용되며, getter메서드와 함께 비즈니스 로직을 포함할 수 있다. VO객체는 값 자체를 표현하기 때문에 불변성을 보장하도록 설계하여야 하므로, setter 메서드는 가지지 않고 생성자를 통해 값을 초기화한다.
public class UserVO {
private final Long id;
private final String name;
private final String email;
public UserVO(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
하지만 이렇게 코드를 작성할 경우 동일한 값의 2개의 객체를 생성하여 비교할 경우 주소가 다르기 때문에 동일한 객체로 판단하지 않는다.
// UserTest.java
public class UserTest {
@DisplayName("VO 동등비교를 한다.")
@Test
void isSameObjects() {
User user1 = new User(1L, "kimdael", "kim.dael@example.com")
User user2 = new User(1L, "kimdael", "kim.dael@example.com")
assertThat(user1).isEqualTo(user2);
assertThat(user1).hasSameHashCodeAs(user2);
// 동일한 주소가 아니므로 assert에서 걸림!
}
}
따라서 equals()와 hashCode()메소드를 오버라이딩하여 객체의 값 비교를 할 수 있도록 하여야 한다.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) && Objects.equals(name, user.name) && Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(id, name, email);
}
오버라이딩한 equals 메소드는 두 객체의 모든 값을 비교하여 같으면 true, 다르면 false를 반환한다. hashCode 메소드는 객체를 식별할 수 있는 고유한 정수 값을 반환한다.
Entity는 DB의 테이블과 매핑되는 클래스다. 해당 클래스를 기준으로 테이블이 생성되고 스키마가 변경된다. 또한 비즈니스 로직을 포함할 수 있다.
Entity 클래스는 단순히 데이터를 전달하는 역할이 아니라 DB와 연동되는 핵심 클래스이기 때문에, 해당 클래스의 정보가 변경되는 경우 DB테이블의 스키마도 변경된다. 따라서 Entiy 클래스를 요청이나 응답 값을 전달하는 용도로 사용하면, 예상치 못한 문제가 발생할 수 있다.
@Entity
@Getter
@Setter
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private Integer age;
public User(String name, String email, String password, Integer age) {
this.name = name;
this.email = email;
this.password = password;
this.age = age;
}
}
위 예시 코드를 보면 @Entity 어노테이션을 사용하여 해당 클래스가 Entity임을 명시한다. 또한 @Id 어노테이션을 사용하여 Entity 클래스의 고유 식별자를 설정하고 @Column 어노테이션을 사용하여 각 필드의 DB column 정보를 명시한다.
여기서 유의할 점은 Entity 클래스가 Setter 메소드를 사용할 수 있다는 점이다. 이 경우 DB에서 값을 변경하는 가변 객체로 활용할 수 있게 된다. 하지만 가변객체를 다루는 데에 있어 신중한 판단이 필요하다.