Blacklist에 Outbox 패턴을 도입하고 잠시 잡담

ext-authz의 성능을 극대화하는 방향으로 기존 로그아웃 로직(Redis 직접 조회)을 로컬 캐시 조회로 수행하도록 디벨롭했다.
위 작업이 기존의 Persistence Layer(Redis) 직접 접근과 동일하게 수행되려면 로컬 캐시와 레디스 간의 정합성을 맞춰야한다.
절차를 안내하면 아래와 같다.
- Logout 엔드포인트 호출
- Auth에서 Redis에 직접 접근해 Blacklist에 JWT 저장
- Logout Event 발행 (Publisher)
- RabbitMQ에서 Fanout
- ext-authz에서 Event 수신 (Subscriber)
- 로컬 캐시 업데이트
위 방식으로 Eventual Consistency를 구성하면, 대다수의 케이스에서 정합성이 유지가 된다. 성능은 RPS 기준 1200->1500으로 +25% 상승했다.
로컬 캐시로 전환하면서 Blacklist 판단이 마이크로초 단위로 떨어졌지만 네트워크 병목이 100ms라 보다 드라마틱한 상승은 보지 못했다.
Outbox를 도입하게 된 경위는 이벤트 전달 실패 시 재시도 로직이 빈약해서였다. 별도의 로깅 스트림없이 RabbitMQ와 Redis만으로 이벤트 기반 동작을 수행하고 있어서 3번 절차인 이벤트(메시지) 전달이 실패하면 로컬 캐시가 업데이트가 되지 않는다.
보완하는 방안으로 실패한 메시지는 Persistence Layer인 Redis에 기록해두고, 이를 기반으로 이벤트를 재발행하도록 디벨롭했다. e2e 테스트를 거치고, 관측 데이터 자체는 문제없이 나왔지만 Outbox가 추가되면서 코드베이스가 그다지 깔끔하지 않게 떨어졌다.
Auth 서버에서 단일로 Redis만 끌어오던 때와 달리 Message Queue도 가져와야하고, Redis도 Outbox와 Blacklist로 용도가 나뉘어지는 걸 더해, Outbox 로직을 수행할 Relay Worker도 추가되니 기존의 레이어드 아키텍처로 관리하기엔 난해한 면이 존재했다.
domains/auth/
├── jobs/ # DB 초기화 스크립트
│ └── init_db.py
├── tasks/ # 백그라운드 워커
│ └── blacklist_relay.py
├── database/ # DB 연결/세션
├── interfaces/ # 추상화 인터페이스
└── services/ # 비즈니스 로직 기존 방식은 domains/auth 아래 service/database/api 등을 flat하게 뒀었다.
그렇지만 Outbox처럼 Integration, Persistence가 중첩된 경우 특정 레이어에 단일로 배치하기 모호했다.
# 초기 구현
class BlacklistEventPublisher:
def __init__(self, amqp_url: str):
self.amqp_url = amqp_url
# Redis 직접 의존 - Clean Architecture 위반!
self._redis = redis.from_url(os.getenv("AUTH_REDIS_URL"))
def _queue_to_outbox(self, event: dict):
self._redis.lpush("outbox:blacklist", json.dumps(event))
이에 더해 Opus 4.5가 one-shot으로 뽑아낸 코드는 Outbox가 Redis에 직접 의존을 하고 있어서 DI가 위반되기도 했다. 이참에 작년 무신사 과제 테스트 때 시도했던 클린 아키텍처를 Auth에 적용하려고 한다. 메시지 큐와 이벤트, 클러스터, Observability가 마련되면서 E2E와 검증을 하기 용이해졌으니 학습하기 좋은 시기라 여긴다.
https://github.com/ivan-borovets/fastapi-clean-example/tree/master?tab=readme-ov-file#architecture-principles
GitHub - ivan-borovets/fastapi-clean-example: Practical Clean Architecture backend example built with FastAPI. No stateful globa
Practical Clean Architecture backend example built with FastAPI. No stateful globals (DI), low coupling (DIP), tactical DDD, CQRS, proper UoW usage. REST API, per-route error handling, session-base...
github.com
참고 예제로 사용할 자료다. 이코에코 클러스터에선 처음 적용하는만큼 웬만해선 규범에 따라 진행할 예정이다.
클러스터 현황에 맞게 일부 변경은 존재할 수 있다. Auth에서 클린 아키텍처 기반 파일 구조가 잡히면 MQ와 연관되는 도메인으로 리팩토링 범위를 확장하려고 한다. 그 과정에서 Persistence Layer와 비즈니스 로직 간의 직접 접근들도 대부분 사라질 듯 싶다.
현재는 character와 my 도메인만 Persistence 직접 접근이 제거된 상태다. (워커를 거쳐 배치큐로 쌓아 일괄 처리한다.)
이코에코의 클러스터가 점점 바라던 모습으로 향해가서 뿌듯하다. 현재 테스트 상으로 2500VUs, 1500 RPS가 베이스라인으로 잡힌 상황이니 이제 성능보다는 유지보수성을 올릴 때다.

어느덧 이코에코 작업도 두 달이 다 되어간다. 인프라 / 백엔드를 아울러서 진행한 만큼 작업 범위가 방대하다.
이벤트 버스(혹은 Event Relay)를 도출하고 구현하는 과정에서 많은 에너지를 쏟아서인지 심적으로 지친 상태다.
프로젝트에 적용한 개념들도 복기하며 기술부채도 해결해야하고, 그 과정을 포트폴리오로 정리도 해야한다.
내세우고 싶은 파트들을 추려서 압축하고 정돈을 할 필요가 있다. '다 도움이 되겠지.'라 여기면서도 걱정이 많다.
시간과 공수를 제외하고 투입한 순수 비용을 추산하면 중고차 한 대정도다. (800-1000만원)
그 비용과 시간이 휘발되지 않게, 오롯이 역량으로 어필하기 위해선 힘을 내야할 시기다. 너무 피로해지지 말자.