DDD · Architecture

Factory는
어디에 넣을지 알고 있었다

몇 년 차이를 두고 만든 두 코드베이스가 Aggregate의 ID를 서로 다른 곳에서 만든다. 그러면 어느 쪽이 DDD를 제대로 따르는지 묻고 싶어진다. 그런데 원저에는 두 코드베이스의 관례보다 더 구체적이고 흥미로운 답이 있었다.

한 코드베이스는 Aggregate의 식별자를 Aggregate 생성자 안에서 만든다. 평범하게 UUID를 호출할 뿐이고 Domain 계층 바깥의 어떤 것에도 기대지 않는다. 다른 코드베이스는 먼저 Application 계층에서 Repository 메서드를 불러 식별자를 받아 오고, 그렇게 만든 ID를 Aggregate를 조립하는 Factory에 넘긴다. 하는 일은 같다. 하이픈을 뺀 UUID v4일 뿐, 그 이상 특별할 것도 없다. 다만 그 일을 하는 자리가 구조적으로 다르다. 그러면 어느 쪽이 "올바른" Domain-Driven Design인지 궁금해진다. 이 질문에는 인용할 수 있는 구체적인 답이 있고, 그 답은 두 코드베이스의 관례가 암시하는 것과 다르다.

두 답을 나란히 놓고 보면

생성자 버전은 이렇다. Aggregate는 만들어지는 순간 ID까지 갖춘 완전한 객체가 되고, 다른 무엇에도 기대지 않는다.

class Order {
  readonly orderId: string
  constructor(params: { orderId?: string; ... }) {
    this.orderId = params.orderId ?? generateId()
  }
}

Factory 버전은 같은 값을 한 단계 더 거쳐 보낸다. Application이 Infrastructure에 ID를 달라고 하고, 받은 ID를 Factory에 넘긴다.

// application layer
const account = accountFactory.create({
  ...command,
  id: await accountRepository.newId(),
})

둘이 만드는 값은 같은 종류다. 어느 쪽도 그 값을 위해 데이터베이스에 가지 않는다. 열어 보면 newId()는 uuid.v4()를 Repository 메서드로 감싼 것뿐이고, 왜 그렇게 감쌌는지는 코드 어디에도 설명이 없다. 겉으로는 생성자 버전이 더 깔끔해 보인다. Factory 버전은 그저 시대를 못 따라간 코드베이스라고 보고 싶어진다.

원저에는 뭐라고 적혀 있나

Eric Evans의 Domain-Driven Design(2003)은 Factory를 다루는 장에서 이 문제를 직접 다룬다. 식별자를 배정하는 책임이 어디에 있어야 하느냐를 묻는 절이 따로 있다.

"프로그램이 식별자를 배정하는 경우, Factory가 그것을 통제하기 좋은 자리다. 고유한 추적 ID를 실제로 만드는 일은 보통 데이터베이스 'sequence'나 다른 인프라 메커니즘이 맡지만, Factory는 무엇을 요청해야 하고 그것을 어디에 넣어야 하는지 알고 있다." (원문 "When the program is assigning an identifier, the Factory is a good place to control it. Although the actual generation of a unique tracking id is typically done by a database 'sequence' or other infrastructure mechanism, the Factory knows what to ask for and where to put it.")

거의 그대로 두 번째 코드베이스의 모양이다. Factory는 식별자를 직접 만들지 않는다. 대신 바깥 메커니즘에 식별자를 요청해야 한다는 것, 그리고 받은 식별자를 어디에 넣을지를 안다. 원저는 생성자가 다 알아서 하는 방식을 기본으로 소개하지 않는다. 원저가 차근차근 설명하는 건 Factory가 중간에서 조율하는 방식이다.

이건 판단이 갈릴 문제가 아니다

원저가 모호해서 어느 쪽으로 읽어도 말이 된다면 얘기가 달라진다. 하지만 이 대목은 모호하지 않다. "누가 식별자를 배정하는가"에 답하려고 쓴 구절이고, 답은 Factory가 조율하고 실제 생성은 인프라에 맡기는 방식이다. 그렇게 하는 코드베이스는 관례에 뒤처진 게 아니다. 오히려 원저에 적힌 것에 더 가깝다.

원래 답은 왜 인프라를 가리켰나

이유는 구절의 나머지 부분을 보면 알 수 있다. 2003년에 "고유한 추적 ID를 실제로 만드는 일"은 대부분의 시스템에서 데이터베이스 sequence를 뜻했다. 데이터베이스가 직접 들고 있다가 요청할 때마다 하나씩 내주는 자동 증가 카운터다. 데이터베이스에 묻지 않고는 그 값을 얻을 수 없었다. 첫 번째 코드베이스의 UUID 버전처럼 혼자서 다 해결하는 생성자는 짤 수가 없었다는 얘기다. 원저가 말하는 Factory의 역할은 그 인프라 왕복을 도메인 모델의 나머지 부분에 드러내지 않는 것이었다. Factory가 "무엇을 요청해야 하는지 알고" 있으니 다른 객체는 몰라도 된다.

무엇이 달라졌나

UUID에는 이 문제가 없다. 하나 만드는 데 바깥의 누구와도 맞춰 볼 필요가 없다. 올려야 할 sequence도, 왕복도, 충돌을 막으려고 지켜야 할 공유 카운터도 없다. 원저가 식별자 배정을 바깥 메커니즘과 통하는 Factory에 맡긴 이유는, 식별자가 바깥 메커니즘을 아예 필요로 하지 않게 되는 순간 사라진다. UUID 함수를 직접 부르는 생성자는 DDD가 요구하는 단계를 빼먹은 게 아니다. 식별자가 인프라 없이도 만들어질 수 있게 된 뒤에야 가능해진 단순화다.

더 일반적으로 보면

특정한 기술 제약을 풀려고 만든 설계 규칙은 그 제약이 사라진 뒤에도 계속 지켜지는 일이 많다. 누가 다시 따져 보고 유지하기로 해서가 아니다. 패턴이 이유보다 오래 살아남았고, 그게 왜 거기 있는지 물어볼 계기가 아무에게도 없었을 뿐이다. Factory가 중간에서 조율하는 버전도 틀린 건 아니다. 식별자 생성이 당연히 인프라의 일이던 시절의 지침을 충실하게 구현한 것이다. 이런 종류의 식별자에 한해서는 그 시절이 이미 지나갔다는 걸 알아챌 기회가 없었을 뿐이다.

둘 다 틀리지 않았다

Factory가 조율하는 코드베이스는 프로그램이 식별자를 배정할 때 원저가 설명하는 방식을 그대로 따르고 있다. 생성자만 쓰는 코드베이스는 원저가 금지한 적 없는 방식을 쓰고 있고, UUID 덕분에 인프라를 거치는 단계가 필수에서 선택으로 바뀌면서 그 방식을 정당화하기가 쉬워졌다. 처음에는 한쪽은 규칙을 지키고 다른 쪽은 벗어난 것처럼 보였다. 따져 보니 둘 다 아니었다. 한쪽은 이제는 없는 제약을 위해 만든 패턴을 물려받았고, 다른 쪽은 누가 선언하지 않아도 그 패턴이 필요 없게 된 것이다.

더 볼 자료

Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2003), 6장 "Factories" · backend-service-playbook/docs/architecture/aggregate-id.md(같은 백엔드 설계를 5개 언어로 나란히 구현해 둔 내 예제 프로젝트의 생성자 버전) · nestjs-rest-cqrs-example/account-factory.ts(Factory가 조율하는 버전)