Context
립리딩(구화 인식) AI와 실시간 음성 변환(STT·TTS)을 결합해 청각장애인과 비장애인이 함께 통화할 수 있게 한 데스크톱 앱입니다.
Problem
- 이메일 로그인, 소셜 로그인, 기존 계정의 소셜 연동이 한 인증 성공 핸들러에 섞여 분기가 깊어졌습니다.
- STT/TTS API는 응답에 3~10초가 걸려, 동시 요청이 몰리면 요청 스레드가 고갈될 위험이 있었습니다.
- CPU 환경에서 구화 인식 추론이 13초 걸려 대화 흐름이 끊겼습니다.
Architecture
- 인증: Spring Security + OAuth2 + JWT, Redis에 Refresh Token과 로그아웃 토큰 블랙리스트를 저장합니다.
- 회원 모델:
member와member_social을 1:N으로 분리해 여러 소셜 계정 연동을 지원합니다. - 외부 AI 호출: WebClient 기반 Non-Blocking 호출, 구화 인식 모델은 Runpod GPU 서버에서 서빙합니다.
My Contribution
- Spring Security·OAuth2·JWT 인증/인가와 SMTP 이메일 검증을 구현했습니다.
- 라이브 세션 입장 제한용 블랙리스트 API를 만들었습니다.
- 외부 STT/TTS 연동과 AI 추론 서버 이전을 맡았습니다.
- JiraOps로 스프린트와 이슈를 관리했습니다.
Engineering Decisions
인증 후처리를 전략 패턴으로 분리
SuccessHandler 하나에 가입 유도, 로그인, 계정 연동이 섞여 if-else가 5단계 이상 중첩됐고, 소셜 제공자를 추가할 때마다 기존 코드를 고쳐야 했습니다. supports()/handle()을 가진 전략 인터페이스를 정의하고 시나리오별 구현체(Guest, User, LinkAccount)로 나눈 뒤, 핸들러가 주입받은 전략 목록에서 맞는 구현을 찾도록 바꿨습니다. 200줄짜리 클래스가 50줄 미만의 독립 클래스 3개가 됐습니다.
Non-Blocking 외부 API 호출
RestTemplate으로 수 초짜리 호출을 하면 요청마다 스레드가 대기합니다. WebClient로 바꿔 대기 중에도 스레드가 다른 요청을 처리하도록 했습니다.
Troubleshooting
WebClient Multipart 전송 거부
WebClient는 큰 파일을 Chunked Transfer Encoding으로 보내는데, 연동 대상 서버가 이를 지원하지 않아 오디오 업로드가 거부됐습니다. 파일을 byte[]로 직렬화해 Content-Length를 확정한 뒤 MultipartEntityBuilder로 전송하도록 우회했습니다. 메모리 사용이 늘어나는 대가가 있어 파일 크기 제한을 함께 적용했습니다.
TTS 음성 앞부분 잘림
생성된 음성의 앞 0.5초 정도가 잘려 들렸습니다. 요청 instruction에 짧게 멈춘 뒤 시작하라는 지시를 추가해 앞부분에 무음 구간이 생기도록 했습니다.
Result
- SSAFY 13기 공통 프로젝트 반 1위 우수상, FLY ASIA 2025 전시 참가
- 구화 인식 추론 시간 13초 → 6초 (약 54% 단축)
Retrospective
Spring Security의 필터와 OAuth2 흐름을 직접 확장하면서 인증 구조를 깊이 이해하게 됐습니다. 파일을 메모리에 올려 전송한 방식은 대용량 입력에 약하므로, 다음에는 스트리밍 전송이나 업로드 전용 경로를 먼저 검토하겠습니다.