IDDD 13장. 바운디드 컨텍스트 통합

통합의 기본

  1. API & RPC (RPC, SOAP, WEB/API) : 탄력성이 떨어짐(Legacy)
  2. 메시지
  3. RESTful API
    • 1.과 유사하지 않은가? 하지만 필자는 HTTP Verbs 로 차이를 이야기하고 있다.
  4. 그 외

분산 시스템은 근본적으로 다르다

분산 컴퓨팅의 원칙

  • 네트워크는 신뢰할 수 없다.
  • 언제나 지연은 있다.
  • 대역폭은 무한하지 않다.
  • 정보와 정책은 다수의 관리자에 걸쳐 퍼져 있다.
  • 네트워크 전송은 비용이 든다.
  • 네트워크는 다양성을 갖고 있다.

왜 원칙인가?

  • 반드시 해결해야하는 문제점
  • 계획해야하는 복잡성
  • 일반적으로 저지르는 실수가 아님

시스템 경계에 걸친 정보의 교환

결국은 발행된 언어Published Language에 의존해야한다.

  • 정보에서는 인터페이스/클래스가 중요한 것이 아니라 말 그대로 데이터의 특성이 중요

예를 들어서

BacklogItemCommitted 알림 이벤트를 발행한다고 하면 아래와 같은 계약을 의미할 것이다.

정의/속성 설명
타입 Notification
포멧 JSON
notificationId 식별자
typeName 알림의 타입(BacklogItemCommitted)
version 알림 버전
occurredOn 발생일시
event 본문(Payload)

직렬화 & 역직렬화 자바 소스는 SKIP

중요한 것

  • 위와 같은 계약을 통해 컨슈머와 프로슈머가 서로 통신(정보의 교환)을 할 수 있다
  • 시스템 상호간 의존성이 줄어든다. 공유 커널과 비교해보아라
    • 버전을 통해서 호환성 이슈가 더 없어진다.
  • 위 내용을 사용자 지정 미디어 타입 계약 이라 한다.

레스트풀 리소스를 사용한 통합

오픈 호스트

서비스의 집합으로서 여러분의 서브시스템으로의 액세스를 허용하는 프로토콜을 정의하자. 여러분과 통합해야 하는 모든 사람들이 사용할 수 있도록 프로토콜을 공개하자. 프로토콜을 강화하고 확장해서 새 통합 요구사항을 처리하도록 하자. [Evans]

  • 결합도를 느슨하게 하기 위해서(자율성) retry와 같은 기법이 요구된다.

RESTful API 예를 들면

특정 사용자의 권한을 조회 /tenants/{tenantId}/users/{username}/inRole/{role}

  • 200 : 권한 있음
  • 204 : 권한 없음 (404도 괜찮지 않을까?)

레스트풀 리소스의 구현

현업에서 우리는 오픈 호스트라고 생각했지만 실상은 공유된 커널 이거나 순응주의자인 경우가 많다.

오픈 호스트라면 바운디드 컨텍스트가 의존성이 매우 낮지만 공유된 커널이나 순응주의자는 상당한 결합도를 가지게 되므로 자율성을 위해서 오픈 호스트 지향하는 것이 좋다고 본다.

UserResource.java

    @GET
    @Path("{username}/inRole/{role}")
    @Produces({ OvationsMediaType.ID_OVATION_TYPE })
    public Response getUserInRole(
            @PathParam("tenantId") String aTenantId,
            @PathParam("username") String aUsername,
            @PathParam("role") String aRoleName) {

        User user = this.accessApplicationService()
                       .userInRole(
                               aTenantId,
                               aUsername,
                               aRoleName);

        Response response = this.userInRoleResponse(user, aRoleName);
        return response;
    }

사용자의 권한을 조회하는 API 응답

HTTP1.1 200 OK
Content-Type: application/vnd.saasovation.idovation+json
...
{
    "role": "Author",
    "username": "zoe",
    "tenantId": "A94A-...",
    "firstName": "Zoe",
    "lastName": "Doe",
    "emailAddress": "zoe@saasovation.com"
}

부패 방지 게층을 통한 REST 클라이언트 구현

ID와 액세스 컨텍스트와 협업 컨텍스트의 부패 방지 계층 사이의 통합을 위해 사용된 오픈 호스트 서비스

그림 13-1 ID와 액세스 컨텍스트와 협업 컨텍스트의 부패 방지 계층 사이의 통합을 위해 사용된 오픈 호스트 서비스

