CareerBee에서는 다음과 같은 기능에서 AI 서버와의 통신이 이뤄지고 있습니다.
- 이력서 및 고급 이력서 생성
- 이력서 정보 추출
- 면접 문제에 대한 AI 피드백 제공
현재 해당 기능들은 동기(Synchronous) 방식으로 구현되어 있어, 동시에 많은 요청이 발생할 경우 처리 속도가 느려지거나 요청이 실패하는 문제가 발생할 수 있습니다.
이러한 문제를 해결하고 병렬 처리 성능을 높이기 위해, 비동기(Asynchronous) 통신 방식으로 전환을 진행하고자 합니다. 이를 위해 기존에 사용하던 RestClient에 CompletableFuture 기반의 비동기 처리를 도입하여, 대량의 요청에도 안정적으로 대응할 수 있는 구조로 개선할 예정입니다.
비동기 처리결과는 어떻게 프론트에 전달할까?
코드를 바로 적용하기에 앞서, 비동기 전환 과정에서의 구조 설계를 먼저 진행해보고자 합니다.
기존 동기 방식에서는, AI 서버로부터 응답이 올 때까지 하나의 스레드가 HTTP Connection을 유지한 채 대기하게 됩니다.
이러한 방식은 요청을 보낸 클라이언트에게 즉시 결과를 응답해 줄 수 있다는 장점이 있지만, 처리량이 제한적이라는 단점이 있는데요.
이러한 한계를 극복하기 위해, 비동기(Async)방식을 도입하기로 결정했습니다.
비동기 구조에서는 요청을 받은 즉시 HTTP 202(Accepted) 응답을 반환하고, 해당 요청에 대한 실제 처리는 별도의 스레드에서 진행됩니다. 이를 통해 HTTP 연결 시간을 최소화하고 작업을 병렬적으로 처리할 수 있게 됩니다.
하지만 이 구조에서는 다음과 같은 의문이 생깁니다.
HTTP 응답은 바로 끊었는데, 처리 결과는 어떻게 프론트엔드에 전달하지?
이를 해결하기 위해 도입한 기술이 SSE(Server Sent Events)입니다.
CareerBee에서는 이미 알림 기능에서 SSE를 활용하고 있었고, 이를 확장하여 AI 처리 결과를 사용자에게 실시간으로 Push 해주고자 합니다. 따라서 서버 측에서 처리 완료 시점에 SSE를 통해 클라이언트에 이벤트를 Push 함으로써, 프론트엔드는 별도의 Polling 없이도 결과를 실시간으로 받을 수 있게 됩니다.
멀티 인스턴스에서는 가능한가?
여기서 중요한 사실 하나를 짚고 넘어가야 합니다.
바로 현재 SpringBoot 애플리케이션이 멀티 인스턴스 환경에서 동작하고 있다는 점입니다.
SSE를 사용해 본 개발자라면, SseEmitter에 대해서 알고 있을 것입니다.
간단하게, 서버가 클라이언트에게 실시간으로 이벤트를 전송하기 위해 사용하는 HTTP 기반의 푸시 객체입니다.
CareerBee에선 SseEmitter를 사용자 식별값(유저의 PK)을 키로 하여
Map <Long, SseEmitter> 구조로 JVM 메모리에 보관하고, 이를 통해 클라이언트와의 연결을 유지하고 있습니다.
이 방식은 단일 인스턴스 환경에서는 전혀 없습니다.
모든 요청이 하나의 서버 인스턴스로 들어오기 때문에, 하나의 메모리 공간에서 SseEmitter를 관리할 수 있기 때문입니다.
하지만 멀티 인스턴스 환경에선 이야기가 달라집니다.
SpringBoot 인스턴스 각각 독립적인 JVM 메모리 공간을 가지므로, SseEmitter 객체는 공유되지 못하게 됩니다.
즉, 이벤트를 발생시킨 인스턴스와 클라이언트가 연결된 인스턴스가 다를 경우, SseEmitter를 찾지 못해 이벤트를 전송할 수 없는 문제가 발생하게 됩니다.
이 문제를 해결하기 위해선 멀티 인스턴스 환경에서도 클라이언트의 SseEmitter 정보를 공유하거나, 이벤트를 브로드캐스팅할 수 있는 아키텍처 설계가 필요합니다.
멀티 인스턴스에서 어떻게 가능하게 할까
앞서 언급했듯이, 멀티 인스턴스 환경에서는 SseEmitter 정보를 공유하거나 이벤트를 브로드캐스트 할 수 있는 아키텍처가 필요합니다.
초기에는 단순히 Redis와 같은 공유 저장소를 활용하여 SseEmitter 객체를 관리하면, 멀티 인스턴스 환경에서도 문제없이 Push 할 수 있을 것이라 생각했습니다. 그러나 이 방식에는 치명적인 한계가 존재합니다.
SseEmitter는 Java의 런타임 객체이기 때문에, Redis와 같은 외부 저장소에 직접 저장하거나 공유할 수 없습니다.
즉, Redis를 이용한 SseEmitter 객체 공유는 기술적으로 불가능한 방식이었습니다.
대안을 찾기 위해 메시지 큐 또는 Pub/Sub 구조를 검토했고, 현재 인프라에서 이미 Redis를 활용하고 있다는 점을 고려하여,
Redis Pub/Sub 기반의 아키텍처를 적용하기로 결정했습니다.
Redis Pub/Sub 기반 아키텍처
멀티 인스턴스 환경에서도 안정적인 SSE 푸시를 가능하게 하는 Redis Pub/Sub 기반 아키텍처는 다음과 같습니다.
동작순서

1. 각 Spring Boot 인스턴스는 서버 실행 시 sse-channel을 구독합니다.

2. 특정 이벤트가 발생하면, 해당 이벤트 메시지를 sse-channel로 발행(Publish)합니다.

3. Redis는 이 채널을 구독 중인 모든 인스턴스에 이벤트를 브로드캐스트 합니다.

4. 실제로 SseEmitter를 보유한 인스턴스가 이를 감지하고, 해당 사용자에게 SSE 푸시를 수행합니다.
이 구조를 통해, 멀티 인스턴스 환경에서도 사용자와 연결된 인스턴스에서 정확하게 SSE 이벤트를 전달할 수 있게 됐습니다.
'스프링부트' 카테고리의 다른 글
| Redis 통신 라이브러리의 비교 (feat. Lettuce VS Redisson) (5) | 2025.08.11 |
|---|---|
| PK기반 단건 조회시 컬럼 수로 성능 개선을 기대할 수 없는 건에 대하여 (5) | 2025.08.11 |
| @Cacheable 은 무적인가? (2) | 2025.07.08 |
| 락을 활용해서 동시성 문제를 해결해보자 (3) | 2025.07.08 |
| double 타입의 위도, 경도 값 테스트시 부동소수점 오차 발생에 관하여 (1) | 2025.05.25 |