비관락의 한계
- 한 트랜잭션이 락을 점유하고 있는 동안 다른 트랜잭션은 모두 큐에 막혀있는 상태
- 일정 수준 이상의 트래픽이 넘어가면 큐에 막히는 요청 수 증가율 > 스프링이 처리하는 요청 수 가 되면서 큐에 막힌 요청들이 Request Time Out이 발생하며 요청이 실패해버림
두번째 해결방법 : Outbox 패턴 + 비동기 집계
기본전제 요구사항 : 팔로워 수는 굳이 실시간으로 처리되지 않아도 괜찮다. (준실시간성 데이터로 가정)
- 즉, 특정 사용자가 팔로잉을 누르자마자 다른 사용자가 해당 타겟 유저의 팔로잉 수를 확인할 때, count가 맞지 않아도 서비스 자체에 큰 결함은 없다.
근본적인 문제 : 팔로잉 요청 API 안의 한 트랜잭션 내부에서 User 테이블과 Following 테이블을 모두 Write하고 있음
⇒ 동기 트랜잭션에서는 Following 테이블에 대해서만 Write 연산을 수행하고, User 테이블에 대한 Write 연산은 Event를 통해 비동기로 처리하자!
1차 설계 흐름
팔로잉 요청 가정
- 팔로잉 요청 API의 서비스 코드에서는 Following 테이블의 INSERT 연산만 수행 → User의 S-Lock만 획득
- 팔로잉 요청 이벤트 발행
- (비동기) 팔로잉 요청 트랜잭션이 커밋되면 팔로잉 요청 이벤트 리스너가 새로운 트랜잭션에서 User 테이블의 followerCount를 업데이트
만약 이벤트를 발행했는데 서버에 문제가 생기면?
⇒ 트랜잭션은 커밋되었는데, 이벤트가 유실되어 DB 데이터 정합성이 맞지 않는 문제 발생!
따라서, Outbox 패턴을 도입한다!
<aside>
☝🏼
Outbox 패턴이란?
MSA(Microservice Architecture) 환경에서 주로 사용되는 데이터 정합성을 맞추기 위한 패턴입니다. 어떤 서비스가 자신의 데이터베이스에 변경을 가한 후, 외부 시스템(다른 마이크로 서비스 등)에 메시지(이벤트)를 안정적으로 발행해야 할 때 사용됩니다.
동작 방식
- 서비스는 비즈니스 로직을 처리하고, 관련 데이터를 자신의 데이터베이스에 저장합니다.
- 동시에, 발행해야 할 메시지(이벤트)를 같은 데이터베이스 내의 별도 테이블(outbox_events 등)에 저장합니다.
- 이 두 작업은 하나의 원자적인 트랜잭션으로 묶입니다.
- 별도의 비동기 프로세스(Scheduler, Message Relay 등)가 outbox_events 테이블을 주기적으로 폴링(polling)하여 아직 처리되지 않은 이벤트를 가져옵니다.
- 가져온 이벤트를 실제 목적지(메시지 큐, 다른 서비스 등)로 안정적으로 전달하고, 전달이 완료되면 outbox_events 테이블에서 해당 이벤트를 처리 완료 상태로 변경하거나 삭제합니다.
장점
- 이벤트 유실 방지(At-Least-Once Delivery): 비즈니스 로직 처리와 이벤트 저장이 하나의 트랜잭션으로 묶여있어, 둘 중 하나만 성공하는 경우가 발생하지 않습니다. 만약 이벤트 발행에 실패하더라도 outbox_events 테이블에 기록이 남아있어 재처리가 가능합니다.
- 서비스 간 결합도 감소: 이벤트를 발행하는 서비스는 메시지 큐나 외부 시스템의 상태에 의존하지 않고 자신의 로직만 처리하면 됩니다.
</aside>
2차 설계 흐름 (Outbox 패턴 도입)