개요
프로그래밍을 하다 보면 데이터를 전달하고 표현해야 한다는 사실에 의문을 가지는 사람을 없을 것입니다.
그렇다면 어떻게(how)라는 방법이 중요한 문제라 생각합니다.
본 페이지에서는 Spring Boot를 공부하면서 DTO와 VO를 활용해 데이터를 전달하고 표현하는 방식에 대해 설명합니다.
내용
1. DTO (Data Transfer Object)
DTO는 Controller, Service, View 간 데이터를 주고받는 용도로 사용합니다.
오로지 데이터를 저장하고 전달하는 역할에 집중하기 위해 로직을 포함하지 않습니다. (getter와 setter만을 가집니다.)
일반적으로 식별자나 특정 속성에 의해 두 객체가 동일하다 판단합니다.
예시
// data class로 선언함으로써 getter와 setter 자동 생성
data class UserDTO(
var id: Long?,
var username: String,
var email: String
)
2. VO (Value Object)
VO는 값 자체를 표현하는 용도로 사용합니다.
DTO와 달리 로직을 포함할 수 있고, 불변성을 가지는 데이터를 저장하므로 getter만 존재합니다.
예시
// data class이지만 속성을 val로 선언함으로써 setter를 생성하지 않음
// 또한 val이기 때문에 속성 직접 변경 불가
data class Money (
val amount: Int,
val currency: String
)
// private으로 감추는 것도 가능
// 외부에서 값을 변경할 수 없게 되어 불변성이 보장
data class Money (
private var amount: Int,
private var currency: String
)
VO는 다른 메모리 주소를 가질지라도 모든 속성 값이 동일하면 같은 객체로 판단합니다.
예시
val money1 = Money(1000, "won")
val money2 = Money(1000, "won")
print(money1 == money2) // true (속성 비교)
print(money1 === money2) // false (메모리 주소 비교)
※ Kotlin에서 == 연산자는 equals()를 호출.
일반 class로 구현하게 되면 equals()와 hashCode()를 직접 구현(Override) 해야 합니다.
예시
class Money (
private val amount: Int,
private val currency: String
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is User) return false
return amount == other.amount && currency == other.currency
}
override fun hashCode() = this.amount.hashCode() + this.currency.hashCode()
}
VO 생성 시점에 유효성 검사를 한다는 특징이 있습니다.
생성자를 private으로 두고, 정적 메소드 패턴을 통해 객체를 생성하는 방식이 일반적입니다.
예시
class Money (
private val amount: Int,
private val currency: String
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is User) return false
return amount == other.amount && currency == other.currency
}
override fun hashCode() = this.amount.hashCode() + this.currency.hashCode()
companion object {
fun of(amount: Int, currency: String): Money {
validateMoney(amount, currency)
return Money(amount, currency)
}
}
}
3. 예시로 DTO와 VO 이해하기
DTO는 ‘학생의 성적표’와 유사하다.
학생의 성적표에는 학생의 이름, 학년, 학번, 그리고 각 과목별 성적이 기록됩니다.
매 학기가 지날 때마다 과목별 성적이 변할 수 있지만, 우리는 성적표가 동일한 학생의 것임을 알고 있습니다.
이유는 이름, 학년, 학번으로 학생을 구별하기 때문입니다.
이처럼 DTO는 식별자(ID), 혹은 특정 속성들이 동일하면 두 객체가 동등하다 판단합니다.
즉, UserDTO에서 name과 email이 서로 다르더라도 id가 동일하면 두 객체는 동등하다 여기는 것입니다.
예시
// id로만 서로 다른 UserDTO를 비교할 수 있게 equals()와 hashCode() override
data class UserDTO(
var id: Long?,
var username: String,
var email: String
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is UserDTO) return false
return id == other.id // id만 비교
}
override fun hashCode(): Int {
return id?.hashCode() ?: 31 // id만 사용하여 hashCode 생성
}
}
val user1 = UserDTO(1, "Kim", "kim@example.com")
val user2 = UserDTO(1, "Lee", "lee@example.com")
print (user1 == user2) // true
VO는 ‘화폐’와 같다.
1000원 짜리 지폐가 2장 있습니다.
서로의 발권 번호는 다르지만 2장의 지폐가 가지는 가치는 동일하기 때문에
우리는 2장 모두 같은 1000원 지폐라 말합니다.
이처럼 VO는 그 값을 나타내는 속성이 동일하다면 두 객체는 동등하다 판단합니다.
예시
val money1 = Money(1000, "won")
val money2 = Money(1000, "won")
print(money1 === money2) // "발권 번호(메모리 주소)"가 다르지만
print(money1 == money2) // "가치(amount)"와 "통화(currency)"는 동일하다
정리
개념 | DTO (Data Transfer Object) | VO (Value Object) |
정의 | 계층 간 데이터 전송을 위한 객체 | 특정 값을 표현하는 객체 |
식별 방법 | ID 또는 특정 속성 활용 | 값이 같으면 같은 객체로 판단 |
가변성 | 가변 | 불변 (값 변경 불가) |
라이프사이클 | 컨트롤러-서비스-뷰 간 일시적으로 사용됨 | 엔티티 내부에서 값으로 사용됨 |
어디서 사용? | Controller ↔ Service ↔ View (API 응답 등) | Entity 내부의 값 필드로 사용 |
주요 목적 | 데이터를 변환해 계층 간 전달 | 특정 개념을 값으로 표현 |
참고
https://curiousjinan.tistory.com/entry/spring-data-transfer-vo-dto
스프링에서 데이터 전달의 핵심: VO와 DTO의 이해 및 활용
스프링에서 Data를 전달하는 객체에는 VO, DTO가 있는데 이게 어떤것인지 알아보자 VO (Value Object)와 DTO (Data Transfer Object)는 모두 Java 및 Spring과 같은 객체 지향 프로그래밍 및 프레임워크에서 데이터
curiousjinan.tistory.com