interface CollaboratorService {
    Author authorFrom(Tenant tenant, String identity);
    Creator creatorFrom(Tenant tenant, String identity);
    Moderator moderatorFrom(Tenant tenant, String identity);
    Owner ownerFrom(Tenant tenant, String identity);
    Participant participantFrom(Tenant tenant, String identity);
}

Java Code

리파지토리를 사용할 수도 있었지만 Author는 엔터티가 아닌 값 객체이므로 부패 방지 계층이 더 어울리다. 반대로 부패 방지 계층에서 애그리게잇(가변성)을 생성한다면 리파지토리가 더 어울린다.

메시징을 사용한 통합

레스트풀 리소스를 이용한 방식보다 더 높은 수준의 자율성을 가짐.

도메인 이벤트를 외부로 발행할 때 좋다.

제품 소유자와 팀 맴버의 정보를 계속해서 제공받는 것

사용자에게 권한을 할당

  1. 권한에 사용자를 추가
    • AccessService#asignUserToRole(AssignUserToRoleCommand)
    • Role#assignUser(User)
  2. 사용자에게 권한 할당됨 이벤트 발행 : new UserAssignedToRole()
  3. 컨텍스트 외부로 이벤트 발행
  4. AgilePM 컨택스트에서 이벤트를 구독
    • TeamMemberEnablerListener#filteredDispatch
    • TeamService#enabledProductOwner(EnableProductOwnerCommand)

당신은 책임을 감당할 수 있는가

외부 바운디드 컨텍스트에서 정보를 (이벤트를 통해서) 복제한 후 그것을 지속적으로 동기화한다. 즉, 데이트를 복사하는 복잡한 책임이 생긴다.

이벤트가 항상 순차적으로 구독자에게 온다는 보장이 없다. 고로 이벤트 처리 시 항상 발생시각을 확인해서 처리해야한다.

class TeamService {
    @Transactional  // 구독 처리
    public void disableTeamMember(DisableTeamMemberCommand command) {
        TenantId tenantId = new TenantId(command.getTenantId);
        TeamMember teamMember = 
                teamMemberRepository.teamMemberOfIdentity(
                        tenantId, command.getUsername());
        if (teamMember != null) {
            // 여기가 중요하다!!!
            teamMember.disable(command.getOccurredOn());
        }
    }
}

class Member {
    // 위에 이어서 disable 메서드를 보면
    public void disable(Date asOfDate) {
        if (this.changeTracker().canToggleEnabling(oaOfDate)) {
            this.setEnabled(false); // enable() 라고 하지..
            this.setChangeTracker(
                this.changeTracker().enablingOn(asOfDate));
        }
    }
    public void enable(Date asOfDate) {
        if (this.changeTracker().canToggleEnablig(asOfDate)) {
            this.setEnabled(true)); // disable() 라고 하지..
            this.setChangeTracker(
                this.changeTracker().enablingOn(asOfDate));
        }
    }
}
  • 위 처리를 통해서 이벤트 순서가 뒤바껴도 처리에 문제가 없다.
  • 또한 멱등하게 만드는 역할도 한다.

새로운 책임을 알아보자 : 정보 복제는 사소한 책임이 아니다

사용자 정보 복제 시 영향을 미치는 이벤트들

  • PersonContactInformationChanged
  • PersonNameChanged
  • UserAssignedToRole
  • UserUnassignedFromRole

하지만 이외에도

  • UserEnablementChanged
  • TenantActivated
  • TenantDeactivated

위와 같은 이벤트들이 존재한다.

= 가능하면 바운디드 컨텍스트 전반에서 정보의 중복을 최소화하거나 완전히 제거하는 것이 최선이다

= 필요할 때 마다 오픈 호스트 서비스와 같은 것을 이용해서 정보를 조회하는 것이 나을수도 있다

어쩌란 건지 명확히 이해가 안된다.??? 필요한 이벤트 발행을 막을 필요는 없지만 굳이 데이터 중복을 통해서 관리하지 말고 이벤트는 이벤트대로 사용하고, 데이터는 필요할 때 마다 조회하는 것이 낫다는 것인가?

장기 실행 프로세스와 책임의 회피

다른 바운디드 컨텍스트에게 데이터 생성 책임을 지게 하고, 단순하게 레코드 시스템이 그 고유한 정보를 처리하게 할 것이다. 즉, 책임을 네트워크 건너로 차버린다.

제품을 생성하는 유스케이스를 살펴본다

  1. 사용자는 제품 설명 정보를 제공한다.
  2. 사용자는 팀토론에 대한 의사를 표시한다.
  3. 사용자는 정의된 제품을 만들도록 요청한다.
  4. 시스템은 포럼과 토론이 들어간 제품을 만든다.

