28 Apr 2019

도메인 주도 설계 06 - 도메인 객체의 생명주기

모든 객체에는 생명주기가 있다. 한 객체는 생성되어 다양한 상태를 거친 후 저장되거나 삭제되면서 소멸한다. 다른 객체와 복잡한 상호의존성을 맺으며, 여러 상태의 변화를 겪기도 하는데 이 때 갖가지 불변식이 적용된다. 이러한 객체들을 관리하는데 실패한다면 Model-Driven Design을 시도하는 것이 쉽게 좌절될 수 있다.

00.png


도메인 객체의 관리와 관련된 문제는 아래의 두 가지 범주로 나뉜다.

  • 생명주기 동안, 무결성 유지하기
    • Aggregate
  • 생명주기 관리의 복잡성으로 모델이 난해해지는 것을 방지하기
    • Factory, Repository

도메인 주도 설계에서는 이런 문제를 해결하기 위해 세 가지 패턴이 있다.

  • Aggregate: 소유권과 경계를 명확히 정의하여 모델을 엄격하게 만들어 객체 간의 연관관계가 혼란스럽게 얽히는 것을 방지하고, 도메인 객체의 무결성을 유지한다.
  • Factory: 복잡한 객체나 Aggregate를 생성, 재구성하는 책임을 맡음으로써 그것들의 내부 구조를 캡슐화한다.
  • Repository: 영속성과 관련된 인프라스트럭처를 캡슐화하면서 영속 객체를 찾아 조회하는 수단을 제공한다.

Repository와 Factory가 도메인에서 나오는 것은 아니지만, 그것들은 도메인 설계에서 중요한 역할을 담당한다. Aggregate를 모델링하고 Repository와 Factory를 통해 객체의 생명주기 동안 그것들을 체계적이고 의미있게 다룰 수 있다.

Aggregate는 생명주기의 모든 단계에서 불변식이 유지되어야 할 범위를 표시하는 것이며, Repository와 Factory는 Aggregate를 대상으로 특정 생명주기로 이동하는 과정에 따른 복잡성을 캡슐화한다.


Aggregate (집합체)

연관관계를 최소주의 관점에서 설계하면 탐색이 단순해지고 관계를 제한하는데 어느정도 도움이 되긴 하지만, 대부분의 업무 도메인은 상호 연관관계가 복잡하므로 결국에는 참조를 통해 얽히고 설킨 객체 관계망을 추적해야 한다. 이런 과도한 관계망은 소프트웨어 설계에서 문제를 일으킬 수 있다.

일반적인 객체 모델의 연관관계망은 변경의 효과가 잠재적으로 미치는 범위를 명확히 한정해주지 않는다. 특히 동일한 객체에 여러 클라이언트가 동시에 접근하는 시스템에서는 문제가 심각해질 수 있다. 여러 사용자가 동시에 다양한 객체를 참조하거나 갱신한다면, 상호 의존 관계에 있는 객체가 동시에 변경되지 않게 해야 한다. 변경의 범위를 알맞게 제한하지 않는다면 심각한 결과가 초래될 것이다.

모델 내에서 복잡한 연관관계를 맺는 객체를 대상으로 변경의 일관성을 보장하기란 쉽지 않다. 개별 객체뿐만 아니라 그 객체가 참조하는, 서로 연관관계에 있는 객체 집합에 대해서도 불변식이 적용되어야 하기 때문이다.

이런 문제의 해법을 찾기 위해서는 도메인을 심층적으로 이해해야 하며, 특히 특정 인스턴스 사이의 변화 빈도와 같은 사항까지도 이해하고 있어야 한다. 경합이 높은 지점을 느슨하게 연결하고, 엄격한 불변식을 더욱 엄격하게 지켜지케 하는 모델을 찾을 필요가 있다.

문제의 근원은 모델에 경계가 정의되어 있지 않다는 점이다. 모델을 근간으로 하는 해법을 이용하면 모델을 좀 더 이해하기 쉬워지고 설계한 바가 더 쉽게 전달될 것이다.

다음과 같은 엄격한 체계는 그와 같은 개념에서 정수를 뽑아낸 것이다.

모델 내부에서 참조에 대한 캡슐화를 추상화할 필요가 있다. Aggregate는 한 데이터 변경의 “단위”로 다루는 연관 객체의 묶음을 말하는데, 각 Aggregate에는 루트(root)와 경계(boundary)가 있다.

  • 경계: Aggregate에 무엇이 포함되고 포함되지 않는지를 정의한다.
  • 루트: Aggregate 내 단 하나만 존재하며 특정 Entity를 가리킨다.

