경력기술서 · 2024.01 – 2024.07 · 퓨렌스

우리은행 ODS AI 화자분리 파이프라인 구축

녹취 엔진 · 화자분리 라우팅 · STT 전송 · 라이선스 토픽

태블릿 녹취는 마이크가 하나라 화자를 가를 수 없어, 내부망 GPU에서 MP3를 RX·TX PCM으로 나눈 뒤 기존 PC 녹취의 STT·TA로 이어집니다. 녹취 엔진의 화자분리 라우팅, 분리 음원 STT 전송, 라이선스용 Kafka 토픽을 개발했습니다.

Netty TCP Socket WebSocket Kafka Python JEUS L4 / VIP Linux
00

망 구간 아키텍처

개발 관점

외부망 태블릿이 내부 녹취 엔진에 바로 붙지 않도록 중계 Proxy를 두고, 화자분리는 내부 GPU 서버 2대로 라우팅하게 개발했습니다. 웹뷰의 라이선스·STT·TA는 녹취 엔진 WebSocket에서 내려주게 했습니다.

외부망 → 중계망 → 내부운영망 화자분리 서버 2대 · Round Robin
외부망
중계망
내부운영망
녹취
TCP
태블릿 앱마이크 1개 · MP3
TCP
중계 VIP이중화
Proxy 1TCP · WebSocket
Proxy 2TCP · WebSocket

기존 Proxy 개선 · ping-pong

TCP
녹취 VIPHash
녹취 엔진 1완본 저장 · 라우팅
녹취 엔진 2분리 음원 · STT
TCP
화자분리 VIPRound Robin
화자분리 1GPU · 모델 연동
화자분리 2WebSocket · RX·TX PCM

엔진은 직접 구현 · 모델은 연동

화면
WSS
앱 웹뷰WSS 클라이언트
WSS
JEUSWebSocket 중계
WSS
WebSocket 서버녹취 엔진 · 라이선스 · STT · TA

직접 구현

화자분리 경로 녹취 엔진TCP화자분리 1·2PCM 청크녹취 엔진start·endSTTKafka녹취 엔진
녹취 AP · WAS · Kafka 골격은 태블릿 서비스 고도화와 불완전판매방지 솔루션 구축을 그대로 사용합니다. 이번 프로젝트에서 내부망에 GPU 화자분리 서버 2대를 Round Robin으로 붙였고, Python 모델은 그 서버에 연동했습니다. 라이선스 토픽은 기존 Kafka 브로커 1·2에 추가했습니다.
01

규모와 역할

태블릿 상담 녹취마이크 1개 · 화자 구분
화자분리 2대Round Robin · 모델 연동
녹취 엔진화자분리 라우팅 · STT · 라이선스
7개월2024.01 – 2024.07
02

태블릿 녹취에서 화자별 STT까지

개발 관점

마이크가 하나라 채널 분리는 엔진에서 하지 않고, 완본 저장 후 별도 스레드로 GPU 서버에 보내게 개발했습니다. 돌아온 PCM부터는 기존 PC 녹취와 같이 TCP로 STT에 넘기고, 결과는 Kafka로 받게 했습니다.

태블릿
중계망
녹취 엔진
화자분리
STT
Kafka
녹취 업로드 (TCP)
① MP3 업로드
② TCP 프록시
화자분리 (TCP · Round Robin)
③ 별도 스레드 · 파일
④ RX·TX PCM 청크
STT (TCP 송신 · Kafka 수신)
⑤ start · end · connection
⑥ STT 결과 발행
⑦ 엔진이 결과 수신
웹뷰 수신 (WSS)
⑧ WSS 접속
⑨ JEUS 중계
⑩ 라이선스 · STT · TA
② 프록시

태블릿 프로젝트에서 만든 중계망 Proxy를 이 업로드 구간에 맞게 수정했습니다. 녹취 파일은 Netty TCP로 주고받고, 유휴 세션은 ping-pong으로 유지합니다.

③ ④ Round Robin

화자분리 서버는 2대이고, 앞단은 Round Robin입니다. 서버 안에서 WebSocket으로 Python 모델을 호출해 MP3를 RX·TX PCM으로 나눕니다. 모델은 서버에 연동했고, 파일 하나의 연결이 그 서버에 머물기 때문에 Round Robin으로 나눕니다.

⑤ ⑦ STT

녹취 엔진은 STT와 TCP로 start · end · connection을 주고받은 뒤 음성 패킷을 보냅니다. STT 결과는 Kafka로 돌아와, 이 구간부터 기존 PC 녹취 코드를 그대로 씁니다.

