이 글은 원 저자분의 허락을 받아 번역했습니다. 훌륭한 글을 적고 번역을 허락한 Jorge Manrubia 씨에게 다시 한 번 감사드립니다. 원문은 https://world.hey.com/jorge/code-i-like-i-domain-driven-boldness-71456476 에서 확인하실 수 있고, 원문 하단 링크에서 구독도 할 수 있습니다.
3년 전 쯤, 37signals에서 일하기 시작했을 때 가장 먼저한 일 중 하나는 Basecamp 레포지토리를 클론받은 것이었다. 레포지토리를 여기저기 둘러보다 이런 메소드를 맞닥뜨렸다.
역주: 37signals는 basecamp랑 hey.com을 만든 회사로 RoR의 저자인 DHH가 재직 중입니다.
module Person::Tombstonable
...
def decease
case
when deceasable?
erect_tombstone
remove_administratorships
remove_accesses_later
self
when deceased?
nil
else
raise ArgumentError, "an account owner cannot be removed. You must transfer ownership first"
end
end
end
Basecamp의 사용자는 특정 유형(사용자, 클라이언트 등)을 나타내는 delegated type attribute를 가진다. 계정에서 사용자를 제거하면, Basecamp는 해당 사용자를 플레이스홀더로 교체해 관련한 데이터들이 변경되지 않고 잘 작동하게 한다.
도메인 중심 설계나 도메인을 잘 반영하는 코드의 중요성은 잘 알고 있었다. 하지만 이렇게 그 개념을 잘 실현한 코드는 보지 못했다. 사용자를 제거할 때 플레이스홀더로 교체하는 정도를 생각했지만, 비석을 세운다는게 훨씬 나았다.
객관적으로 의도가 명확히 드러났고 간결했다. 주관적으로는 개성이나 영혼까지 느껴질 정도였다. 코드가 그런 것들까지 가질 수 있을까? 그럴 수 있다고 본다. 적절히 사용한다면 코드베이스를 상당히 개선할 수 있을 것이다. 머릿 속에 전구가 켜지는 것 같았다.
다른 예를 살펴보자. HEY의 스크리닝 시스템이다. 내부적으로, 시스템은 다음과 같다. user
는 메일 발송처가 보낸 통관 의뢰
를 검사한다.
다시, 이런 살아 움직이는듯한 구성요소를 볼 수 있었다. 통관의뢰는 정식적인 절차를 암시한다는 점에서 단순한 요청과는 다르다. HEY에서 스크리닝 시스템은 설계상 통관 절차에 가깝다. 사용자의 허락 없이는 인박스에 메일을 보낼 수 없기 때문이다. 그래서 심사를 받아야하는 통관절차는 이 시스템이 작동하는 방법을 설명하는 아주 명확한 방법이다. 코드는 이 점을 명확히 반영한다.
class Contact < ApplicationRecord
include Petitioner
...
end
module Contact::Petitioner
extend ActiveSupport::Concern
included do
has_many :clearance_petitions, foreign_key: "petitioner_id",
class_name: "Clearance", dependent: :destroy
end
...
end
class User < ApplicationRecord
include Examiner
end
module User::Examiner
extend ActiveSupport::Concern
included do
has_many :clearances, foreign_key: "examiner_id",
class_name: "Clearance", dependent: :destroy
end
def approve(contacts)
...
end
...
end
위 예시에서 concern
의 사용은 DCI 설계 패턴에서 role
을 떠오르게 했다. DCI는 흥미로운 아이디어로 가득 차 있지만 잘 구현되지 않을 때가 많다. 이런 식의 concern
사용은 꽤 실용적인 role
의 구현이다.
개인적으로 복잡한 모델을 만들 때는 단순히 줄글로 설명하는 것을 가장 선호한다. HEY에서 이메일 분석 시스템을 개선할 때, 나는 새로운 도메인 모델의 모습에 대해 메모를 써 나갔다. 하단에 관련한 내용을 첨부했다. 상단에는 스스로 작업하면서 쓴 노트가 있고 하단에는 구현 후 PR에 첨부한 설명이 있다. 복잡한 시스템을 생각하기에 단순한 텍스트는 훌륭한 시발점이다. 사전은 이 일을 하기에 훌륭한 동반자이고.
입력은 Analysis::InboundEmail
이다. 나중에 좀 더 다양한 요소를 다루려면 새로운 엔티티를 만들면 될 것이다.
Analysis
는 여러 Analysis::Rules
를 포함한다. Rule은 Analysis::InboundEmail
을 인자로 받고 Analysis::Insights
의 리스트를 반환한다.
Analysis
의 결과는 Analysis::Insights
를 여러 개 담고 있는 Analysis::Result
객체다.
분석 결과가 올바르지 않으면 결과를 ActionMailbox::InboundEmail
와 같이 저장해둔다. 항목이 만들어지기도 전에 작업할 수 있을 거니까
AnalysisInsight
는 식별 가능한 코드로 AnalysisInsightDecision
를 갖는다. bounce
, spam
중 하나의 유형하고 크기를 갖는다.
규칙 집합의 집계 결과가 1보다 크면 그 AnalysisInsightDecision
을 결과로 삼는다.
다음 4개의 기본 엔티티를 만들었어요.
Rule
은 이메일에 대해 Insight
를 리턴한다.Insight
는 action의 종류를 정한다. (지금은 :ok
, :reject
, :spam
세 종류가 있음) 분석 결과를 되돌아볼 수 있도록 가중치랑 기타 속성을 저장한다.Analysis
는 여러 Rules
를 갖는다. 분석 수행은 Rule
들이 반환한 여러 Insight
를 종합해 Result
를 반환한다.Result
는 분석의 결과를 그룹화한다.Result
는 ActionMailbox::InboundEmail
과 polymorphic association를 가져요. 이메일은 ok
가 아닐 때만 저장해요. Receipt
계층에서 추가하는 것도 고민했지만 inbound 수준에서 저장하는게 동일 계층에서 반송할 수 있으므로 그게 더 낫다고 생각했어요.
바로 활용할 건 아니지만 여러 가중치를 통해 퍼지추론을 할 수도 있을 거예요. 분석을 수행하면 주어진 kind
에 대해 가중치의 합이 1.0이 넘지 않으면 모든 규칙을 순차적으로 실행할 거예요. 지금, 우리 insight들은 1.0의 가중치를 가져요.
HEY와 Basecamp 모두 첫 커밋부터 도메인 주도 설계에 열심이었다. 물론 털어서 먼지 한 톨 안 나오지는 않을 것이다. 하지만 그들의 코드베이스를 읽는 건 즐겁다. 좋은 도메인 모델을 만드는 것은 많은 책들의 주제이다. 내가 여기서 배운 것은, 도메인을 대담하게 표현하라는 것이다.
이게 내가 좋아하는 코드들에 대한 첫 번째 게시글이다.
Photo by Brett Jordan on Unsplash