애자일 프로젝트 관리 컨텍스트에서

애자일 프로젝트 관리 컨텍스트에서 흐름도

ProjectService#newProductWithDiscussion(NewProductCommand)

  • ProductProductDiscussion을 생성
  • ProductCreated 이벤트 발행
    • Discussion#availablity#isRequested(토론 가능 상태)가 참이면 장기 프로세스를 시작 한다.
    • 장기 실행 프로세스 == 상품 토론을 생성
  • 추후에 구매를 통해서 토론을 생성할 수도 있다. Product#requestDiscussion(DiscussionAvailablity)
    • ProductDiscussionRequested 이벤트 발행

그렇다면 사용 가능 상태가 REQUESTED가 아니라면 이 이벤트의 발행의 의미가 있는가?

> 이것에 대한 책임은 이벤트 구독자가 갖는다.

ProductDiscussionRequestedListener#listensToEvents

  • ProductCreated, ProductDiscussionRequested 이벤트를 수신한다.

ProductDiscussionRequestedListener

    protected void filteredDispatch (String type, String textMessage) {
        // 토론 사용 가능상태가 아니면 이벤트 처리 없음
        if (!reater.eventBooleanValue("requestingDiscussion")) {
            return;
        }
        ...
        // CreateExclusiveDicussion 커맨드를 만들어 협업 컨텍스트에 메시징 인프라를 통해서 이벤트 발행
        this.messageProducer().send(...);
    }

여기서 잠깐

  • 애자일 프로젝트 관리 컨텍스트에서 Product 애그리게잇에서 발행된 이벤트(ProductCreated)를 구독할 리스터를 만들 필요가 있을까?
    • 차라리 협업 컨텍스트에서 ProductCreated 이벤트를 바로 구독하는 것이 낫지 않을까?
    • 그런데 ProductCreated는 협업 컨텍스트의 유비쿼터스 언어와 어울리지 않고, 이 이벤트가 ForumDiscussion을 생성하게 한다는 것을 연결시키는 것이 힘들다.
    • 고로 협업 컨텍스트 입장에서 CreateExclusiveDicussion 명령을 이벤트로 받는 것이 어색하지 않다.

협업 컨텍스트에서

협업 컨텍스트에서 흐름도

ExclusiveDiscussionCreationListener#filteredDispatch(String, String)

  • 위에서 이어 애자일 프로젝트 관리 컨텍스트에서 발행한 CreateExclusiveDicussion를 구독
  • ForumService#startExclusiveForumWithDiscussion를 호출해서 ForumDiscussion을 생성
    • 생성과 동시에 DiscussionStarted 이벤트를 발행한다.

다시 애자일 프로젝트 관리 컨텍스트로 돌아와서

다시 애자일 프로젝트 관리 컨텍스트에서 흐름도

DiscussionStartedListener#filteredDispatch(String, String)

  • 협업 컨텍스트에서 발행한 DiscussionStarted 이벤트를 구독한다.

이어서 ProductService#initiateDiscussion(InitiateDiscussionCommand)

  • Product#initiateDiscussion 를 실행해서 아까 생성한 Product에서 Discussion 생성 상태 변경
    • 당연히 멱등하다. 이미 Discussion을 생성해서 READY 상태라면 아무일도 일어나지 않는다. 즉 REQUESTED 상태에서만 이하 작업이 실행된다.
    • 그리고 ProductDiscussionInitiated 이벤트를 발행한다.

여기서 잠깐

  • 이 장기 실행 프로세스는 네트워크를 건너서 통신하고 메시징 인프라에 의존하는데 여기에 문제가 생기면 어떻게 될까?
  • 즉 프로세스가 끝까지 실행한다고 확신할 수 있을까?

프로세스 상태 머신과 타임아웃 트래커

위 문제점 해결을 위해서 타임아웃 트래커가 필요하게 된다.

타입아웃 트래커

트래커는 완료까지 부여된 시간이 만료된 프로세스를 감시하는데. 이런 프로세스는 만료되기 전까지 몇번이고 재시도될 수 있다. 트래커는 원하는 경우 고정된 시간 간격을 재시도하도록 설계할 수 있고, 재시도를 전혀 하지 않거나 정해진 횟수만큰을 재시도한 후에 타임아웃을 발생시키도록 할 수도 있다. 기술적 서브도메인의 일부다

상품의 토론 생성 흐름도 with 타임아웃 트래커

ProductService#startDiscussionInitiation

package com.saasovation.agilepm.application;

