
난이도 ⭐️
작성 날짜 2025.09.23
찾아본 이론을 바탕으로 적용해봤는데, 진짜 작동하는지 궁금하다!
🤔 그래서 트랜잭션 아웃박스 패턴 쓰면 얼마나 좋은 건데?


우선 Test 용 Mock 클래스를 만들어서 Profile 어노테이션을 달아주었다.
이렇게 하면 profile 환경 변수가 dev일 때는 Test~ 클래스가, dev가 아닐 때는 원래 클래스가 실행된다.
이럴 때는 참 헥사고날이 좋다 ㅠㅠ


Mock 클래스로 바꿔 준 이유는 몇 가지 상황을 설정하기 위해서이다.
일단 실제 OpenAI API를 사용하지 않도록 한다.
몇 천 건의 요청을 진짜 보낸다면.. 내 토큰...
그리고 OpenAI의 경우 40% 확률로 실패, Redis Stream 발행은 20%의 확률로 실패하는 상황을 만들었다.
이제 테스트를 해보자.
브랜치를 나눠서
1) 아웃박스 패턴을 적용하기 전
2) 아웃박스 패턴을 적용
으로 분리하였다.

단위 테스트는 K6로 진행하였다.
많은 양의 API 부하테스트를 진행하기에 편리한 툴이라 종종 사용하고 있다!
20명의 유저가 3분 간 지속적으로 API 요청을 보내는 상황으로 설정.
이를 위해 트랜잭션 아웃박스 패턴에서 사용하는 Scheduler의 주기도 1분으로 단축하였다.
테스트 결과 지표는 다음과 같이 설정하였다.
메시지 처리 비율 = (ASSISTANT의 메시지 수) / (USER + ASSISTANT의 메시지 수)
처음에는 아웃박스 엔티티의 상태 비율로 하려고 했는데 그러면 아웃 박스 적용 전에선 결과를 구할 수 없다.
USER의 메시지는 API 요청이 오자마자 저장된다.
그리고 ASSISTANT의 메시지는 OpenAI API 요청이 성공해야만 저장된다.
따라서 이 두 가지의 지표로 처리 비율을 계산할 수 있다.
각 테스트가 종료되면 스트림에 남아있는 Pending 메시지로 인하여 테스트 간 간섭이 발생하지 않도록 스트림을 제거 후 재생성해주었다.

위에서 언급한 결과 비교 지표 확인을 위해 아래의 SQL 쿼리를 실행하였다.
SELECT
SUM(CASE WHEN cm.sender_type = 'USER' THEN 1 ELSE 0 END) AS user_count,
SUM(CASE WHEN cm.sender_type = 'ASSISTANT' THEN 1 ELSE 0 END) AS assistant_count,
ROUND(
SUM(CASE WHEN cm.sender_type = 'ASSISTANT' THEN 1 ELSE 0 END) /
NULLIF(SUM(CASE WHEN cm.sender_type = 'USER' THEN 1 ELSE 0 END), 0),
2
) AS assistant_to_user_ratio
FROM chat_message_entity cm;
트랜잭션 아웃박스 패턴만의 효과를 확인하기 위해 우선 재시도 로직을 제거하여 테스트하였다.
이후 재시도 로직을 추가하여 실제 재시도 로직의 효과를 비교할 예정이다.
K6 테스트 결과

미리 설정해둔 20% 확률의 Stream 메시지 발행 실패를 확인할 수 있다.
OpenAI API의 실패는 Consumer가 비동기로 처리하기 때문에 REST API 요청 단계에서는 확인할 수 없다.
SQL 쿼리 실행 결과

이론값과 근사한 값이 나왔다.
Stream 발행 성공률 = 0.8
OpenAI 성공률 = 0.6
예상 최종 성공률 = 0.8 × 0.6 = 0.48 (48%)
이론 값과 오차율 1%p 정도
최종 실패율은 1 - 0.49 = 0.51, 즉 51%이다.
아웃 박스 메시지들을 실시간으로 확인해봤는데,


이렇게 점점 줄어든다. (스케줄러가 계속 확인해 처리해주기 때문)
K6 테스트 결과

여기서는 전체의 API 요청이 성공하였는데,
이는 트랜잭션 아웃박스 패턴을 적용하면서 내부 로직을 바꿨기 때문이다.

어차피 publish에서 예외가 터져도 Outbox는 이미 저장된 상태이기 때문에, Exception을 삼켜도 나중에 Scheduler에 의해 처리된다.
이는 클라이언트에서도 서버 내부의 일시적인 오류로 인한 불필요한 500 응답을 막아주는 역할도 할 수 있다.
SQL 쿼리 실행 결과
처음 종료 시

일정 시간 이후

처음 종료 시에는 80% 대가 나오다가 이후 100%를 찍었다.
적용 전보다 살짝 전체 숫자가 적긴 한데 아웃박스 엔티티의 입출력 단계에서 발생하는 커넥션이나 스레드의 블로킹으로 예상된다.
최종 결과 성공률 100%, 실패율 0%
K6 테스트 결과

SQL 쿼리 실행 결과

약 78%의 성공률을 확인할 수 있다.
이 역시 이론값과 유사하다.
재시도 로직은 3회 수행되기 때문에,
P(OpenAI 최종 성공)=1−P(4번 모두 실패)=1−0.44=1−0.0256=0.9744≈97.44%
따라서 스트림 발행 성공률인 80%와 곱했을 때,
P(최종 성공)=P(Stream 성공)×P(OpenAI 최종 성공)=0.8×0.9744≈0.7795≈78%
로 오차가 매우 적다.
따라서 최종 성공률 78%, 실패율 22%
K6 테스트 결과

SQL 쿼리 실행 결과

성공률 100%, 실패율 0%
다만 재시도 로직 적용 전 보다 결과를 더 빨리 확인할 수 있다는 장점이 있다.
실패율 기준
| 시나리오 | 트랜잭션 아웃박스 도입 전 실패율 | 트랜잭션 아웃박스 도입 후 실패율 | 개선 정도 |
|---|---|---|---|
| 재시도 로직 제거 | 51% | 0% | 51%p 개선 |
| 재시도 로직 추가 | 22% | 0% | 22%p 개선 |
처리 메시지 수 기준
| 시나리오 | 재시도 | 아웃박스 도입 전 | 아웃박스 도입 후 | 개선 메시지 수 | 개선률 |
|---|---|---|---|---|---|
| 재시도 로직 제거 | 없음 | 1,720 | 3,491 | 1,771 | 103% ↑ |
| 재시도 로직 추가 | 있음 | 2,756 | 3,481 | 725 | 26% ↑ |
결론
트랜잭션 아웃박스 패턴과 재시도 로직으로 실패율 0%를 달성할 수 있었다!
머리 속의 이론을 활용하여 적용한 결과를 눈으로 확인할 수 있어서 시간을 헛 쓰진 않았구나 하는 안도감이 들었다..ㅎㅎ그리고 코드 캡처할 때 인텔리제이 플러그인 Easy Code Screenshots 처음 써봤는데 캡처도 깔끔하고 너무 편하다... 진작 쓸걸