[번역] Code I like (I): Domain-driven boldness

구경회·2022년 10월 23일
1
post-thumbnail

이 글은 원 저자분의 허락을 받아 번역했습니다. 훌륭한 글을 적고 번역을 허락한 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에 첨부한 설명이 있다. 복잡한 시스템을 생각하기에 단순한 텍스트는 훌륭한 시발점이다. 사전은 이 일을 하기에 훌륭한 동반자이고.


notes.md

입력은 Analysis::InboundEmail이다. 나중에 좀 더 다양한 요소를 다루려면 새로운 엔티티를 만들면 될 것이다.

Analysis는 여러 Analysis::Rules를 포함한다. Rule은 Analysis::InboundEmail을 인자로 받고 Analysis::Insights의 리스트를 반환한다.

Analysis의 결과는 Analysis::Insights를 여러 개 담고 있는 Analysis::Result 객체다.

분석 결과가 올바르지 않으면 결과를 ActionMailbox::InboundEmail와 같이 저장해둔다. 항목이 만들어지기도 전에 작업할 수 있을 거니까

AnalysisInsight는 식별 가능한 코드로 AnalysisInsightDecision를 갖는다. bounce, spam 중 하나의 유형하고 크기를 갖는다.

규칙 집합의 집계 결과가 1보다 크면 그 AnalysisInsightDecision을 결과로 삼는다.


Description in Pull Request.md

다음 4개의 기본 엔티티를 만들었어요.

  • 분석 Rule은 이메일에 대해 Insight를 리턴한다.
  • Insight는 action의 종류를 정한다. (지금은 :ok, :reject, :spam 세 종류가 있음) 분석 결과를 되돌아볼 수 있도록 가중치랑 기타 속성을 저장한다.
  • Analysis는 여러 Rules를 갖는다. 분석 수행은 Rule들이 반환한 여러 Insight를 종합해 Result를 반환한다.
  • Result는 분석의 결과를 그룹화한다.

ResultActionMailbox::InboundEmail과 polymorphic association를 가져요. 이메일은 ok가 아닐 때만 저장해요. Receipt 계층에서 추가하는 것도 고민했지만 inbound 수준에서 저장하는게 동일 계층에서 반송할 수 있으므로 그게 더 낫다고 생각했어요.

바로 활용할 건 아니지만 여러 가중치를 통해 퍼지추론을 할 수도 있을 거예요. 분석을 수행하면 주어진 kind에 대해 가중치의 합이 1.0이 넘지 않으면 모든 규칙을 순차적으로 실행할 거예요. 지금, 우리 insight들은 1.0의 가중치를 가져요.


HEY와 Basecamp 모두 첫 커밋부터 도메인 주도 설계에 열심이었다. 물론 털어서 먼지 한 톨 안 나오지는 않을 것이다. 하지만 그들의 코드베이스를 읽는 건 즐겁다. 좋은 도메인 모델을 만드는 것은 많은 책들의 주제이다. 내가 여기서 배운 것은, 도메인을 대담하게 표현하라는 것이다.

이게 내가 좋아하는 코드들에 대한 첫 번째 게시글이다.


Photo by Brett Jordan on Unsplash

profile
즐기는 거야

0개의 댓글