경계 안의 객체들은 서로 참조할 수 있지만, 경계 바깥의 객체는 Aggregate의 구성요소 중 루트만 참조할 수 있다.

Aggregate 내부에서 루트 이외의 Entity는 지역 식별성을 가지며, Aggregate 내에서만 구분된다. Aggregate 경계 밖에서는 루트 Entity 말고는 내부를 직접 들여다 볼 수 없도록 하기 때문이다.

01.png

위의 그림과 같이 Car는 외부에서 식별할 수 있는 루트 Entity이며, Aggregate 내부의 Wheel, Tire는 외부에서 바로 접근할 수 없다.

불변식은 데이터가 변경될 때마다 유지되어야 하는 일관성 규칙을 뜻하며, Aggregate를 구성하는 각 구성요소 간의 관계도 포함한다.

이런 특징을 가지는 Aggregate에 대한 트랜잭션의 규칙은 다음과 같다.

  1. 루트 Entity는 전역 식별성을 가지며, 불변식을 검사할 책임이 있다.
  2. Aggregate 경계 안의 Entity들은 지역 식별성을 지니며, Aggregate 내부에서만 유일하다.
  3. Aggregate 경계 밖에서는 루트 Entity를 제외하고는 내부의 구성요소에 대해 직접 참조할 수 없다.
    • 루트 Entity가 내부 Entity에 대한 참조를 바깥에 전달해 줄 수는 있지만 그런 객체는 바깥에서 일시적으로만 사용해야 되고 계속 보유되면 안된다.
    • 방어적 복사를 통해 다른 객체에 전달하여 외부에 의해 불변식이 깨지지 않도록 한다.
  4. 데이터베이스에 질의를 하면 Aggregate의 루트만 직접적으로 획득하도록 구현한다.
    • Aggregate 내부의 다른 객체들은 모두 Aggregate 루트의 연관관계로 탐색해서 발견해야 한다.
  5. Aggregate 안의 객체는 다른 Aggregate의 루트만 참조 가능하다.
  6. 삭제 연산은 Aggregate 경계 안의 모든 요소를 한 번에 제거해야 불변식을 지킬 수 있다.
  7. Aggregate 경계 안의 어떤 객체를 변경하더라도 전체 Aggregate의 불변식은 지켜져야 한다.

객체 간의 복잡한 연관관계로 인해 발생하는 문제점을 해소하고, 지켜져야 하는 불변식의 경계를 명확히 하기 위해서는 Entity와 Value Object를 하나의 Aggregate로 모으고 각각에 대해 경계를 정의하도록 한다.


Factory (팩터리)

어떤 객체나 Aggregate를 생성하는 일이 복잡하거나 외부로 내부 구조를 많이 드러내는 경우 Factory가 이를 캡슐화해준다.

객체의 장점 중 상당 부분은 객체의 내부구조와 연관관계를 정교하게 구성하는 데서 나온다. 객체는 그 것의 존재 이유와 관련이 없거나 다른 객체와 상호작용함에 있어서 필요없는 것이 남지 않을 때까지 정제해야 한다. 이러한 객체의 책임에는 객체 전체 생명 주기의 중간 단계에서 수행하는 것들이다. 문제는 이러한 책임만으로도 복잡한 객체에 객체 자체를 생성하는 책임까지 맡기는 데 있다.

복잡한 객체를 조립하거나 생성하는 일은 생성 후 해당 객체의 책임과 가장 관련성이 적은 일이다.

객체 생성하는 책임을 클라이언트 객체로 옮긴다면 문제가 훨씬 더 나빠진다. 클라이언트가 객체 생성 책임을 가진다는 것은 클라이언트가 도메인 객체의 내부 구조를 어느 정도 알고 있어야 한다는 것이다. 그 도메인 객체의 불변식을 지키기 위해 클라이언트는 해당 객체의 규칙을 알아야 한다. 이렇게 되면 객체의 클래스와 클라이언트가 결합되어, 클래스를 변경하면 클라이언트도 변경해야 한다.