⑧ ⑩ 웹뷰

웹뷰는 중계망 JEUS를 거쳐 녹취 엔진 WebSocket 서버에 붙습니다. 라이선스 발급·검증과 STT·TA 결과를 이 연결로 받습니다.

03

화자분리 채널 라이선스

개발 관점

엔진 1·2가 서로 다른 서버라 메모리를 합쳐도 라이선스 절대 개수가 되지 않습니다. 발급·반납을 Kafka 토픽으로 모으고, 같은 컨슈머 그룹에서 클러스터 전체 개수를 세도록 개발했습니다.

녹취 엔진 1서버 A · 파티션 0
녹취 엔진 2서버 B · 파티션 1
같은
컨슈머 그룹
라이선스 토픽파티션 2개
복제 2
브로커 1파티션 0 · 1 보관
브로커 2파티션 0 · 1 보관

평시 · 파티션을 하나씩

파티션 0 녹취 엔진 1 이 파티션 메시지만 발급 · 검증
파티션 1 녹취 엔진 2 이 파티션 메시지만 발급 · 검증

한 대가 내려간 뒤

엔진 1 중단 파티션 0 → 엔진 2 남은 엔진이 두 파티션을 이어 받음
브로커 1 중단 브로커 2의 복제본 토픽이 남아 발행 · 구독이 이어짐
같은 컨슈머 그룹

서버마다 세면 그 서버 개수만 나옵니다. 그룹 하나에서 메시지를 나눠 받으면 클러스터 전체의 절대 개수가 되고, 같은 건은 한 엔진만 처리합니다.

파티션 2

건수와 메시지 크기가 작아 파티션은 엔진 대수와 같게 2입니다. 엔진마다 파티션 하나를 맡아 두 대가 함께 처리합니다.

replication factor 2

브로커가 1·2로 이중화되어 있어 복제도 2입니다. 파티션이 양쪽 브로커에 남아, 브로커 한 대가 내려가도 토픽이 유지됩니다.

04

트러블슈팅

01중계 프록시 세션이 주기적으로 끊김

문제

녹취 업로드 중 중계망 프록시에서 idle timeout이 처리되지 않아 세션이 주기적으로 끊겼습니다.

원인

유휴 연결에 대한 timeout 처리가 없어, 잠시 데이터가 없으면 세션이 종료됐습니다.

해결

태블릿 때 만든 Proxy를 수정해 timeout을 처리하고, ping-pong으로 유휴 세션을 유지했습니다.

결과

업로드 세션이 주기적으로 끊기지 않게 됐습니다.

02세션 참조가 남아 힙 메모리가 초과됨

문제

멀티스레드로 TCP·WebSocket 세션을 함께 관리하면서, 객체 참조와 정리되지 않은 세션 때문에 OOM이 자주 발생했습니다.

원인

연결이 끝난 세션과 버퍼 참조가 스레드마다 남아 힙이 계속 늘었습니다. Netty 버퍼는 release하지 않으면 회수되지 않습니다.

해결

세션 보관소를 싱글톤으로 두고, 채널이 닫힐 때 세션을 빼도록 구조를 다시 짰습니다. ChannelGroup이 닫힌 채널을 스스로 제거하고, 받은 버퍼는 release로 참조를 놓습니다. JVM 힙도 함께 늘렸습니다.

결과

힙을 늘린 상태에서, 참조가 쌓이던 기존 OOM은 다시 발생하지 않았습니다.

05

핵심성과

세션 업로드가 중간에 끊기지 않음
한 일

태블릿 때 만든 Proxy에 idle timeout 처리와 ping-pong을 넣었습니다.

개선

녹취 업로드 중 데이터가 잠시 없어도 세션이 주기적으로 끊기지 않습니다.

메모리 반복되던 OOM이 다시 발생하지 않음
한 일

세션 보관소를 싱글톤으로 두고, 닫힌 채널은 ChannelGroup이 빼고 버퍼는 release하게 했습니다. JVM 힙도 함께 늘렸습니다.

개선

끝난 TCP·WebSocket 세션이 힙에 쌓이던 기존 OOM은 다시 발생하지 않았습니다.

집계 두 서버의 라이선스를 한 개수로
한 일

엔진 1·2의 발급·반납을 같은 컨슈머 그룹의 Kafka 토픽으로 모았습니다.

개선

서버가 달라도 클러스터 전체의 라이선스 절대 개수를 한곳에서 셉니다.