티스토리 뷰

MSA

비동기 메시징(SNS-SQS Fanout)

mandykr 2022. 12. 2. 14:46

목차

1. 메시징

2. 이벤트기반 아키텍처

 

 

 

서비스에 적용 가능한 IPC(Inter-Process Communication) 기술은 HTTP 기반 REST나 gRPC 등 동기 요청/응답 기반의 통신 매커니즘도 있고, AMQP, STOMP 등 비동기 메시지 기반의 통신 메커니즘도 있다.

IPC의 선택은 시스템 가용성에 영향을 미치는데, 요청을 처리하는 과정에서 타 서비스와 동기 통신을 하면 그만큼 가용성이 떨어지므로 가능한 서비스가 비동기 메시징을 이용하여 통신하도록 설계하는 것이 좋다.

 

1. 메시징

메시징은 서비스가 메시지를 서로 비동기적으로 주고받는 통신 방식이다.

 

메시지

메시지는 헤더와 바디로 구성되고 종류는 다음과 같다.

  • 문서(document) : 데이터만 포함된 제네릭한 메시지, 메시지 해석은 수신자가 결정.
  • 커맨드(command) : 호출할 작업과 전달할 매개변수가 지정되어 있다.
  • 이벤트(event) : 송신자에게 어떤 사건이 발생했음을 알리는 메시지(도메인 이벤트).

 

메시지 채널

메시지는 메시지 채널을 통해 교환되고 종류는 다음과 같다.

  • 점대점(point-to-point) 채널 : 채널을 읽는 컨슈머 중 딱 하나만 지정하여 메시지를 전달(예: 커맨드 메시지)
  • 발행-구독(publish-subscribe) 채널 : 같은 채널을 바라보는 모든 컨슈머에 메시지를 전달(예: 이벤트 메시지)

 

메시지 브로커

메시징 기반의 애플리케이션은 대부분 메시지 브로커를 사용한다.

메시지 브로커는 메시지가 지나가는 중간 지점이고 송신자가 메시지 브로커에 메시지를 쓰면 브로커는 메시지를 수신자에게 전달한다.

 

장점

  • 느슨한 결합 : 클라이언트는 적절한 채널에 메시지를 보내는 식으로 요청하고 서비스 인스턴스를 몰라도 되므로 느슨한 결합을 갖게 한다.
  • 메시지 버퍼링 : 메시지 브로커는 처리 가능한 시점까지 메시지를 버퍼링한다. 동기 요청/응답 프로토콜과 달리 메시징을 쓰면 컨슈머가 처리할 수 있을 때까지 큐에 메시지가 쌓인다.

 

단점

  • 단일 장애점 가능성 : 메시지 브로커의 가용성이 낮으면 시스템의 신뢰도가 떨어진다.
  • 운영 복잡도 부가 : 메시징 시스템 역시 설치, 구성, 운영해야 하는 시스템 컴포넌트이다.

 


2. 이벤트기반 아키텍처

비동기 메시징 패턴으로 이벤트 기반 아키텍처를 구축한다.

 

먼저, 스프링 애플리케이션 이벤트로 관심사를 분리하고 트랜잭션을 통해 데이터 일관성을 보장한다.

다음으로 AWS SNS-SQS 메시징 시스템으로 비관심사를 분리하고 외부 이벤트를 발행한다.

마지막으로 AWS SNS-SQS 메시징 시스템으로 외부 이벤트의 구독 계층을 구성한다. 

 

1) 첫번째 구독자 계층

(1) 트랜잭셔널 메시징

트랜잭셔널 아웃박스

도메인 엔티티를 DB 업데이트 후 메시지가 전송되지 않은 상태에서 서비스가 중단되거나 메시징 시스템에 장애가 발생한 경우 시스템의 장애로 이어지는 문제가 발생할 수 있어 DB 업데이트와 메시지 발행 두 작업이 서비스에서 원자적으로 수행되도록 해야한다.

이벤트를 도메인과 동일한 저장소를 사용해 저장하면 ACID 트랜잭션에 대한 처리를 DBMS에 믿고 맡길 수 있다.

동일 저장소를 통해 데이터베이스를 저장하고 이벤트를 발행함에 안정적인 정합성을 보장하는 방식은 Transactional outbox Pattern 이라고 한다.

 

[ 스프링 애플리케이션 이벤트 발행 ]

@Transactional
public ProjectResponse createProject(ProjectCreateRequest request) {
    Project project = request.toProject(UUID.randomUUID());
    codeStoreValidationService.validateToApply(project);

    Project saveProject = projectRepository.save(project);
    eventPublisher.publish(ProjectEvent.of(saveProject, EventType.CREATED));
    return ProjectResponse.of(saveProject);
}

@Transactional
public void publish(ProjectEvent event) {
    eventPublisher.publishEvent(event);
}

 

[ 이벤트 저장 ]

@Transactional
@EventListener
public void handleEvent(ProjectEvent event) {
    projectEventRecordUseCase.record(event);
}

 

이벤트 재발행

구독자들이 이벤트를 정상적으로 처리하더라도, 이벤트 처리를 잘못할 수 있기 때문에 언제든 이벤트를 재발행 해줄 수 있어야 하고 이 때 구독자들이 원하는 이벤트들의 형태는 자유롭다.

따라서 다음과 같은 조건을 고려해 이벤트 형태를 설계한다.

 

