본문 바로가기
도메인 주도 설계

왜 DDD 에서 팩토리 패턴을 사용하는 걸까?

by simplify-len 2020. 8. 22.

Photo by Christopher Burns on Unsplash

팩토리 패턴은 애그리게잇을 생성하는 책임을 가지는 메소드나 객체를 말한다.

도메인 주도 설계에서 팩토리 패턴을 사용되는 곳은

1. 도메인 모델 내의 팩토리
2. 애그리게잇 루트상의 팩토리 메소드
3. 서비스의 팩토리

그리고 에릭 에반스가 말하는 팩토리 패턴을 사용하는 주된 동기는 아래와 같다.

 복잡한 객체와 애그리게잇 인스턴스를 생성하는 책임을 별도의 객체로 이동시키자. 여기서의 책임은 도메인 모델과 관련이 있진 않지만, 여전히 도메인 설계를 구성하는 한 요소다. 모든 복잡한 조립 과정을 캡슐화하고, 클라이언트가 인스턴스화된 객체의 구체적 클래스를 참조할 필요가 없도록 인터페이스를 제공하자. 전체 애그리게잇을 하나의 조각으로 생성하고, 고정자를 지정하자. [Evans]

1. 도메인 모델 내의 팩토리

도메인 모델 내에서 팩토리은 어떻게 사용될까?
팩토리는 도메인 모델 내에서 객체 생성 외의 추가적인 책임을 가질 수도 있고 그렇지 않을 수도 있다.

특정한 애그리게잇 타입만을 인스턴스화하기 위한 객체에겐 그 외의 책임은 부여하지 않으며, 모델의 가장 중요한 구성 요소중 하나로 취급되지도 않는다. 이는 단지 팩토리일 뿐이다.

 다른 애그리게잇 타입(또는 내부 파트)의 인스턴스를 생성하기 위해 팩토리 메소드를 제공하는 애그리게잇 루트는 주요 애그리게잇 행동을 제공할 책임을 갖게 되며, 팩토리 메소드는 단지 그중 하나일 뿐이다.

가능한 팩토리 위치는

  • 애그리게잇 루트
    • 정정 생성자를 통해서 도메인 의도가 나오면 더 좋다. static Task#forDraft()
    • 아니면 상위 루트에서 하위 엔터리 생성도 가능 Project#createTask()
  • 도메인 서비스 : 서비스 기반 팩토리
    • ProjectService#createProject()
  • 팩토리 : 이 책은 다루지 않음
    • ProjectFactory#create()

2. 애그리게잇 루트상의 팩토리 메소드

IDDD_Sample 프로젝트에서 사용 예는 다음 아래와 같다.

바운디드 컨텍스트 애그리게잇 팩토리 메소드
식별자와 액세스 컨텍스트 Tenant offerRegisterationInvitation()
    provisionGroup()
    provisionRole()
    registerUser()
협업 컨텍스트 Calendar scheduleCalendarEntry()
  Forun startDiscussion()
  Discussion post()
애자일 PM 컨텍스트 Product planBacklogItem()
    scheduleRelease()
    scheduleSprint()

책에서는 CalendarEntry 인스턴스 코드를 통해 확인해보자.

@Entity
class Calendar extends AbstractAggregateRoot {
    CalendarEntry scheduleCalendarEntry(...) {
        CalendarEntry calendarEntry = new CalendarEntry(...);
        this.registerEvent(new CalendarEntryScheduled(...));
        return calendarEntry;
    }
}

- Calendar 는 CalendarEntry 라는 이름의 새로운 애그리게잇을 인스턴스화

-  팩토리 메소드에서 가드가 필요없는이유는 new CalendarEntry의 생성자에서 책임진다.

- 인자의 갯수가 줄어든다. 
: scheduleCalendarEntry(...)에서는 11개가 요구되지만, new CalendarEntryScheduled(...)에서는 9개로 줄어든다.

Discussion 인스턴스 생성하기 

