개발/Spring

[MSA] Outbox Pattern

joon95 2022. 11. 4. 13:58
반응형

MSA 아키텍처?

마이크로서비스아키텍처에 대한 화두가 널리 퍼진지 한 8년정도 된 것 같다.

대학교 1학년 때(2014년도)에 쿠버네티스에 대해 알게되어 도커컨테이너에 대해 공부했었던 기억이 있는데,

인프라(infrastructure)영역은 쿠버네티스, 오픈시프트(redhat), 탄주(vmware) 등의 오케스트레이터를 통해 널리 사용하고 있는 것 같다.

 

그럼 이제 어플리케이션영역이 마이크로서비스화 되어야한다.

필자는 작년 한 해동안 마이데이터 제공자 API 프로젝트를 openshift 기반 환경에서 진행하였는데,

redhat 기반이라 해당 플랫폼에서 제공하는 3scale(API G/W), rhsso(인증), Fuse, Jboss 를 사용하여 컨테이너를 운용했다.

하지만 마이데이터 제공자는 단순히 Read API를 제공하는 것이기 때문에 복잡한 비즈니스 로직을 가져가진 않았다.

 

그럼 어플리케이션영역의 마이크로서비스는 어떻게 해야할까?

먼저 비즈니스 로직을 도메인별로 분류하고 구분지어야 한다.

실제 MSA 서비스로의 전환 사례는 배달의민족 기술블로그 글에 잘 설명되어 있으니 한번 읽어보길 바랍니다.

 

회원시스템 이벤트기반 아키텍처 구축하기 | 우아한형제들 기술블로그

{{item.name}} 최초의 배달의민족은 하나의 프로젝트로 만들어졌습니다. 배달의민족의 주문수는 J 커브를 그리는 빠른 속도로 성장했고, 주문수가 커지면서 자연스럽게 트래픽 또한 매우 커졌습니

techblog.woowahan.com

 

MSA 중 Outbox 패턴

마이크로 서비스 아키텍처의 종류는 여러가지가 있다.

- SAGA 패턴(Choreography, Orchestration)

- CQRS 패턴

- OUTBOX 패턴

- Event Sourcing

... 

여러개가 나오는데 필자는 이번에 Outbox 패턴과 SAGA패턴 2가지를 해보았고,

이 포스팅에선 Outbox 패턴에 대한 포스팅을 할 것이다.

 

그럼 이 친구는 무엇일까? Outbox 패턴에서의 핵심은 kafka connector 라는 놈이다.

이 친구가 database의 트랜잭션 로그를 읽어들여 카프카 토픽에 정보를 보내고, 나머지 마이크로서비스가 토픽을 읽어 back 작업을 하게 된다.

 

카프카 커넥터는 Confluent 라는 유료제품과 Debezium 무료제품이 있다.

필자는 Debezium(데브지움)을 사용하였고 사이트에서 설명하는 아키텍처는 아래와 같다.

Debezium Kafka Connector Architecture

말 그대로 db 트랜잭션 로그를 읽어 다른곳에 연결해주는 역할이다.

 

Outbox 아키텍처

예제 소스는 링크에서 가져왔는데, 

springboot application까지 image로 만들어 1개의 docker network 안에서 통신하는 예제로

디버깅을 하면서 체크할 수 없게 구현을 해놓았다.

 

그래서 개발자의 pc에서 docker network에 어떻게 접근하여 통신이 이루어지는지에 대한 그림을 그렸다.

MSA outbox pattern

1. 클라이언트가 주문서비스에 주문을 요청한다.

2. 주문서비스는 주문(customer_order), 주문이벤트(outbox) 테이블에 insert 한다.(이때 outbox 테이블은 바로 delete 함)

3. kafka 커넥터는 database의 특정 테이블(oubox)의 트랜잭션로그를 연결(connect)하고 있기 때문에 변화를 감지한다.

4. kafka 커넥터는 특정 테이블(outbox)의 트랜잭션을 kafka에 날린다.(이 때 보내는 토픽명은 orders_server.orders.outbox : 커넥터에정의한db명.db명.table명)

5. 배달서비스는 위 토픽을 구독하고 있기 때문에 데이터를 가져가 처리한다.

 

docker-compose

위에서 그린 그림을 docker 컨테이너로 구현해 보자.!!!

 

- mysql : 데이터베이스

- zookeeper : 카프카관리용

- kafka : 카프카서버

- kafka maanger : 모니터링용

- kafka connector : 카프카커넥터

총 5개의 컨테이너를 구동할 것이다.

 

많은 시행착오를 겪었는데, docker network bridge(default)로 사용하면 dns 구성이 안되어 container 끼리 통신이 안되었다. 그래서 network 를 새로 만들면 auto dns 부분이 적용되 컨테이너끼리 통신이 된다고 한다. 

그래서 docker compose 를 사용하면 어떻게 되나 찾아보니 bridge network를 자동으로 생성하여 각 도커서비스의 이름으로 dns를 이름에 맞게 구성할 수 있다.

 

또, 이번에 알았는데 window docker는 host 모드를 못쓴다고 한다.

 

카프카, 주키퍼 이미지를 debezium 꺼로 하니 연결이 잘안되어 bitnami 이미지로 변경하였다

 

그리고 depends_on 옵션을 사용해 먼저 기동되야 할 컨테이너의 의존성을 걸 수 있다.(docker --link 옵션)

kafka 쪽은 docker 네트워크 안에선 kafka:29092 로 통신하도록하고, 외부에선 localhost:9092로 접근하도록 옵션을 준다.