이벤트 발행을 보장하기 위해 이벤트가 발행되었는지 확인할 수 있어야 한다.

특정 회원, 특정 행위, 특정 속성 변화, 특정 기간을 조회하여 재발행할 수 있어야 한다.

 

이벤트 형태

create table project_event (
   id bigint not null auto_increment,
   aggregate_id varbinary(16),
   event_type varchar(255),
   created_at datetime(6),
   published bit not null,
   published_at datetime(6)
)

 

정상 발행되지 않은 이벤트는 이벤트 발행 감지 배치를 통해 자동 재발행 처리되도록 한다.

 

(2) 관심사 분리

이벤트를 메시징 시스템으로 전달하는 것은 도메인에게는 관심사가 아니지만 시스템에서는 중요한 정책다. 이런 경우 도메인 정책에 변경없이 트랜잭션을 확장하여 구독자의 행위를 트랜잭션 내에서 처리되도록 변경할 수 있다.

 

[ 메시지 전달 ]

@Transactional
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleEvent(ProjectEvent event) {
    messagePublisher.publish(ProjectEventPayload.from(event));
}

 

2) 두번째 구독자 계층

(1) 이벤트 발행 보장

첫번째 구독자 계층에서 최초 이벤트를 기록할 때는 발행 여부를 false로 저장하였다. 두번째 구독자 계층에서 이벤트 발행 여부를 기록하는 구독자를 추가하여 데이터를 업데이트 처리한다. 발행된 이벤트는 삭제하지 않고 발행 여부만 true로 변경한다.

 

[ 이벤트 발행여부 업데이트 ]

@SqsListener(value = "sqs-project-event-publish-record.fifo", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void recordEventPublish(@NotificationMessage EventPayload payload) {
    log.info("listen from sqs-project-event-publish-record.fifo: {}", payload);
    projectEventRecordUseCase.recordPublish(Long.valueOf(payload.getEventId()));
}

 

(2) 비관심사 분리

도메인 행위가 수행될 때 함께 수행되어야 하는 정책들이 있을 수 있다. 이러한 부가 정책들이 도메인의 주 행위인 것으로 착각될 수 있으며, 의존성 관계를 확장시키고 도메인의 주 행위에 대한 응집을 방해하게 된다.

따라서 주요 기능을 찾고 비관심사를 분리하여 도메인 행위의 응집을 높이고 비관심사에 대한 결합을 느슨하게 만들어야 한다.

 

예를 들어 로그인 프로세스에서 부가 기능은 다음과 같을때 비 관심사로 분리한다.

  • “동일 계정 로그인 수 제한” 규칙에 따라 동일 계정이 로그인된 타 디바이스 로그아웃 처리
  • 회원이 어느 디바이스에서 로그인되었는지 기록
  • 동일 디바이스의 다른 계정 로그아웃 기록
@SqsListener(value = "${sqs.login-device-login}", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void loginDevice(@Payload MemberLoginApplicationEvent payload) {
    devices.login(payload.getMemberNumber(), payload.getDeviceNumber());
}

@SqsListener(value = "${sqs.login-member-other-device-logout}", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void logoutMemberOtherDevices(@Payload MemberLoginApplicationEvent payload) {
    devices.logoutMemberOtherDevices(payload.getMemberNumber(), payload.getDeviceNumber());
}

@SqsListener(value = "${sqs.login-other-member-device-logout}", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void logoutOtherMemberDevices(@Payload MemberLoginApplicationEvent payload) {
    devices.logoutOtherMemberDevices(payload.getMemberNumber(), payload.getDeviceNumber());
}

 

(3) 외부 이벤트 발행

MSA 를 위한 외부 시스템과의 관심사 분리를 위한 외부 이벤트 발행이 필요하다. 외부 시스템에 이벤트를 전파하는 행위 또한 도메인 내에 존재하던 비관심사로 볼 수 있다.

@SqsListener(value = "sqs-change-project-broadcast.fifo", deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void handleBroadcast(@NotificationMessage ProjectEventPayload payload) {
    log.info("listen from sqs-change-project-broadcast.fifo: {}", payload);
    messageBroadcaster.broadcast(ProjectBroadcastMessage.from(payload));
}

 

3) 세번째 구독자 계층

세번째 구독자 계층은 애플리케이션의 외부 이벤트를 구독하여 처리한다.

커맨드와 쿼리를 서로 분리해야 하는 필요에 따라 쿼리 서비스를 만들어 이벤트를 구독하도록 할 수 있다.

예를 들어, 여러 서비스에 흩어진 데이터를 조회하는 경우, 데이터를 저장하는 서비스와 별도로 효율적으로 쿼리하는 DB가 필요한 경우에 데이터를 저장하는 서비스가 발행한 이벤트를 구독하여 항상 최신 상태로 유지되는 DB를 쿼리하는 서비스를 만들 수 있다. CQRS 참고

 

 

정리

애플리케이션 이벤트를 통해 이벤트 발행을 보장할 수 있고 내부 이벤트를 통해 비관심사를 분리할 수 있다.

Github

 

 

 

참고

마이크로서비스 패턴

회원시스템 이벤트기반 아키텍처 구축하기

728x90

'MSA' 카테고리의 다른 글

트랜잭셔널 메시징(아웃박스, 폴링 발행기)  (0) 2022.10.22