언어에 특화된 Forum의 startDiscussion()

public class Forum extends EventSourcedRootEntity {

    ...
    public Discussion startDiscussion(
            ForumIdentityService aForumIdentityService,
            Author anAuthor,
            String aSubject) {

         if (this.isClosed()) {
                throw new IllegalStateException("Forum is closed.");
            }

            Discussion discussion =
                    new Discussion(
                        this.tenant(),
                        this.forumId(),
                        aForumIdentityService.nextDiscussionId(),
                        anAuthor,
                        aSubject,
                        anExclusiveOwner);

            return discussion;
    }
    ...
}

팩토리 메서드를 사용하는 다른 예제.

 - 유비쿼터스 언어로 표현될 수 있는 기반을 만들어 준다.

- Discussion을 생성할 뿐만 아니라, Forum이 닫혀있는 상황일때 생성을 막아준다.

3. 서비스의 팩토리

 아래 서비스 인터페이스는 식별자와 액세스 컨테스트로부터 협업 컨텍스트로의 객체 변환을 제공한다. 

public interface CollaboratorService {

    public Author authorFrom(Tenant aTenant, String anIdentity);

    public Creator creatorFrom(Tenant aTenant, String anIdentity);

    public Moderator moderatorFrom(Tenant aTenant, String anIdentity);

    public Owner ownerFrom(Tenant aTenant, String anIdentity);

    public Participant participantFrom(Tenant aTenant, String anIdentity);
}

추상 기반의 Collaborator로부터 파생된 새 객체를 서비스가 생성하기 때문에, 실제론 서비스가 팩토리로서 기능한다. 아래는 인터페이스 메소드 구현을 한 코드이다.

public class TranslatingCollaboratorService implements CollaboratorService {

    private UserInRoleAdapter userInRoleAdapter;

    public TranslatingCollaboratorService(UserInRoleAdapter aUserInRoleAdapter) {
        super();

        this.userInRoleAdapter = aUserInRoleAdapter;
    }

    @Override
    public Author authorFrom(Tenant aTenant, String anIdentity) {
        Author author =
                this.userInRoleAdapter()
                    .toCollaborator(
                            aTenant,
                            anIdentity,
                            "Author",
                            Author.class);

        return author;
    }

    @Override
    public Creator creatorFrom(Tenant aTenant, String anIdentity) {
        Creator creator =
                this.userInRoleAdapter()
                    .toCollaborator(
                            aTenant,
                            anIdentity,
                            "Creator",
                            Creator.class);

        return creator;
    }
}

 이 구현은 Tenant와 식별자를 Author 클래스의 인스턴스로 바꾸기 위해 UserInRoleAdaptor를 활용한다. 이 어탭터는 식별자와 액세스 컨텍스트의 오픈 호스트 서비스와 상호작요아며 주어진 사용자에게 저자라는 이름의 역할이 있는지 호가인. 만약 그렇다면 변환 시켜준다.

UserInRoleAdapter와 CollaboratorTranslator에선 복잡성의 척도가 있다. 결론적으로 UserInRoleAdpater는 외래 컨텍스트와의 의사소통만을 책임진다. 

Wrap up

- 팩토리의 사용이 컨텍스트의 유비쿼터스 언어와 밀접하게 부합하는 표현적 모델의 생성으로 이어지는 이유를 이해하게 됐다.
- 애그리게잇의 행동으로서 구현된 구현된 두 가지 팩토리 메소드를 살펴봤음
- 민감한 데이터의 올바른 생성과 사용을 확실히 함과 동시에, 팩토리 메소드를 사용해 다른 타입의 애그리게잇 인스턴스를 생성하는 방법을 이해하는 데, 도움이 됐다. 
- 다른 바운디드 컨텍스트와 상호작용하며, 외래 객체를 로컬 타입으로 변환해 줘야하는 상황에서, 팩토리로서 도메인 서비스를 설계하는 방법을 학습.

댓글