왜 DDD인가?
도메인 주도 설계(Domain Driven Development . 이하DDD)는 말 그대로 도메인 패턴을 중심에 놓고 프로그램을 설계/개발해 나가는 방식을 말한다. 사실 DDD자체는 하나도 새로울 것이 없는 개발 방법으로 객체 지향 프로그래밍(OOP)과도 사실상 동의어라 할 수 있다.OOP가 도입 되기 이전에 대부분의 프로그램에서 사용되는 문제 해결 방식인 트렌젝션스트립트Transaction Script나 SMART UI의 경우 문제 해결을 위해 일반적으로 처리를 순서 별로 나열하여 처리하므로 직관적이고 대부분의 문제 영역에서 큰 문제 없이 작동한다. 하지만 문제 영역이 복잡해지면 이러한 방식은 한계를 드러내게 되며, 이를 극복하기 위한 시도가 OOP이고 이를 구현하기 위한 실천적인 어드바이스를 모아 놓은 것이 이 DDD이다.
도메인 모델링에 대한 자세한 설명은 예전에 작성한 "도메인 주도 설계와 애자일 개발"을 참고하기 바란다.
오늘은 일본의 유명 모바일 게임 회사인 GREE의 블로그에 게재된 도메인 주도 설계에 대한 기사를 소개해 보고자 한다.
저자인 카토 준이치씨는 GREE(2014년 9월경 ChatWark사로 이직)에서 선임 엔지니어로 활동하면서 사내에 Scala와 DDD를 도입 시킨 전도사로 유명하며 InfoQ Tokyo 2014에서 동일한 주제로 강연을 하기도 하였다.
이 글은 비교적 난해한 도메인 주도 설계에 대해 게임을 예로 들어 알기 쉽게 설명하고 있으며, 예제로 등장하는 코드는 Scala이지만 그다지 어려운 코드는 사용하지 않고 있으므로 Scala를 잘 모른다 하더라도 이해하는데 큰 무리는 없을 것 이라 생각한다. (Scala의 용어에 대해서는 중간 중간 주석을 첨부하였다.)
원문 : Scalaコードでわかった気になるDDD
참고로 이 번역 기사는 저자의 허락을 받았음을 밝혀둔다.
(사실은 QCon에서 만난 카토씨 에게 기사를 번역해서 한국에 소개한다고 일방적으로 통보한 거지만...)
본 기사는 내용이 많아 다음과 같이 두 번에 나눠서 게재 하도록 하겠다. 절대 블로그 방문자 수를 올려보기 위한 수작이 아님을 믿어 달라.
1. 문제 영역에 대한 올바른 이해
2. 라이프사이클의 관리
문제영역에 대한 올바른 이해
DDD란?
DDD는 이름 그대로 도메인을 중심으로 생각한 설계 방법 입니다. 조금 어려운 표현이지만, 도메인이라고 하는 것은 소프트웨어가 취급하는 '어떤 활동이나 관심과 관계가 있는 지식' 으로서, 바로 이 도메인을 중심으로 설계를 해 나가는 방법입니다. 흐음... 뭔가 잘 와 닿지 않는 표현이군요. ㅋ좀 더 구체적으로 예를 들자면, 롤플레잉게임(이하 RPG)에서는 '플레이어','몬스터','아이템','무기/방어구','도시','던전'과 같은 여러가지 개념이 일반적으로 포함되어 있습니다. 이러한 개념을, RPG의 세계관의 '언어'를 사용하여 '도메인모델'이라고 하는 객체를 표현 하고 있습니다. 플레이어는 무언인가? 어떤 특징이 있고, 무엇이 가능하며 무엇이 가능하지 않은가 등. 그러한 언어를 DDD에서는 유비쿼터스 언어Ubiquitous language라고 부르며, 도메인모델과 대응하게 됩니다. 이러한 도메인 모델dms '구현'과 연결 시킴으로서 소프트웨어의 최종 목적을 달성하게 됩니다. RPG의 경우 "최종 보스를 물리치고 세계에 평화를 가져온다"라고 하는 것이 목적이 됩니다.(당연하지만 최종 목적을 달성하기 위해서 크고 작은 목적들이 먼저 달성해야 합니다.)
그만큼 중요한 요소이지만, 실제 개발 현장 에서는 여러가지 이유로 그러한 지식이 구현 코드 속에 매몰되어 버리기 십상 입니다. 코드를 읽어도 설계의 의미가 좀체 와 닿지 않는 것도 무리가 아니라는 생각이 듭니다. 그렇다면 도메인 지식을 반영한 소프트웨어 제작을 하기 위해서는 어떻게 해야 될까요? 이를 위해 제시된 한 가지 방법론이 DDD입니다. (이렇게 말해 놓고 보니 DDD가 만능처럼 들리지만, DDD는 복잡한 문제를 도메인 모델을 통해 해결하기 때문에 간단한 문제에는 적합하지 않습니다. 우선은 문제에 적합한 해결 방법을 선택 하는 것이 중요합니다.)
왜 DDD를 사용해야만 하는가?
저희 팀에서 DDD를 사용하는 이유는 다음과 같습니다.- 어느 정도 복잡한 문제를 다루는 기반 시스템 이었기 때문에, 비용 대비 효과에 대한 기대가 가능하다고 생각했다.
프로토타입이라던지, 그다지 공을 들이고 싶지 않은 시스템에는 적합하지 않은 것이 사실이지만, 제대로 만들어서 지속적으로 성장 시켜 나가는 시스템에는 적용 가능하다고 판단했습니다.
- 엔지니어 뿐만이 아니라 디렉터나 기획등 비엔지니어가 포함된 팀 멤버가 유비쿼터스 언어를 이용해 같은 눈높이로 시스템을 만들어 나가고 싶었다.
개발측과 기획측이 대립하지 않도록 유비쿼터스언어를 정해, 팀 멤버 전원이 도메인 위에서 문제해결이라는 최종 목적을 공유하고 싶었습니다.
- 내가 팀 멤버가 되어버려서 반 강제적으로 DDD로 개발하는게 되어버렸다...라는게 사실 가장 큰 이유임. (죄송합니다...)
지금부터 가상의 프로젝트를 예로 실제 모델이 만들어지는 과정을 설명하겠습니다.
시나리오로부터 모델을 만들어낸다
DDD를 시작하려면 어디서 부터 손을 대야 할까요?DDD에는 Model Exploration Whirlpool이라고 하는 모델을 정제하기 위한 프로세스가 정의되어 있습니다. (이 프로세스는 도메인주도설계(에릭 에반스)에는 실려있지 않습니다.)
그림을 클릭하면 확대해서 보실 수 있습니다 |
역자주 : Model Exploration Whirlpool에 대하여이 그림에 의하면, DDD는 시나리오를 정의 내리는 것 으로 부터 출발하고 있습니다. 시나리오는 모델의 사용 방법을 표현한 것 입니다.
문제 해결을 위한 도메인 모델은 우리가 만드는 것 이 아니라 발견 하는 것 이다. 마치 문제 그 자체는 우리의 인지와 상관없이 처음부터 그 자리에 있었던 것처럼 말이다. 그래서 DDD의 저자인 Eric Evans는 자신이 제안한 DDD의 표준 프로세스에 Design(설계)가 아니라 Exploration(탐구)라는 단어를 선택했다. 즉, 도메인 모델 자체는 우리의 인식의 경계선에서 관찰하고 인지하여 그것을 언어로 표현하는 것 이므로 우리는 이러한 반복 적인 탐구 과정을 통해 모델을 정제해 나가는 것 으로서 보다 정확한 모델을 얻는 것이 가능해 진다. 이러한 이유로 처음부터 완벽한 모델을 얻기는 쉽지 않다. 여기서 발상의 전환이 필요해진다. 처음 얻어지는 모델은 틀린 것일 수 있다는 가정 하에 일을 진행 시키는 것 이다. 내가 틀릴 수 있다는 가정하에 반복적으로 검토하며 개선 시켜 나가는 작업은 피터드러커나 조지소러스도 비슷한 사고의 프레임워크를 제시해 나가고 있으며 기본적으로 반복형 개발 프로세스에 기반한 애자일 개발 프로세스의 사상과도 일치한다.
예를 들어, "헌터가 몬스터를 사냥한다"는 게임의 도메인을 생각해 본다면 다음과 같은 시나리오가 생각되어집니다.
- 헌터는 무기를 사용하여 몬스터를 공격 하는 것이 가능하다.
- 헌터는 아이템을 사용하는 것이 가능하다.
- 헌터는 다른 헌터 에게 아이템을 건네는 것이 가능하다.
정말 일부분이긴 하지만 도메인의 세계가 보입니다. 적어도 '헌터', '몬스터', '아이템'은 모델의 후보가 될듯 합니다.
유비쿼터스언어를 찾는 방법
시나리오라고 말한다면 확 와 닿지 않을지도 모릅니다. 처음에는 저도 그랬습니다. 게임의 경우 어느정도 세계관이 정해져 있어 모델을 상상하기가 쉽지만, 업무 시스템의 경우에는 전문지식을 가진 사람(DDD에서는 도메인 전문가라고 부릅니다)이라던지, 그 분야의 전문서적 으로 부터 힌트를 얻는 편이 좋을 듯 합니다. 또, 새로운 분야의 도메인에는 도메인 전문가 자체가 존재하지 않는 경우도 있습니다. 그러한 경우, 스스로가 소프트웨어 전문가 이면서 도메인 전문가라는 마음가짐이 필요합니다.시나리오에 사용되는 언어는 실제로는 애매한 표현인 경우도 있으므로, 그것도 팀 내에서 의논하여 언어를 통일해 나갑니다. "헌터란 무엇인가?"라는 질문에 대한 대답을 만듭니다. 즉, 유비쿼터스 언어의 정의입니다. 설계라는것은 국어의 문제로군요! DDD에서 가장 어려운것이 바로 이 언어의 정의 이기도 합니다. 유비쿼터스 언어를 정해가면서 화이트보드같은것을 사용해 모델의 이미지를 공유해 나갑니다. 이런 이미지 입니다.
이러한 이미지의 공유가 가능해지면, 바로 코드작성이 가능합니다. 코드의 예는 잠시후에 소개하겠습니다.
역자주 : 모델을 표현하는데에는 여러가지 표기법이 있어 왔지만 현재는 UML로 거의 통일된 상태이다. 표준언어인 UML로 모델을 작성하는 경우 적은 학습비용으로 오해없이 모델에 대한 이미지를 공유하는것이 가능해 지므로 개발자가 아아니더라도 디렉터나 기획자등 프로젝트 이해관계자 전원이 UML의 사용법을 숙지하는 약간의 노력만으로 커뮤니케이션에 소요되는 비용을 크게 줄일 수 있다. UML을 익히는 데에는 숙련자에 의한 일일 워크샵이 가장 효과적으로 각각의 UML요소들에 대해 잘게 나누어 설명, 실습, 쪽지시험을 한 사이클로 묶어 시행하는것이 참가자들의 집중도와 이해도를 높이는데 크게 도움이 될 것이다.
역자주 : 유비쿼터스언어를 만드는 가장 좋은방법
당연히 워크샵이다. 교외로 놀러가서 술마시라는 이야기가 아니다. 프로젝트 관계자를 모아놓고 만들어 가고자 하는것에 대해 정의해 나가자. 의외로 많은 부분들에 대해서 다른 생각을 가지고 있음을 알게 될 것이다. 프로젝트 사용 언어에 대해 정의내리고 정리해 나가자. 기획자나 개발책임자는 의논의 토대가 될 기본적인 모델이나 용어 일람을 미리 준비하자. 너무 완벽을 추구하지는 말되 각각의 워크샾은 작던 크던 의미있는 성과를 낼 수 있어야 한다. 처음부터 문서화에 공들이지는 말자. 우선은 화이트보드에 적힌 내용을 휴대폰 카메라로 찍고 이것을 공유하는것으로 충분하다.
모델을 코드에 반영한다
서론이 길어졌습니다만, 이제부터 여러분이 좋아하는 구현에 대한 이야기 입니다. 여기서부터는 유비쿼터스언어로 표현한 모델을 실제로 반영하기 위한 방법인 '모델주도설계'를 간단하게 설명하겠습니다. 구현에 대한 이야기를 꺼내면, 프레임웍 이라던지 DB억세스 같은 것이 떠오르시겠지만 여기서는 일단 잊어주시기 바랍니다. (덧붙이자면, 저희들은 DDD를 효과적으로 구현하기 위한 라이브러리인 scala-dddbase를 이용하고 있습니다. 이제부터 등장하는 'Entity','Identity'등은 scala-dddbase의 것을 참조하고 있습니다.)
식별을 위한 엔티티Entity
우선은 엔티티부터 설명하겠습니다. 엔티티는 (동일성의) 식별을 목적으로 하는 모델입니다. 예를 들어 은행 계좌로의 '송금'은 일시, 계좌번호, 금액이라는 속성만 가지고는 '송금'의 식별이 완벽하게 보장되지는 않습니다. 이러한 '송금'의 식별이 불가능 하다는 것은 상식적으로 있을 수 없는 일이기 때문에 '송금'이라고 하는 개념은 식별의 대상이 됩니다. 즉, '송금'은 엔티티 입니다.
엔티티의 임무는 동일성을 보증하는것 입니다. 소속하는 속성이 변화하였다고 해도 동일성을 보증합니다. 예를 들면, '사람'이라는 엔티티의 속성인 주소, 신장, 체중 등은 때에 따라 변화합니다. 이름이 변경될 가능성도 있습니다. 어린 시절의 속성은 어른이 되면 변화할지도 모릅니다. 하지만, 그 '사람'자체는 동일 인물로서 보는 모델이 엔티티인 것입니다. 표현을 달리하자면, 엔티티는 아이덴티티를 가지고 있다고 말할 수 있습니다. 구현에 대한 이야기를 하자면, 이 아이덴티티는 불변의 식별자(ID)로서 표현하는것이 일반적입니다.
컬럼 : 아이덴티티의 재이용에 대해서
아이덴티티에 대한 이야기는 상당히 심오합니다. 사람을 예로 들자면 태어나는 순간부터 아이덴티티가 발생하여 죽은 후 고인이 되어도 그 아이덴티티는 쭉 남습니다. 그러니까 제가 스티브잡스로 변하는것은 불가능하다는 이야기 입니다. 당연하지요. 즉, 엔티티의 라이프사이클이 끝났다 하더라도 아이덴티티는 재 이용 불가 하다는 것을 의미 합니다. 예를 들어, 재이용되어지는 "전화번호"로 사람을 구분하는 식별자로 사용한다고 하는 경우 큰 혼란을 가져 올 수 있습니다. 사용하던 사용하지 않던, 이러한 리스크에 대한 고려 위에서 손익 계산이 이루어져야 합니다.
이 게임의 "헌터"는, 케릭터의 이름은 동일하더라도, 각각 개별적으로 구분되어야 할 필요가 있기 때문에 엔티티 입니다. 아마 몬스터도 사냥중 같은 종류의 몬스터가 두마리 이상 등장할 경우 이를 구분해야 되기 때문에 엔티티가 된다고 생각합니다.
Hunter엔티티의 구현예는 다음과 같습니다. Hunter엔티티는 Entity를 상속합니다. 실별자에는 Entity트레이트의 identity필드가 이용되어집니다. 기본적으로는 식벌자를 보면 구분이 가능합니다. 하지만, 컬렉션프레임웍의 엔티티 등록에 대한 이용성을 생각하여 equals,hashCode는 엔티티가 가진 속성에 기초하여 구현 하는 것이 아닌, 식별자(identity)가 동일한지 아닌지 만을 구현 하는 것이 바람직하다고 생각합니다.
trait Hunter extends Entity[HunterId] {
// val identity: HunterId
val name: HunterName
// 몬스터를 공격한다
def attack(methodType: MethodType.Value, monster: Monster): Try[(Hunter, Monster)]
}
엔티티를 발견하는 방법
팀맴버와 시나리오에 기초하여 (아니면 유저스토리를 힌트로 하여) 엔티티에서 찾아나갑니다. 화이트보드로 가서, 팀 맴버와 의논하며 대략적인 이미지가 그려지면 바로 토레이트를 작성합니다. 처음에는 화이트보드에 모델을 그린 후에 코드를 작성하였지만 이후에 코드와의 동기화가 귀찮게 되어 버려 바로 트레이트 코드가 그림을 대신하게 되었습니다. 또, 테스트시에 Mock화 하는 경우에 구현이 번거로워 지는 경우가 있으므로 트레이트를 먼저 작성하는것을 우선하고 있습니다.
예를 들자면 다음과 같습니다.
// 헌터객체
object Hunter {
def apply(identity: HunterId, name: HunterName): Hunter =
new HunterImpl(identity, name)
}
private class HunterImpl(val identity: HunterId, val name: HunterName)
extends Hunter {
def attack(methodType: MethodType.Value, monster: Monster): Try[(Hunter, Monster)] = {
// ...
}
}
역자주 : 트레이트란?
스칼라(Scala)에서 사용하는 오브젝트의 한 종류. 한마디로 정의하자면 자바8에서도 가능하게 된 실행코드를 지니는것이 가능한 인터페이스라고 할 수 있다. 다만 자바8의 인터페이스는 맴버변수의 정의가 불가능한 반면, 스칼라의 트레이트는 가능하다. 자바에서는 인터페이스를 구현(implemnets)한다고 표현하는 것에 대하여 트레이트는 믹스인(mixin)한다고 표현한다.
값을 설명하기위한 값 객체Value Object
다음은 값 객체 입니다. 이것은 엔티티와는 다르게 동일성은 신경 쓰지 않고 값 자체를 설명 하는 것을 목적으로 하는 모델입니다.헌터가 사용하는 아이템으로 설명하자면, 회복약 등의 아이템은 어떠한 종류의 아이템인가, 그 효과는? 몇 개 있는가? 에 대한 것 밖에 관심이 없습니다. 이러한 특징을 가진 모델이 값 객체에 속하게 됩니다.
아이템을 구현하는 코드로 표현하면 다음과 같은 이미지 입니다. 동일성을 나타내는 식별자는 없습니다. 그 대신, 아이템이 가지고 있는 속성이나 효능에 대한 기술이 있습니다.
// 값 객체
sealed trait Item {
val name: String
def beUsedBy(hunter: Hunter): Try[Hunter]
}
case class Analepticum() extends Item {
val name = "analepticum"
def beUsedBy(hunter: Hunter): Try[Hunter] = {
// 헌터를 회복시킨다
}
}
case class Antidote() extends Item {
val name = "antidote"
def beUsedBy(hunter: Hunter): Try[Hunter] = {
// 헌터를 해독시킨다
}
}
class HunterImpl(
val identity: HunterId,
val name: HunterName,
val items: Set[Item]
) extends Hunter {
// 헌터가 아이템을 사용
def use(item: Item): Try[Hunter] = {
require(items.exists(_ == item))
// 아이템의 작용을 헌터에 기술하고싶은경우는
// Visitor패턴을 사용하면 된다
item.beUsedBy(hunter)
}
}
다른 예를 하나 살펴봅시다. 헌터의 이름을 나타내는 Hunter엔티티의 name속성은 HunterName형 입니다. 이름 자체로는 동일성이 보장되지 않으며 값 만이 표현됩니다. 예를들면, 어떤 헌터가 이름이 kato라고 했을 때 그것이 같은 이름인지 다른 이름 인지 하나하나 신경쓰지 않습니다. 아이템과 마찬가지로 이름 또한 값 객체로서 표현 가능합니다.
스칼라로 구현을하는경우에는 간단히 case class라고 하는 선언을 하는 것 만으로 충분합니다. case class의 equals, hasCode는 엔티티와는 다르게 생성자 인자값이 같은 값 인가에 의해 구현됩니다. 즉, 가지고 있는 속성이 같은 것 인가 아닌 가로 판단합니다.
trait HunterName {
val firstName: String
val lastName: String
}
object HunterName {
def apply(firstName: String, lastName: String): UserName =
new HunterNameImpl(firstName, lastName)
}
private case class HunterNameImpl(firstName: String, lastName: String) extends Hunter
|
값 객체를 발견하는 방법
엔티티를 찾아냈다면, 그것이 가지고 있는 속성을 열거해나가게됩니다만, 최초의 헌터는 다음과 같은 이미지일지도 모릅니다. 헌터의 성별이나 랭크를 각가 보유하고 있는 형태입니다.
trait Hunter extends Entity[HunterId] {
val firstHunterName: String
val lastHunterName: String
val hunterRank: Int
val hunterRankPoint: Int
val nextHunterRankPoint: Int
/// …
}
|
- 헌터의 이름은, 성과이름이 모두 포함됩니다.
- 헌터의 랭크에는 현재 랭크와 현재의 랭크포인트 다음랭크에 올라가기위한 포인트가 포함됩니다.
유비쿼터스언어에는, 성과이름이 포함되므로, 다음과 같이 통합해서 하나의 값 객체 HunterName을 만들면 좋을것 같습니다(이러한 개념이 아직 없다면, 팀맴버와 의논하여 유비쿼터스 언어를 만들어 둡시다).
헌터의 랭크에 대해서도 마찬가지입니다. 그렇게하면 Hunter 엔티티도 심플하게 되어 보다 응집도가 높은 값 객체가 구현됩니다.
// 이름에 대한 값 객체
trait HunterName {
val firstName: String
val lastName: String
}
// 랭크에대한 값 객체
trait HunterRank {
val rank: Int
val point: Int
val nextRankPoint: Int
}
// 응집도가 높은 값객체를 지닌 엔티티
trait Hunter extends Entity[HunterId] {
val name: HunterName
val rank: HunterRank
//
}
행위을 표현하는 서비스Service
마지막은 서비스입니다. 서비스는 서비스는 어떤 모델인가 하면, 행위(DDD번역서에서는 "연산"이라고 표현함)을 표현하는 모델입니다. 행위중에는 엔티티나 값 객체에 속하기엔 부자연스러운 표현이 되버리는 것이 있습니다. 그러한경우에는 서비스로서 표현합니다.
예를들어 헌터가 다른 헌터에게 아이템을 건네는경우를 생각 해 봅시다. 헌터가 자신으로부터 다른 헌터에게 아이템을 넘기는경우는 다음과 같은 모될이 되지 않을까요?
class Hunter {
// 성공한경우에는, 각자 상태가 업데이트 되면서
// Success(fromHunter, toHunter)를 반환한다.
def transferItems(items: Seq[Item], to: Hunter): Try[(Hunter, Hunter)] = {
// this의items로부터 인수items를 감소시킨다.
// to.items에 인수items를 늘린다.
// 이것은 주는쪽에서 하는것인가?
}
// ...
}
그리고, 넘겨주는 쪽에서는 받는 쪽에서 해야 할 동작도 함께 넘겨줍니다. 하지만, 주는 쪽이 받는 쪽의 아이템 주머니에 추가하는 행위는 자연스러운 것일까요? 이러한 행위가 헌터에게는 어울리지 않는다고 판단하는 경우에는, 아직 발견하지 못한 다른 모델은 없는지 찾아봅시다. 그렇게 해도 적당한 주인을 찾을 수 없는 경우 어떻게 하는 게 맞는 걸까요? DDD에서는 또다른 서비스를 정의하고 그곳에 행위를 소속시킵니다. 서비스는 행위 만을 표현하는 모델로서, 의외로 인위적인 이미지가 강합니다.
아이템을 넘겨주는 서비스 코드의 예는 다음과 같습니다.
object ItemService {
def transferItems(items: Seq[Item], from: Hunter, to: Hunter):
Try[(Hunter, Hunter)] = Try {
require(from.has(items))
(from.withoutItems(items), to.withItems(items))
}
}
지금까지의 모델은 "식별(구분)", "값의 설명"을 위한 모델이었기 때문에 명사적인 표현을 했습니다만, 서비스는 행위를 나타내는 모델이므로 동사로 표현하는 모델입니다. 또, 행위의 이름은 유비쿼터스 언어와 연결 시킬 필요가 있어, 입출력을 위해 취득하는 모델은 엔티티나 값 객체에 있어야 합니다. 이들 객체가 상태를 가지기 때문에, 원칙적으로 서비스는 Stateless 이어야만 한다고 말해집니다.
컬럼 : 서비스로 할 것인가 말 것인가
적당히 갈 곳이 없어 공중에 떠버린 "행위"를 엔티티나 값 객체에 위치 시킬 것 인가, 서비스에 포함 시킬 것 인가는 상당히 어려운 부분입니다.
서비스를 활용하면 도메인 모델로부터 행위를 뺏어와 도메인모델 빈혈증을 일으킬 가능성이 있습니다. 하지만 억지로 가공의 모델을 만든다던지, 기존 모델에 붇여버린다던지 하는것도 문제가 있습니다. "이렇게 하면 절대로 된다"라는 만능 해결책은 잘 없기때문에 제 자신또한 늘 고민하는 부분입니다. 어느쪽인가의 설계원칙에 위배된다 하더라도, 유비쿼터스언어와 연결되는 모델링을 해 나가는것이 현실적이라고 생각합니다.
그렇다고는 해도, 인위적인 절차로서의 서비스가 아닌 엔티티나 값 객체등의 오브젝트로서 표현 할 수 없는 것이 대상 이라고 생각되어집니다. 부연 설명을 하자면, DCI라고 하는 프로그래밍 패러다임이 하나의 수단이 될 수도 있다고 생각합니다. DCI에는 유스케이스에 대응하는 역할과 행위를 도메인모델에 투영 하는것 으로서 유저의 멘탈모델에 접근시키는 방식이 있는 듯 합니다. 예를들면, 아이템을 건네는 장면을 생각해 봤을 때, "아이템을 건넨다"라고 하는 장면(유스케스)에 대해서 헌터(도메인모델)각각에게 "건네 주는 자(Sender)"나"넘겨 받는 자(Receiver)"라고 하는 행위를 지닌 역할(롤)이 부여된다 라고 하는 사고방식입니다. 관심있으신분께는 DCI아키텍쳐 - Trygve Reenskaug and James O. Coplien 을 읽어보실것을 추천합니다.
도메인모델을 그룹화하는 모듈
모듈은 도메인 모델을 그룹화 하기 위한 것 입니다. 이것도 모델의 한종류입니다. 객체지향언어에서 모듈을 구현하기위해서는 패키지가 주로 사용됩니다(패키지가 없는 언어라도 클래스명의 일부로서 모듈명을 넣는것에의해 구현이 가능합니다).
모듈을 어떤 단위로 그룹화 할 것 인가에 대해서는 여러가지 패턴이 있습니다만, 가장 유명한 것은 다음과 같이 엔티티, 값 객체, 서비스등의 오브젝트 종류에 의해 모듈을 분리하는 패턴입니다(이 외에도 XXXLgoci, XXXBean과 같은 패턴으로 분리하는 경우도 있는데 이러한 분류 방법을 기술(技術)주도 패키지라고 말합니다).
- net.gree.xxx.domain.enitity
- Hunter, Monster
- net.gree.xxx.domain.valueobject
- HunterName, HunterRank, Item, ...
- net.gree.xxx.domain.service
- ItemService,...
DDD의모듈에서는 다음과 같이 분류합니다. 도메인오브젝트는 유비쿼터스언어에 관련된 세계입니다. 모듈의 이름도 예외는 아닙니다. 위의 예에서는 모쥴명이 entity, valueobject, service인것처럼 말입니다. 그러한 언어는 유비쿼터스언어에는 없기 때문에 명명할 수가 없습니다. 여기서 헌터관련 모듈에는 hunter모듈, 몬스터 관련 모듈에는 monster모듈이라고 부릅니다(도메인모델군의 상위개념이 발견된다면, 그 이름을 쓰는편이 좋을지도 모릅니다.)
- net.gree.xxx.domain.hunter
- Hunter, HunterName, HunterRank
- net.gree.xxx.domain.monster
- Monster
- net.gree.xxx.domain.item
- Item,ItemService
도메인모델은 유비쿼터스언어와 매핑된다.
저또한 익숙해지기 전에는 오브젝트의 종류에 따라서 그룹핑을 했었습니다. 정확히는 언떤게 불편한것인지를 몰랐었다고 하는게 정확한 표현이겠지요. 하지만, 잘 생각해보면 개념적으로 같은 그룹에 속한 도메인모델을 놓고봤을때 복수의 패키지를 오가면서 모델의 이미지를 연결하고 있다는사실을 깨달았습니다. 게다가 더더욱 나쁜것은, 패키지간의 의존관계가 단단하게 결합되어지고 말았다는 사실입니다. 이러한 결과, 테스트 코드 또한 작성하기가 어려운 상황이 되어버렸습니다. 본래 구현레벨의 흐름상에는 표현되지 않는, 도메인모델을 억지로 그룹핑을 하게되면, 이러한 폐해가 발생할 가능성이 잇습니다. 도메인모델은 유비쿼터스언어로 그루핑을 하도록 합시다.
모델구현에 있어서의 주의점
모델에 대한 설명의 마지막으로서, 주의해야할 사항이 한가지 있습니다. 도메인모델의 클래스명, 속성명,행위명은 모두 유비쿼서스언어와 대응해야한다는점을 잊지 말아주세요. 반대로 말하자면, 프로그래머들만 알아듣는 구현상의 언어가 등장하게 되면 어떻게 해서든지 저항해 나가야 합니다. 도메인모델이라는것은 그러한 모델입니다. 이것을 잊어버리면 기껏 팀 내부에서 정의해 놓은 유비쿼터스언어가 쓸모없게 되어버리고 맙니다.