본문 바로가기

SpringBoot

[SpringBoot] DTO와 VO

개요

 

프로그래밍을 하다 보면 데이터를 전달하고 표현해야 한다는 사실에 의문을 가지는 사람을 없을 것입니다.

그렇다면 어떻게(how)라는 방법이 중요한 문제라 생각합니다.

 

본 페이지에서는 Spring Boot를 공부하면서 DTOVO를 활용해 데이터를 전달하고 표현하는 방식에 대해 설명합니다.


내용

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