← Posts

Event-Driven · Architecture · Messaging

이으면 생기는 문제들 - 이벤트 패턴 지도

쪼개고 비동기로 이으면 편해지는 대신 새 문제가 생긴다. 중복·발행 누락·분산 트랜잭션·복잡한 조회 - 벽마다 정해진 패턴이 있다. 그 이름들의 지도를 먼저 편다.

목차
  1. 쪼개고 이었더니 새 문제가 생겼다
  2. 문제 하나: 같은 메시지가 두 번 온다
  3. 문제 둘: 저장은 됐는데 발행이 안 됐다
  4. 문제 셋: 하나의 일이 여러 서비스에 걸친다
  5. 문제 넷: 조회가 점점 복잡해진다
  6. 문제 다섯: 어쩌다 그 상태가 됐는지가 없다
  7. 이 패턴들은 공짜가 아니다
  8. 그래서 언제 꺼내나
  9. 정리

서비스를 쪼개고 발행/구독으로 이으면 결합이 느슨해진다. 대신 새 문제가 딸려 온다. 이 시리즈는 그 문제마다 붙은 패턴의 이름을 다룬다. 먼저 지도를 편다.

쪼개고 이었더니 새 문제가 생겼다

한 프로세스 안에서 함수를 부르면 셋이 공짜였다. 호출은 즉시 돌아오고, 실패하면 그 자리에서 알고, 하나의 트랜잭션이 전부를 묶었다.

서비스를 쪼개 메시지로 이으면 이 셋이 다 깨진다. 응답은 나중에 오고, 실패도 나중에 오고, 트랜잭션은 경계에서 끊긴다. 이게 message-queue 글microservices 글이 “대가”로 나열한 것들이다. 이 시리즈는 그 대가를 감당하는 방법이다.

문제 하나: 같은 메시지가 두 번 온다

메시지 시스템은 대개 “적어도 한 번은 전달한다”를 보장한다. 확실히 전달하려다 보니 애매할 때 다시 보낸다. 그래서 같은 메시지가 두 번 도착한다.

받는 쪽이 그대로 두 번 처리하면 결제가 두 번 되고 재고가 두 번 빠진다. 해법의 이름은 멱등 소비자 - 두 번 와도 한 번과 같게 삼킨다. 멱등 소비자 글에서 다룬다.

문제 둘: 저장은 됐는데 발행이 안 됐다

주문을 DB에 저장하고, 이어서 “주문 생성됨” 이벤트를 발행한다. 그런데 저장 직후 프로세스가 죽으면? DB엔 주문이 있는데 이벤트는 안 나갔다. 반대 순서면 이벤트는 나갔는데 주문이 없다.

넣었는데 안 넣어진 경우두 저장소에 나눠 쓰는 문제(dual-write)라 부른다. 해법의 이름은 아웃박스 - 저장과 발행을 한 트랜잭션으로 묶는다. 아웃박스 글에서 다룬다.

문제 셋: 하나의 일이 여러 서비스에 걸친다

“주문한다”는 한 줄이 결제·재고·배송 세 서비스를 건드린다. 한 트랜잭션으로 묶고 싶지만, 데이터가 갈려 있어 그럴 수 없다. 결제는 됐는데 재고가 없으면 결제를 되돌려야 한다.

어긋난 걸 누가 되돌리나의 이름이 사가(Saga) - 여러 로컬 트랜잭션을 잇고, 실패하면 보상으로 되돌린다. 잇는 방식이 둘이라 코레오그래피오케스트레이션 두 글로 나눠 다룬다.

문제 넷: 조회가 점점 복잡해진다

쓸 때 좋은 데이터 모양과 읽을 때 좋은 모양은 다르다. 쓰기는 정규화된 작은 덩어리가 편하고, 조회는 여러 곳을 합친 넓은 화면이 편하다. 한 모델로 둘 다 하려면 어느 한쪽이 고생한다.

해법의 이름은 CQRS - 쓰는 모델과 읽는 모델을 아예 가른다. CQRS 글에서 다룬다.

문제 다섯: 어쩌다 그 상태가 됐는지가 없다

DB엔 보통 지금 상태만 남는다. 잔액이 5만원인 건 알아도, 그게 어떤 입출금을 거쳐 왔는지는 덮어써져 사라진다. 감사·추적·되감기가 필요하면 곤란하다.

해법의 이름은 이벤트 소싱 - 상태 대신 일어난 일의 목록을 진실로 저장하고, 필요하면 재생해 상태를 얻는다. 이벤트 소싱 글에서 다룬다.

이 패턴들은 공짜가 아니다

지도를 펴 보면 공통점이 하나 보인다. 이 패턴들은 대부분 “지금 당장 완벽히 일치”를 포기하고 “잠깐 어긋났다가 곧 맞춤”을 받아들인다. 아웃박스의 이벤트도 릴레이가 보낼 때까지 늦고, 사가도 보상이 돌 때까지 어긋나 있고, CQRS의 읽기 모델도 잠깐 옛 값을 보여준다.

이 “잠깐 어긋남”이 바로 결과적 정합성이다. 그래서 이 시리즈의 밑에는 consistency 글이 늘 깔려 있다 - 이벤트 패턴은 결과적 정합성을 실제로 구현하는 방법이라고 봐도 된다.

그래서 언제 꺼내나

이름을 안다고 다 써야 하는 건 아니다. 이 패턴들은 비동기로 이은 시스템의 문제를 푼다. 한 서비스, 한 DB로 충분하면 트랜잭션 하나가 위의 다섯 문제를 전부 안 생기게 한다.

  • 아직 안 쪼갰다면 아웃박스·사가는 필요 없다. 로컬 트랜잭션이 다 해준다.
  • 쪼갰지만 조회가 단순하면 CQRS·이벤트 소싱은 과하다.
  • 문제가 실제로 나타났을 때 해당 패턴을 꺼낸다. 이름을 알아두는 값은 “그 문제엔 이미 답이 있다”를 아는 것이다.

정리

  • 쪼개고 비동기로 이으면 즉시·그 자리·한 트랜잭션이라는 세 공짜가 깨진다.
  • 그 자리마다 패턴이 있다 - 중복엔 멱등 소비자, 발행 누락엔 아웃박스, 분산 트랜잭션엔 사가, 복잡한 조회엔 CQRS, 이력엔 이벤트 소싱.
  • 공통 밑바닥은 결과적 정합성 - “잠깐 어긋났다 곧 맞춤”을 받아들이는 것이다.
  • 이름을 아는 것과 쓰는 것은 다르다. 문제가 실제로 나타났을 때 꺼낸다.

다음 글은 첫 번째 벽 - 같은 메시지가 두 번 와도 한 번처럼 삼키는 멱등 소비자로 간다.

관련 글