version: '3'
services:
  zookeeper:
    hostname: zoo
    image: 'bitnami/zookeeper:latest'
    ports:
      - '2181:2181'
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes
  kafka:
    hostname: kafka
    image: 'bitnami/kafka:latest'
    ports:
      - '9092:9092'
    privileged: true
    environment:
      - KAFKA_BROKER_ID=1
      - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      - KAFKA_LISTENERS=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://kafka:9092
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
      - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
      - ALLOW_PLAINTEXT_LISTENER=yes
    depends_on:
      - zookeeper
  mysql:
    image: mysql
    hostname: mysql
    ports:
      - "3307:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
  manager:
    image: sheepkiller/kafka-manager
    ports:
      - 9000:9000
    environment:
      - ZK_HOSTS=zookeeper:2181
    depends_on:
      - zookeeper
  
  connect:
    links:
      - zookeeper
      - kafka
    ports:
      - 8083:8083 
    environment:
      - GROUP_ID=1 
      - CONFIG_STORAGE_TOPIC=my_connect_configs 
      - OFFSET_STORAGE_TOPIC=my_connect_offsets 
      - STATUS_STORAGE_TOPIC=my_connect_statuses 
      - BOOTSTRAP_SERVERS=kafka:29092
    image: quay.io/debezium/connect:1.9
    depends_on:
      - mysql
      - kafka
      - zookeeper

 

kafka Connector 연결

connect 컨테이너에 접근해 POST /connectors 에 생성요청을 해야한다.

curl -i -X POST -H "Accept:application/json" \
-H  "Content-Type:application/json" http://localhost:8083/connectors \
-d '{
    "name": "orders-connecter",
    "config": {
        "connector.class": "io.debezium.connector.mysql.MySqlConnector",
        "tasks.max": "1",
        "database.hostname": "mysql",
        "database.port": "3306",
        "database.user": "root",
        "database.password": "root",
        "database.server.id": "100",
        "database.server.name": "orders_server",
        "database.include.list": "orders",
        "table.include.list":"orders.outbox",
        "database.history.kafka.bootstrap.servers": "kafka:29092",
        "database.history.kafka.topic": "schema_changes.orders",
        "transforms": "unwrap",
        "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState"
    }
}'

docker compose 를 이용해 각 컨테이너의 dns를 세팅해주었기 때문에 데이터베이스와 카프카서버의 접근정보를 다음과 같이 기입하였고, 사용할 커넥터명, 연결 할 데이터베이스, 테이블명, 트랜잭션변경을 기록할 토픽을 정의해주면 된다.

 

연결된 커넥터 확인은 동일한 주소에 GET 으로 요청하면된다.

 

kafka Manager

카프카 클러스터의 정보를 GUI 환경으로 보기 위한 툴 manager를 설정해보자.

localhost:9000 에 접근한 뒤 Add Cluster 버튼을 눌러 추가한다.

kafka manager cluster add

host 정보는 zookeeper 를 입력하거나 host.docker.internal 을 사용해야한다.

왜냐하면 매니저컨테이너에서 주키퍼서버로 연결을 하기 때문이다.

가볍게 localhost:2181 을 써주면 매니저컨테이너에서 자신의 컨테이너의 2181로 binding 하기때문에 error가 발생한다.

 

이렇게 등록된 클러스터를 보면 토픽정보를 볼 수 있다.

kafka manager dashboard

테스트

자 스프링부트를 기동하고 주문을 요청해보자.

curl --location --request POST 'http://localhost:8080/order' \
 --header 'Content-Type: application/json' \
 --data-raw '{
               "name": "joon",
               "quantity": 95
             }'

데이터베이스에 접근해 생성된 데이터를 확인하면.

show databases;
use orders;
show tables;

select * from customer_order;
select * from outbox;

customer_order table

customer_order 테이블과 outbox 테이블에 insert 한 뒤

outbox 테이블은 delete 를 날린다.

(주문서비스에서 stdout 로그를 남기고 있음.)

Outbox [id=4, event=order_created, eventId=3, payload={name=joon, quantity=95}, createdAt=2022-11-03T10:50:33.784361600]

 

이 때 kafka connector 는 outbox 테이블에 들어온 트랜잭션로그를 읽어 kafka에 보내는데

토픽의 내용을 보면 아래와 같다.

# order_server.orders.outbox 
{"schema":{"type":"struct","fields":[{"type":"int32","optional":false,"field":"id"},{"type":"string","optional":true,"field":"event"},{"type":"int32","optional":true,"field":"event_id"},{"type":"string","optional":true,"name":"io.debezium.data.Json","version":1,"field":"payload"},{"type":"string","optional":true,"name":"io.debezium.time.ZonedTimestamp","version":1,"field":"created_at"}],"optional":false,"name":"orders_server.orders.outbox.Value"},"payload":{"id":3,"event":"order_created","event_id":3,"payload":"{\"name\":\"joon\",\"quantity\":95}","created_at":"2022-11-03T10:50:34Z"}}

 

이제 딜리버리서비스는 해당 토픽을 읽어 Payload 값을 출력한다.

KafkaMessage [payload=PayLoad [id=3, event=order_created, eventId=3, payload={"name":"joon","quantity":95}, createdAt=2022-11-03T10:50:34Z]]

 

마치며

마이크로서비스아키텍처를 그동안 들어만 봤지 어떻게 구성하고 유지하는지에 대해 감이 잡히지 않았는데

SAGA패턴과 outbox 패턴을 구현해보면서 조금 알것 같다.

현재 MSA 프로젝트에서 이러한 패턴을 정의하고 비즈니스 로직을 수립해 나아가고 있는데

결국 다른 개발자들의 역량이 어느정도 받쳐주어야 가능할 것 같다는 생각이 든다.

 

조만간 SAGA 패턴 2가지에 대해 포스팅을 남길 예정이다.

반응형