객체 생성을 맡은 클라이언트는 불필요하게 복잡해지고, 도메인 객체와 Aggegate의 캡슐화를 위반한다. 또한 이 클라이언트가 만약 응용 계층의 일부를 구성한다면, 도메인 계층에서 책임이 새어나와 도메인 계층의 격리가 무력화된다.

어떤 객체를 생성하는 것이 그 자체로도 주요한 연산이 될 수 있지만 복잡한 생성 / 조립 연산은 생성된 객체 자체의 책임으로는 어울리지 않는다. 그렇다고 이 책임을 클라이언트에 두면 이해하기 힘든 설계, 구현이 나올 수도 있다.


복잡한 객체를 생성하는 일은 도메인 계층의 책임이지만, 그 책임이 모델을 표현하는 객체에 속하는 것은 아니다. 일반적으로 객체 생성하는 것은 도메인에서는 의미가 없긴 하지만, 구현 측면에서는 반드시 필요하다. 이 문제를 해결하기 위해 Entity나 Value Object, Service가 아닌 다른 무언가를 도메인 설계에 추가해야 한다. 도메인 모델링의 결과로 나타나는 모델 중 어떤 것에도 해당되지 않는 요소를 추가하는 것이지만, 이는 도메인 계층에서 맡고 있는 책임의 일부를 구성한다.

자신의 책임이 다른 객체를 생성하는 것을 Factory라고 한다.

02.png

어느 한 인터페이스가 자신의 구현을 캡슐화하고 객체의 동작방식을 알 필요가 없도록 해주듯이 Factory는 복잡한 객체나 Aggregate를 생성하는데 필요한 지식을 캡슐화한다.

복잡한 객체와 Aggregate를 생성하는 책임을 가지는 Factory는 도메인 설계의 일부를 구성하며, 이를 통해 클라이언트로부터 해당 객체의 내부구조나 규칙을 캡슐화할 수 있다. 클라이언트에는 인스턴스화되는 객체의 구체적인 클래스를 알 필요가 없도록 인터페이스로 제공해야 한다. 또한 Factory는 전체 Aggregate를 하나의 단위로 생성해서 그 것의 불변식이 지켜지도록 해야 한다.

Factory를 설계하는 방법에는 여러가지가 있지만, 다음 두 가지 요건을 통해 Factory를 잘 설계할 수 있다.

  1. 각 생성 방법은 원자적이어야 하며, 생성된 객체나 Aggregate의 불변식은 반드시 지켜져야 한다.
  2. Factory가 리턴하는 타입은 생성되는 구체적인 클래스보다는 생성하고자 하는 타입으로 추상화되어야 한다.


Factory와 Factory의 위치 선정

Aggregate 내부에 요소를 추가하기 위해 Aggregate 내부 객체를 생성하는 용도라면, 해당 Aggregate 루트에 Factory 메서드를 둘 수 있다. 다음과 같이 한 요소가 추가될 때마다 Aggregate의 무결성을 보장하는 책임을 루트가 담당하고 동시에 외부에 대해 Aggregate의 내부 구현을 숨길 수 있다.

03.png

또 다른 예로는 생성된 객체를 소유하지는 않지만 이 객체를 만들어내는 것과 밀접한 관련이 있는 특정 객체에 Factory 메서드를 두는 것이다.

04.png

위 그림에서 TradeOrder 객체와 Brokerage Account 객체는 같은 Aggregate를 구성하지는 않지만 Brokerage Account 객체가 TradeOrder 객체를 생성함에 있어서 충분한 정보를 가지고 있다면 Factory 메서드를 둘 수 있다.

Factory는 해당 Factory에서 생성되는 객체와 매우 강하게 결합되므로, 자신이 생성하는 객체와 가장 밀접한 관계에 있는 객체에 있어야 한다. 생성 과정이 복잡하여 여러 프로그램 요소가 개입되는 경우라면 별도의 Factory 객체나 Service를 만들어야 한다. 이런 독립형 Factory는 전체 Aggregate를 생성하여 루트에 대한 참조를 리턴할 것이다.

05.png

특정 Aggregate 안의 어떤 객체가 Factory를 필요로 하는데, Aggregate 루트가 해당 Factory가 있기에 적절하지 않다면 독립형 Factory로 만든다. 단, Aggregate 내부에 접근하는 것을 제한하는 규칙을 지켜야하므로, Factory를 통해 생성된 Aggegate의 내부 객체는 Aggregate로 건네주기 전까지는 일시적으로만 참조하도록 해야 한다.


생성자만으로 충분한 경우

