💡 실시간 번역 상태 처리를 위한 큐 기반 아키텍처와 Redis 확장기
1. 도입 – 왜 이 기능이 필요했는가?
법령 번역 비교 서비스에서는 AI 기반 번역 요청 후, 번역 완료까지의 시간이 수초에서 수분 이상 소요되며, 프론트엔드는 SSE(Server-Sent Events)를 통해 사용자에게 실시간 상태를 반영해야 합니다.
그러나 다음과 같은 실무적 요구사항과 문제가 발생했습니다:
- 다건 작업 트래킹: 여러 작업을 동시에 처리 중인 사용자가 각 작업별 상태를 실시간으로 받아야 함
- 진행률 상태 역주행 이슈: 이전에 전송된 진행률보다 낮은 값이 수신되는 경우 사용자 혼란 야기
- SSE 전송 누락/지연: 순차 처리 없이 push할 경우, 병렬 작업 간 race condition 발생
- 수천 개의 법령 작업에 대한 상태 정합성 보장이 필요했으며, 단순한 Polling 방식으로는 한계가 명확
2. 구현 방식 – 큐 기반 이벤트 드리븐 구조
🔁 기본 구조: Concurrent Queue + 상태 동기화 워커
Queue<TranslationEvent> translationQueue = new ConcurrentLinkedQueue<>();
- enqueue() 함수: 상태 변화 발생 시 TranslationEvent 객체 등록
- startWorker(): 단일 스레드 워커에서 큐를 지속적으로 모니터링하여 SSE 전송 수행
- trackedList: companyNo+employeeNo 기준으로 연결된 SSE emitter 리스트를 다건 관리
📦 TranslationEvent 구조
class TranslationEvent {
String taskId;
String companyNo;
int progress; // 0~100
String status; // WAITING, PROCESSING, SUCCESS, FAIL
}
3. 문제 해결 전략
✅ 1. 상태 역주행 방지
if (currentProgress < lastSentProgress) {
log.warn("Rollback detected. Ignore outdated progress.");
return;
}
- 단건/다건 모두 lastSentProgress Map으로 상태 기억
- race condition 예방: 낮은 진행률 무시
✅ 2. 다건 SSE Emitter 트래킹
- 사용자마다 여러 작업을 동시에 보게 되는 구조를 고려해, trackedTaskMap<userId, Set<taskId>> 구조 유지
- 새로운 작업이 생성되면 subscribe() 시 tracked 리스트에 자동 추가
- 모든 emitter는 상태 수신 시, 해당 taskId와 매핑된 emitter에만 push
✅ 3. 상태 전송 중복 방지 및 누락 대응
- DeduplicationKey(taskId + progress + status) 기반 중복 제거
- 작업 완료 후 SSE emitter 자동 해제하여 자원 누수 방지
4. 확장: Redis Pub/Sub로 구조 고도화
단일 인스턴스 기반 구조에서는 문제없이 동작하였지만, 서비스가 멀티 인스턴스(MSA 구조)로 확장됨에 따라 큐 + 워커 구조만으로는 한계가 발생했습니다. 따라서 Redis Pub/Sub 기반 브로드캐스팅 구조를 도입하였습니다.
🧱 Redis 구조
- translation-status-channel 채널에 상태 변경 시 메시지 발행
- 모든 WAS 인스턴스가 Subscriber로 등록되어 이벤트 수신 → 로컬 emitter로 전송
- 메시지 페이로드는 JSON 직렬화된 TranslationEvent 객체
- 트랜잭션 처리 후 commit 성공한 경우에만 publish
redisTemplate.convertAndSend("translation-status-channel", event);
🔧 장점
- 무중단 배포 및 WAS scale-out 시에도 상태 푸시 일관성 유지
- 트래픽 분산 환경에서도 상태 싱크 정합성 확보
- 상태 전송 로직을 application decoupling 처리하여 유지보수성 향상
5. 실서비스에서 얻은 효과
항목 개선 전 개선 후
SSE 수신 누락 | 빈번 | 0건 |
진행률 역주행 | 있음 | 방지 로직으로 100% 해결 |
멀티 유저 동시 작업 처리 | 트래킹 누락 존재 | trackedList + emitterMap 구조로 정합성 확보 |
MSA 대응성 | 불가능 | Redis Pub/Sub 구조 도입으로 고가용성 확보 |
6. 결론 – 단순한 큐에서 시작한 구조적 진화
이번 구조는 단순한 ConcurrentLinkedQueue에서 시작해, 실시간성과 상태 정합성을 유지하는 이벤트 기반 아키텍처로 진화했습니다. 이후 Redis Pub/Sub로 확장하면서 멀티 인스턴스 환경에서도 안정적 실시간 상태 처리를 가능하게 했습니다.
이 과정은 단순히 큐를 사용하는 문제를 넘어, 실시간 시스템에서 발생할 수 있는 race condition, 중복 상태, 상태 일관성 이슈를 해결하기 위한 종합적인 접근이었습니다.
'프로그래밍 언어 > Java' 카테고리의 다른 글
실시간 번역 상태 처리 시스템: Spring 기반 Redis Pub/Sub, SSE Emitter 트래킹, 구조 비교까지 (2) | 2025.07.06 |
---|---|
Java 애플리케이션 성능 최적화: JVM 힙 메모리 설정 가이드 (0) | 2025.03.12 |
[Java] 싱글톤은 Enum 타입으로 만들어라 (0) | 2025.01.27 |
[Java] 싱글톤(Singleton) 패턴의 사용 이유와 문제점 (1) | 2025.01.27 |
[Java] 멀티스레딩 및 동시성 (1) | 2025.01.05 |