VO - Value Object
by SoonYong Hong
Value Object
VO(Value Object)란
VO는 도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정 값을 나타내는 객체를 의미한다. 잘 설계된 VO는 아래와 같은 모습을 보여준다.
VO의 특징
어떤 대상을 측정하고 수량화하고 설명한다.
이 VO는 도메인의 실제 대상을 설명하는 객체이다.
불변성
생성되면 변경되지 않는다.
- setter를 제공하지 않는다.
- 생성자 외의 메소드에서 상태 변경이 불가하다.
관련 특성을 모아서 전체 개념을 설명한다.
각각의 VO는 실제 의미를 가지고 있어야한다.
예를들면 1000원짜리 사과는 아래와 같이 표현할 수 있다.
data class Product (
val name : String,
val amount : Integer,
val unit : String
)
val myApple : Product = Product("apple", 1000, "won")
하지만 실제로 amount와 unit이 모여야 실제 가격을 나타내는 전체 개념이 된다. 따라서 아래와 같이 개선이 필요하다.
data class Product (
val name : String,
val price : Price
)
data class Price (
val amount : Integer
val unit : String
)
val myApple : Product = Product("apple", Price(1000, "won"))
이렇게 하위 개념을 모아서 상위 개념을 만드는 것 뿐만아니라 상위 개념을 나누는 경우도 있다.
data class User (
val name: String,
val age: Integer
)
val myUser : User = User("john", 20)
클라이언트에서 Caplitalized Name이 필요한 경우가 있을 것이다.
val myUser : User = User("john", 20)
val capitalizedName = myUser.name[0] + myUser.name.subString(1)
이렇게 name을 중심으로한 도메인 로직이 외부에 새어나가게 되었다. 아래와 같이 개선되야한다.
data class User (
val name: Name,
val age: Integer
)
data class Name (
val value: String
) {
fun getCaplitalizedName() : String {
return "${value[0].uppercase()}${value.substring(1)}"
}
}
val myUser : User = User(Name("john"), 20)
val capitalizedName = myUser.name.getCaplitalizedName()
측정이나 설명이 변경되면 대체가능하다.
아래 처럼 새로운 값으로 할당이 가능해야 한다.
var user1 = User(Name("john"), 20)
user1 = User(Name("john"), 21)
다른 값과 equality 비교가 가능하다.
아래 처럼 비교할 수 있어야 한다.
class Name (
val value: String
) {
fun getCaplitalizedName() : String {
return "${value[0].uppercase()}${value.substring(1)}"
}
override fun equals(other: Any?): Boolean {
return if(other is GalaxyTab)
other.value == this.value
else false
}
}
미니멀리즘
VO는 가능하면 가장 최소한의 형태를 가져야한다.
data class User (
val name: String,
val age : Integer
)
enum class Role (
val read: Boolean,
val write: Boolean
) {
ADMIN(true, true), USER(true,false)
}
Role 도메인과 User 도메인의 모든 정보를 어드민 컨텍스트에서 알 필요가 없다. 이런 경우 User와 Role에서 필요한 정보만 전달하는 것으로 충분하다.
data class AdminUser (
val name: String
)
User에서 필요한 정보를 포함시키고 클래스 이름으로 Role을 충분히 알려주고 있다.
표준 타입
전화번호를 나타낼때 휴대폰 번호인지 집전화인지 직장전화인지 구분해주는 타입이 필요하다. 이런 타입을 별개의 컨텍스트에 관리하여 제공하여야한다. 이렇게 사용한다면 잘못된 타입을 할당할 수 있지만 존재하지 않는 타입을 사용하지는 못한다. 이러한 타입에는 Enum타입을 사용하는 것이 좋다.
구현 팁
- setter를 구현하지 않는다.
- 복사생성자를 사용할 경우, 얕은 복사를 제공한다.
- 불변성이 보장되기때문에 얕은 복사를 제공해도 무방하다.
- 구현상의 이유로 도메인 모델이 데이터 모델의 설계를 반영하지 않아야한다.
Subscribe via RSS