때로는 직접 생성자를 통해 객체를 생성하는 것이 최선의 선택일 때가 있다. 특히 Factory는 다형성을 활용하지 않는 간단한 객체를 이해하기 어렵게 만들 수 있다. 반대로 생성자를 사용하는 편이 좋은 상황은 다음과 같다.

  • 클라스가 타입이거나 어떤 클래스 계층 구조의 일부로 구성되지 않으며, 인터페이스를 구현하는 식으로 다형적으로 사용되지 않는 경우
  • 클래스가 Strategy, 즉 전략 패턴을 위해 구현체에 관심이 있는 경우
  • 클라이언트가 이미 객체의 속성을 모두 알고 클라이언트에 노출된 생성자 자체적으로 객체 생성할 수 있을 경우
  • 생성자가 복잡하지 않은 경우
  • 공개 생성자가 Factory가 동일한 규칙을 준수하는 경우. 마찬가지로 생성자로 객채 생성시에도 불변식은 충족시켜야 한다.

다른 클래스의 생성자 내에서 생성자를 호출하지 않도록 한다. 생성자는 극도로 단순해야 한다. Aggregate와 같이 복잡한 조립과정을 거쳐 만들어지는 것을 생성하려면 Factory가 필요하다.


인터페이스 설계

Factory 메서드를 설계할 때는 Factory가 어떻게 구현되든 상관없이 다음의 두 가지 사항을 알아야 한다.

  1. 각 연산은 원자적이어야 한다.
    • 복잡한 객체를 생성하기 위해 필요한 모든 것들을 한 번에 전달해야 한다.
    • 생성이 실패할 경우 예외를 던지거나 null을 리턴하는 등, Factory에서 발생하는 실패에 대해 결정해야 한다.
  2. Factory는 Factory 메서드의 인자와도 결합된다.
    • 매개변수 타입이나 클래스에 대해서 결합이 생기게 된다.
      • 특히 매개변수가 생성될 객체에 주입만 되는 것이라면 의존성이 적당하지만, 이 매개변수의 메서드를 활용하여 객체 생성 과정에 이용한다면 결합은 더 강해진다.
    • 구체적인 클래스가 아닌 추상적인 타입의 인자를 사용하도록 한다.


불변식 로직의 위치

Factory의 책임은 만들어내는 객체나 Aggregate의 불변식이 지켜지도록 보장하는 것이다. 자기 자신이 직접 불변식을 검사할 수도 있지만, 간혹 생성된 객체에 위임할 수도 있다. 특히 각 도메인 객체 각자가 불변식을 검사하는 것이 더 깔끔할 때이다.

예를 들면 Entity를 생성할 때, Entity 식별성을 위해 사용하는 값은 Entity 내부에서 검사하는 것이 좋을 수 있다. 하지만 해당 객체가 활동하는 생명주기 동안 결코 수행되거나 적용되지 않을 불변식 로직을 객체에 위치시킬 필요는 없다. 이 때는 Factory가 불변식을 둘 논리적인 위치가 되며, 객체는 더 단순하게 유지될 것이다.


Entity Factory와 Value Object Factory

Entity Factory와 Value Object Factory는 두 가지 점에서 다른데, Value Object가 불변적이고, Entity는 식별성을 가진다는 차이에 기인한다.

특히 Entity는 식별성을 위해 식별성 할당이 필요하므로 그런 식별자를 관리하기에는 Factory가 적절한 곳이다.


저장된 객체의 재구성

Factory는 특정 객체의 생명주기의 초반에 관여하지만, 객체를 재구성할 때 (DB에서 데이터를 읽어 객체를 생성하는 등)도 사용할 수 있다.

객체를 재구성할 때 사용되는 Factory는 객체 생명주기의 초반에 관여하는 Factory와 유사하지만 주된 차이점은 아래의 두 가지가 있다.

  1. 재구성에 사용되는 Entity Factory는 식별성을 위해 새로운 ID를 할당하지 않는다.
  2. 객체를 재구성하는 Factory는 불변식 위반을 다른 방식으로 처리해야 한다.
    • 새로운 객체를 생성할 경우에는 단순히 객체 생성을 멈추면 되지만, 재구성할 때의 불변식 위반은 좀 더 탄력적으로 대응해야 된다. 가령 DB의 데이터 정합성이 맞지 않거나 일시적인 오류가 원인일 수도 있기 때문이다.

