이 글은 1) 리액티브가 왜 실패하는지를 구조적 측면에서 설명하고, 2) 어떤 조건에서는 여전히 강력한지를 정의하며, 3) Kafka / 메시징이 이 문제를 어떻게 분리하는지를 아키텍처 관점에서 정리해본 것이다.
리액티브를 충분히 써 본 팀은 비슷한 지점에 도달한다.
도입 초기의 성과를 지나, 운영 복잡성과 성능 병목을 동시에 마주하는 시점이다.
1. 리액티브의 전제는 명확하다
“느린 I/O를 숨긴다”
즉, 1) 스레드를 오래 점유하지 않는다. 2) 대기 시간 동안 다른 일을 처리한다. 3)높은 동시성을 적은 리소스로 처리한다
이 모든 것은 하나의 전제 위에서만 성립한다.
I/O가 비동기·논블로킹일 것
CPU 작업, 네트워크 I/O, 이벤트 처리 파이프라인, 이 영역에서 리액티브는 압도적으로 효율적이다.
문제는 DB다.
2. DB는 리액티브 모델과 구조적으로 충돌한다
현실의 DB 접근은 다음 특성을 가진다.
1) JDBC 기반의 blocking I/O
2) 유한한 커넥션 풀
3) 트랜잭션이라는 상태 유지
4) 락, 인덱스, 디스크 I/O
즉, DB 앞에서는 결국 직렬화 압력이 발생한다.
리액티브 애플리케이션이 아무리 빠르게 이벤트를 처리해도 DB 앞에서는 다음이 발생한다.
1) 커넥션 풀 고갈
2) 요청 대기 큐 증가
3) 응답 지연 전파
4) tail latency 폭발
이 시점에서 개발조직은 깨닫는다.
“이건 리액티브의 문제가 아니라 DB라는 병목이 모든 걸 무력화한다”
3. 리액티브가 만들어내는 착시: 도입 초기에는 분명히 효과가 있다.
1) TPS 증가
2) 스레드 수 감소
3) CPU 사용률 개선
하지만 트래픽이 증가하거나 DB latency가 흔들리는 순간, 상황은 급변한다.
1) event-loop 지연
2) back-pressure 전파 실패
3) 전체 throughput 감소
그리고 그 시점에 남는 것은 개발 및 운영 복잡성이다.
(물론 개발 조직의 성숙도가 높은 곳이면, 고려대상에서 논외로 보아도 될 것이다.)
1) 디버깅 난이도 상승
2) 스택 트레이스 분절
3) 컨텍스트 전파 문제
4) 모니터링 난이도 증가
성능 이득이 사라진 상황에서 이 복잡성은 더 이상 정당화되지 않는다.
그래서 많은 팀이 리액티브를 포기한다.
4. 중요한 전제: 이건 “리액티브가 틀린 것”이 아니다
핵심은 이것이다. 리액티브는 DB 앞에서 무력해진다
왜냐하면 DB는 리액티브가 아니기 때문이다.
리액티브는 1) CPU, 2) 스레드, 3)이벤트 처리를 최적화한다.
하지만 병목은 항상 DB에 남아 있다.
이 병목의 위치를 바꾸지 않는 한, 리액티브는 구조적으로 한계를 가진다.
5. Kafka는 이 문제를 “해결”하지 않는다
대신 “분리”한다
여기서 Kafka가 등장한다.
Kafka는 DB를 빠르게 만들지 않는다.
락을 제거하지도 않는다.
Kafka가 하는 일은 단 하나다.
DB 병목을 동기 경로에서 제거한다
6. “기다림”이 “지연”으로 바뀐다
이 차이는 결정적이다.
- 기존 구조
→ DB가 느리면 요청이 멈춘다 - Kafka 구조
→ DB가 느리면 처리가 늦어질 뿐이다
결과적으로:
- API는 즉시 응답한다
- event-loop는 멈추지 않는다
- 부하는 큐에 쌓인다 (통제 가능)
시스템은 멈추지 않고, 대신 상태가 드러난다.
7. Back-pressure가 암묵적 → 명시적으로 바뀐다
리액티브의 back-pressure는 파이프라인 내부에서는 유효하다.
하지만 DB는 이를 이해하지 않는다.
Kafka에서는 다르다.
- consumer lag
- 처리량
- 파티션별 부하
- throttle 지표
즉, “지금 DB가 감당 못한다” 라는 사실이 숫자로 노출된다
병목이 숨겨지지 않는다. 이것이 운영 안정성의 차이다.
8. 트랜잭션 경계가 명확히 분리된다
- API 트랜잭션
→ Kafka publish까지만 - DB 트랜잭션
→ Consumer 내부에서 처리
이 분리로 인해:
1) 긴 트랜잭션 제거
2) 락 유지 시간 감소
3) 장애 전파 차단
리액티브 모델과 트랜잭션의 충돌 지점이 사라진다.
9. 그렇다면 리액티브는 언제 쓰는가?
조건은 명확하다.
1) DB 트랜잭션 빈도가 낮고
2) DB I/O가 20ms 이내로 안정적이며
3) 구조적으로 blocking 병목이 발생할 가능성이 낮은 도메인
예를 들면 기획전 / 전시, 카탈로그, 피드 / 랭킹, 추천 preview 등이 있다.
즉, DB가 사실상 메모리 확장처럼 동작하는 영역이다.
이 조건에서는 리액티브는 여전히 강력하다.
10. 동기 API + 내부 이벤트 처리 = 현실적 정답
외부 인터페이스는 단순하게 유지한다.
- 동기 API
- 명확한 SLA
내부는 다르게 설계한다.
- 이벤트 기반 처리
- Redis 캐싱
- DB는 최종 일관성 담당
이것이 Reactive를 API 스타일이 아니라
시스템 성질로 사용하는 방식이다.
11. jOOQ + JPA 조합은 자연스러운 CQRS를 만든다
- Command
- JPA
- 트랜잭션
- Object Graph
- Query
- jOOQ
- Flat read model
- 캐시 친화
CQRS를 규율로 강제하지 않아도
도구 선택만으로 자연스럽게 분리된다.
이건 성숙한 조직에서만 가능한 상태다.
12. WebSocket이 들어오는 순간, 그림은 완성된다
WebSocket은 요청/응답 모델이 아니다.
- push 기반
- event-driven
- 연결 수가 핵심 병목
Kafka + WebSocket 조합에서는:
- 상태 변경 이벤트 fan-out
- DB 조회 없는 실시간 업데이트
- 연결 수 대비 효율 극대화
리액티브의 진짜 가치는
DB가 아니라 연결 수에서 나온다.
13. 최종 결론
많은 팀이 리액티브를 포기하는 이유는 리액티브가 틀려서가 아니다.
DB가 리액티브가 아니기 때문이다.
Kafka는 DB 병목을 제거하지 않고 그 병목이 전체 시스템을 멈추지 않게 만든다.
리액티브는 포기할 기술이 아니라, 아무 데나 쓰면 안 되는 기술이다.
그리고 올바른 조건과 분리 구조 위에서는 여전히 가장 강력한 도구 중 하나다.