class ProductService {
    @Transactional
    public void startDiscussionInitiation(
            StartDiscussionInitiationComman command) {
        Product product = productRepository.findById(...)
        
        TimeConstrainedProcessTracker tracker = new TimeConstrainedProcessTracker(
                product.tenantId().id,
                ProcessId.newProcessId(),
                "Create discussion for product : " + product.name(),
                new Date(),
                5L * 60L * 1000L, // 5분 마다 재시도
                3,                // 총 3회 재시도한다.
                ProductDiscussionRequestTimedOut.class);

        processTrackerRepository.add(tracker);  // 타임아웃 트래커 등록
        
        product.setDiscussionInitiationId(
                tracker.processId().id());
    }
}

상품의 토론 타임아웃 or 재시도 흐름도

상품의 토론 타임아웃 or 재시도 흐름도 with 타임아웃 트래커

마지막에 상품과 토론이 정상적으로 생성되면 흐름도

상품과 토론이 정상적으로 생성 시 흐름도

  • 애자일 프로젝트 관리 컨텍스트에서 마지막으로 상품의 토론이 정상적으로 생성되면 타임아웃 트래커를 완료 시켜야 한다.
  • 이것으로 프로세스는 끝난다.

위 장기 실행 프로세스의 문제점과 해결책

  • 동일한 CreateExclusiveDiscussion 커맨드가 여러 번 발송된다.
    • 멱등성을 지켰기 때문에 문제가 없을 것 같다.
    • 하지만 애자일 프로젝트 관리 컨텍스트가 실패한다면 문제가 발생할 수 있다.
    • 결국 협업 컨텍스트의 ExclusiveDiscussionCreationListener가 멱등한 애플리케이션 서비스 메서드로 작업을 위임할 수 있다면 많은 문제점을 해결할 수 있다.

협업 컨텍스트에서 흐름도

  • 위 흐름도 기준에서 startForum, startDiscusssion 행위가 멱등하면 해결된다.

좀 더 복잡한 프로세스 설계하기

복잡한 상황에서 다수의 완료 단계가 필요하게 된다면 상태 머신이 좋은 선택

Proces process = new TestableTimeConstrainedProcess(
        TENANT_ID,
        ProcessId.newProcessId(),
        "Testable Time Constrained Process",
        5000L); // 재시도 없이 5초 내로 완료되야 한다.

TimeConstrainedProcessTracker tracker = 
        process.timeConstrainedProcessTracker();
    
process.confirm1();

assertFalse(process.isCompleted());
assertFalse(process.didProcessingComplete());
assertEquals(process.processCompletionType(), ProcessCompletionType.NotCompleted);

process.confirm2();

assertTrue(process.isCompleted());
assertTrue(process.didProcessingComplete());
assertEquals(process.processCompletionType(), ProcessCompletionType.CompletedNormally);
assertNull(process.timedOutDate());

tracker.informProcessTimedOut();    // 어짜피 이미 완료되어 있으므로 아무처리가 없을 것
assertFalse(process.isTimedOut());  // 즉 타임아웃 되지 않았으므로 언제나 false
  • TestableTimeConstrainedProcess는 재시도 없이 5초 내로 완료되야 한다.
  • confirm1(), confirm2()만 호출된다면 완전히 완료 상태로 표시된다.
    • 내부적으로 2가지 상태를 속성으로 지니고 있다.
    • 즉, confirm1, confirm2 둘 다 true 여야지 완료 상태

메시징이나 시스템을 활용할 수 없을 때

  • 고가용성 확보가 최선이다.
  • 메시징인프라가 죽었다가 다시 살아났을때 자동으로 리스너가 재활성화 될 수록 해야한다.

마무리

  • 분산 컴퓨팅에서의 통합
  • RESTful VS 메시징인프라
  • 장기 수행 프로세스
    • Retry와 타임아웃
    • 멱등성
    • 복잡한 상태를 해결해주는 상태 머신

MJ

MJ
Backend 개발자 사람입니다. 어플리케이션의 복잡성을 다루는 DDD에 관심이 많습니다. 어제보다 더 나은 개발자가 되려고 항상 노력합니다.

spring boot 2.4.x 에서 openfeign + hystrix 통합하기

spring-boot 2.4.x spring-cloud 2020.x 의존성 상황에서 feign.hystrix.enabled=true가 안됨`feign.circuitbreaker.enabled=true` 로 바꿔보지만 openfeign과 hystr...… Continue reading

IDDD 14장. 애플리케이션

Published on June 19, 2018

IDDD 12장. 리파지토리

Published on June 10, 2018