Factory는 모델의 어떤 부분도 표현하지는 않지만 해당 모델을 나타내는 객체를 뚜렷하게 드러내는 데 일조하는 도메인 설계의 일부로 볼 수 있다. Factory는 객체의 생성과 재구성이라는 생명주기 전이를 캡슐화한다.

이와 다르게 영속 저장소에 들어갈 때와 나올 때 거치는 전이가 있는데. 이러한 전이는 Repository의 책임이다.


Repository

연관관계를 통해 다른 객체와의 관계에 근거하여 특정 객체에 접근할 수도 있지만, 객체의 생명주기 중간에 Entity나 Value를 탐색하기 위한 첫 진입점이 존재해야 한다.

객체를 이용해 무언가를 하려면 해당 객체에 대한 참조를 가지고 있어야 한다. 이를 위해 객체를 직접 생성해서 참조를 획득할 수 있다. 또는 연관관계를 탐색하는 것인데, 이미 알고 있는 객체로부터 시작하여 해당 객체의 연관관계를 토대로 필요한 객체의 참조를 얻는 방법이 있다.

객체지향 프로그램에서는 이러한 일이 기본이며, 이러한 연결 구조를 바탕으로 객체 모델은 풍부한 표현력을 가지게 된다. 어쨋든 필요한 객체를 찾기 위한 진입점이 되는 첫 번째 객체가 있어야 한다.

모든 객체지향 시스템은 객체에 어떤 유용한 일을 시키기 위해서는 해당 객체의 참조를 얻는 것부터 시작한다. 이 참조를 얻기 위해 객체를 생성하거나 이 참조를 가지고 있는 (연관관계로 탐색할 수 있는) 객체가 필요하다.

객체 지향 시스템은 거대한 객체들의 네트워크로 구성된다. 객체는 상호 연결된 객체들 간의 협력을 통해 책임을 완수한다. 한 객체에서 다른 객체로 이동하기 위해 객체 간의 연관 관계를 이용한다. 따라서 특정한 작업을 수행하기 위해서는 얽히고 설킨 수 많은 객체들 중 어떤 객체에서 항해를 시작할 것인지 결정해야 한다. 모든 객체가 메모리 상에 존재한다고 가정하고 객체와 객체 간의 관계를 항해함으로써 목적 객체로 이동한다. 따라서 어떤 객체 그룹을 사용할 필요가 있다면 해당 객체 그룹 간의 관계를 항해하기 위한 시작 지점을 선정해야 한다.

대부분의 객체 내용은 관계형 데이터베이스에 저장하는 경우가 많다. 데이터베이스에 저장된 데이터를 토대로 객체로 구성하여 진입점으로 사용하기 위해, 데이터베이스 질의를 수행하여 데이터를 찾아 객체에 대한 참조를 획득하는 방법이 필요하다.

객체 참조를 획득함에 있어서 연관 관계를 통한 탐색을 제공할 것이냐, 어떠한 객체든 바로 접근하게 해주는 데이터베이스 검색에 의존할 것이냐가 설계 결정이 되며, 연관관계의 응집성과 검색의 분리는 상충관계에 있다. 예를 들어 데이터베이스에서 조회한 Customer 객체를 통해 (Order 컬렉션을 필드로 가지고 있는) Order를 접근해야 하는가? 아니면 CustomerID를 통해 데이터베이스에서 직접 Order를 검색해야 하는가?

기술적 관점에서 보면 저장된 객체를 가져오는 것은 실제로는 생성의 한 부분집합이다. 데이터베이스에서 가져온 데이터를 토대로 객체를 생성하기 때문이다. 하지만 개념상 객체의 생명주기 가운데 중간 단계에 불과하다.


데이터 중심의 구현이 안좋은 점

도메인 주도 설계의 목표는 기술보다는 도메인에 대한 모델에 집중하여 더 나은 소프트웨어를 만들어내는 것이다. 개발자가 직접 SQL 질의문을 구성하여 인프라스트럭처 계층의 질의 서비스에 전달하고, 테이블 행의 결과 집합을 획득하여 필요한 정보를 꺼내 직접 생성자나 Factory로 전달할 때쯤이면 도메인 모델에 집중하기 힘들어진다.

