Post

도메인 모델 분리가 꼭 필요할까?

도메인 모델 분리가 꼭 필요할까?

도메인 모델 분리란, 영속화 시점에 사용되는 모델과, 비즈니스 문제를 표현할 때 사용되는 모델을 분리하는 것을 의미한다.

오랫동안 나는 도메인 모델을 분리하는 편이 더 낫다는 점에 의심을 품지 않았다. 도메인 모델을 분리함으로써 비즈니스 표현력을 높이고, 이는 곧 탐사 비용을 낮출거라는 기대 때문이었다. 그러나 기대와는 달리, 결과적으로 일반적인 서비스 개발에서 도메인 모델을 분리하는 것은 탐사 비용을 더 높이는 아쉬운 결과를 가져왔던 것 같다.

어떤 결론을 내린 것은 아니고, 최근에 동료 개발자와 논의하면서 더 좋은 방안을 찾기 위해 고민하는 중이다.

도메인 모델 분리의 장단점

도메인 모델을 분리하면 다음과 같은 장단점을 마주하게 된다.

장점

  • 도메인 모델을 분리하면 도메인 모델이 순수해지고, 책임이 명확해진다.
  • 도메인 모델이 영속화 기술에 종속되지 않음으로써 VO(Value Object) 와 Entity 를 손쉽게 분리할 수 있고, 이는 모델로 하여금 비지니스를 더 풍부하게 표현할 수 있게 한다.

단점

  • 영속화 시점에 사용되는 모델과 비즈니스 문제를 표현할 때 사용되는 모델 사이의 변환 비용이 발생한다.
  • 영속화 모델과 도메인 모델 사이의 간극이 벌어질수록 탐사 비용이 증가한다.

도메인 모델 합치: 카드 시스템

카드 시스템을 예로 들어보자.

카드의 배송지를 Value Object로 표현하고 싶다면 다음과 같이 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
class Card(
    @Id
    val id: Long,
    val shippingAddress: ShippingAddress
)

@Embeddable
class ShippingAddress(
    val base: String, // 기본 주소
    val detailed: String, // 상세 주소
    val zipCode: String // 우편 번호
)

그러나 이 방식은 shippingAddress 가 nullable 이고, shipping address의 프로퍼티 중에서 detailed 만이 optional인 상황을 제대로 표현하지 못한다. 그 예로, 아래와 같이 선언할 경우 JPA에서 제대로 매핑하지 못하고 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
class Card(
    @Id
    val id: Long,
    val shippingAddress: ShippingAddress?
)

@Embeddable
class ShippingAddress(
    val base: String, // 여기도 nullable 로 바꿔야 컴파일 에러를 해소함
    val detailed: String?,
    val zipCode: String // 여기도 nullable 로 바꿔야 컴파일 에러를 해소함
)

비지니스를 표현하는 데에 있어 한계점이 생긴다는 것이다.

도메인 모델 분리: 카드 시스템

만약 카드 시스템에 도메인 모델 분리를 하게 되면, 형상은 아래와 같아진다.

1
2
3
4
5
6
7
8
9
10
class Card(
    val id: Long,
    val shippingAddress: ShippingAddress?
)

class ShippingAddress(
    val base: String,
    val detailed: String?,
    val zipCode: String
)
1
2
3
4
5
6
7
8
@Entity
class CardEntity(
  @Id
  val id: Long? = null,
  val base: String?,
  val detailed: String?,
  val zipCode: String?
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
object CardEntityMapper {
  fun toEntity(card: Card): CardEntity {
    return CardEntity(
      id = card.id,
      base = card.shippingAddress.base,
      detailed = card.shippingAddress.detailed,
      zipCode = card.shippingAddress.zipCode
    )
  }

  fun toDomain(cardEntity: CardEntity): Card {
    return Card(
      id = cardEntity.id ?: throw IllegalStateException("Id cannot be null"),
      shippingAddress = cardEntity.base?.let {
          ShippingAddress(
            base = cardEntity.base!!,
            detailed = cardEntity.detailed!!,
            zipCode = cardEntity.zipCode!!
          )
        }
      }
    )
  }
}

책임이 명확해지고 도메인 모델이 순수(pure)해진데다, 도메인 모델 합치시 발생했던 표현력의 한계점까지 극복했다.

그러나 코드가 굉장히 verbose하고 복잡하다. 이런 방식이 익숙하지 않은 사람에게는 낯설고 이해하기 어렵다.
실용적이지 못하다는 것이다.

결론

진리의 케바케는 어쩔 수 없는 것 같다. 장단점에 대해 팀원과 함께 논의하며 타협안을 찾아야 한다.

개인적으로는 Spring + Kotlin + JPA 환경에서는 도메인 모델을 분리하는 것이 탐사 비용을 조금 더 높이더라도 비지니스 표현력을 높인다는 점에서 더 마음이 간다.

This post is licensed under CC BY 4.0 by the author.