클라이언트가 직접 필요한 정보를 꺼내온다면, 자연스럽게 도메인 객체가 데이터만 담고 행위는 설명하지 않는 빈약한 도메인 객체가 되기 쉽다. 이는 곧 객체를 데이터의 컨테이너로 여기게 되고, 전체 설계가 데이터 중심의 처리 방식으로 나아간다.

특히 클라이언트 코드에서 직접적으로 데이터베이스를 이용하고 데이터베이스의 데이터를 다룰수록 개발자들은 Aggregate나 캡슐화와 같은 특징을 활용하는 것을 우회하려 하고 직접 조작하려하는 유혹에 빠진다. 필요도 없는 연관관계를 추가할 수도 있다. 이럴 경우 점점 많은 도메인 규칙이 SQL 질의 코드로 들어가거나 그냥 사라져 버린다. 개발자들은 자신이 원하는 객체가 무엇이든 직접 획득하려들 것이다.

인프라스트럭처에서 아무 도메인 객체의 참조를 쉽게 획득하게 해준다면 개발자들은 좀 더 탐색이 쉽도록 아무 연관관계(도메인 개념과는 상관없는)를 추가해 모델을 엉망으로 만들 수 있다. Aggregate 루트로부터 탐색하지 않고 정확히 필요한 데이터를 DB에서 뽑아내거나 몇가지 특정한 객체를 가져오기 위해 질의를 직접 사용할 수도 있다. 이러면 Aggregate 개념을 추가한게 무색해질만큼 모델을 엉망으로 만들고 불변식을 훼손할 수 있다. 도메인 로직은 질의나 클라이언트 코드로 들어가고 Entity나 Value Object는 단순히 데이터 홀더로서의 역할로 끝나게 될 수 있다.

Aggregate 내부에 존재하는 모든 객체는 루트에서 탐색을 토대로 접근하는 것 말고는 직접 참조를 획득하거나 접근해서는 안된다.

영속 객체는 진입점 제공을 위해 해당 객체의 속성에 근거하여 검색하는 식으로, 전역적으로 접근할 수 있어야 하지만, 그러한 접근 방식이 필요한 곳은 탐색으로 찾기에는 쉽지 않은 Aggregate의 루트만으로 한정해야 한다. 마음대로 데이터베이스 질의를 수행하면 도메인 객체와 Aggregate의 캡슐화가 깨질 수도 있다. 또한 기술적 인프라스트럭처와 데이터베이스 접근 메커니즘을 드러내면 클라이언트가 상당히 복잡해질 수 있다.

Repository 패턴은 이를 해결하고 개발자로 하여금 다시 모델에 집중할 수 있도록 개념적 틀에 해당한다. Repository는 특정 속성에 근거하여 요청된 객체를 가져오며 데이터베이스 질의 및 메타데이터 매핑에 대한 장치를 캡슐화한다. 이를 통해 클라이언트는 단순해지고 인터페이스를 통해 소통하며 모델 측면에서 필요로 하는 것들을 요청할 수 있게 된다.

진입점 제공을 위해 전역적인 접근이 필요한 객체 타입에 대해, 메모리 상에서 해당 타입의 객체를 요소로 가지는 컬렉션이 있다는 착각을 불러일으키게 Repository를 구성한다. 클라이언트 입장에서는 객체가 메모리 상에 존재하는 것처럼 보여야 한다.

Repository의 기능을 메모리에 존재하는 컬렉션에 대한 오퍼레이션으로 바라보는 것은 도메인 모델을 단순화하기 위한 중요한 추상화 기법이다. 도메인 모델을 설계하고 필요한 오퍼레이션을 식별하는 동안 우리는 하부의 어떤 메커니즘이 도메인 객체들의 생명 주기를 관리하는지에 관한 세부사항을 무시할 수 있다. Repository가 제공하는 인터페이스의 의미를 메모리 컬렉션에 대한 관리 개념으로 추상화함으로써 자연스럽게 하부의 데이터와 관련된 영속성 메커니즘을 도메인 모델로부터 분리할 수 있다.

Repository를 통해 실제로 데이터 저장소에 데이터를 저장하고 제거하는 연산을 캡슐화하고, 외부에서 직접 접근 가능한 Aggregate의 루트에 대해서만 Repository를 제공해야 한다. 모든 객체 저장과 접근은 Repository에 위임하여 클라이언트가 모델에 집중하도록 해야 한다.


Repository에 질의하기

가장 만들기 쉬운 Repository는 질의에 구체적인 매개변수를 직접 전달하는 것이다. 식별자를 기준으로 Entity를 조회하거나 특정 속성 값을 가지는 객체 컬렉션 요청이 있다.

06.png

Repository를 통해 질의를 하는 또 다른 접근법은 Specification(명세)에 기반을 둔 질의를 사용하는 것이다. 이 명세를 이용해 클라이언트는 질의의 획득 방법에는 신경쓰지 않고도 원하는 바를 서술할 수 있다.

07.png


클라이언트 코드가 Repository 구현을 무시한다.

영속화 기술을 Repository를 통해 캡슐화하면 클라이언트가 매우 단순해지고, Repository 구현에서 완전히 분리된다. 그러나 캡슐화가 종종 그렇듯이 내부 구현에서 무슨 일이 일어나는지는 반드시 파악하고 있어야 한다. Repository가 의도하지 않은 방식으로 사용되거나 작동한다면 수행 성능이 극단에 치우칠 수 있다. 개발자들은 캡슐화된 행위를 활용하는 것에 내포된 의미를 알아야 한다.


Repository 구현

Repository 구현은 영속화에 사용되는 기술과 인프라스트럭처에 따라 매우 다양하겠지만 이상적인 모습은 클라이언트에 모든 내부 기능을 숨기고 어떤 기술을 사용하느냐에 상관없이 클라이언트 코드를 동일하게 유지되도록 하는 것이다.

저장, 조회, 질의 매커니즘을 캡슐화하는 것은 Repository 구현의 가장 기본적인 기능이다.

08.png

위 그림처럼 TradeOrder 객체를 조회함에 있어서 식별자(ID)를 통해 Repository에 전달하고, 필요한 SQL문으로 질의하고, 객체로 재구성하는 것을 알 수 있다.

Repository를 구현함에 있어서 명심해야 할 몇 가지 중요한 사항이 있다.

  1. 타입을 추상화한다.
    • 특정 타입의 인스턴스를 Repository가 담기는 하지만, 이것이 각 클래스마다 하나의 Repository가 필요하다는 것이 아니다.
    • 타입은 상황에 따라 인터페이스가 될 수도 있고, 추상 상위 클래스가 될 수도 있고 구현 클래스가 될 수도 있다.
    • 사용하는 영속화 기술에 따라 다형성이 제약될 수도 있다.
  2. 클라이언트와의 분리를 활용한다.
    • 클라이언트와 영속화 기술을 분리함으로써 더 자유롭게 Repository의 구현을 변경할 수 있도록 한다.
  3. 트랜잭션 제어를 클라이언트에 둔다.
    • 불변식을 제어하는 트랜잭션의 시작와 끝은 클라이언트가 잘 알고 있다. 클라이언트에 올바르게 단위 작업을 시작하고 커밋하는 컨텍스트가 있기 때문이다.


Factory와의 관계

Factory가 객체 생애의 초기 단계를 다루는 데 반해, Repository는 중간 단계(재구성)와 마지막 단계(삭제)를 관리한다.

데이터베이스로부터 데이터를 읽어 객체를 생성하므로 Repository를 Factory로 생각할 수도 있는데, 기술적 관점에서는 그렇다고 볼 수 있다. 그러나 모델을 중심으로 생각했을 때, 저장된 객체를 재구성하는 것이 실질적으로 새로운 객체를 생성하는 것은 아니다. 도메인 주도 관점에서 봤을 때는 Repository와 Factory의 책임이 뚜렷이 구분되는데, Factory가 새로운 객체를 만들어 내는 데 반해 Repository는 기존 객체를 찾아낼 뿐이다. 클라이언트 입장에서는 Repository를 통해 기존 객체를 찾는 것처럼 보여야 한다. 이런 객체는 그 객체 생명주기 상에서 중간 단계에 해당한다.

다만 다음과 같이 Repository가 Factory에 데이터베이스로부터 읽은 데이터를 전달하여 인스턴스 생성을 위임할 수도 있다.

09.png

사람들이 Factory와 Repository를 합쳐서 생각하게 만드는 또 하나의 경우는 “데이터베이스에서 찾아서 없을 때, 생성하는” 기능을 원할 때다. 이런 기능은 사용하는 것을 자제해야 한다. 기껏해야 조금 더 편리할 뿐이다. 일반적으로 새로운 객체와 이미 존재하는 객체를 구분하는 것은 도메인에서 중요하다.


Tags:
Stats:
0 comments