Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Pause & Play

[MS AI School 최종 프로젝트] AI 응답 14초 → 1.5초, 성능 최적화 전면전 — SimSpeak 개발기 (2/2) 본문

MS ai School

[MS AI School 최종 프로젝트] AI 응답 14초 → 1.5초, 성능 최적화 전면전 — SimSpeak 개발기 (2/2)

eun_ll 2026. 7. 7. 02:30

비동기 투트랙 아키텍처로 LLM 파이프라인 병목을 제거하고, N+1 쿼리를 걷어내고, 실패 경로까지 방어한 기록. 1편에서 다룬 아키텍처와 인프라 위에서, 이번 편은 숫자로 증명하는 성능 개선을 다룬다.

시리즈 [1편 — 설계 편]: 배경 · 팀 빌딩 · 아키텍처 · 인프라 비용 최적화
2편 — 성능 편 (현재 글): 레이턴시 · N+1 · 음성 엔진 이원화 · 데이터 계약 · 신뢰성 설계

 


이 글의 핵심 성과

  • E2E 응답 지연 최대 14초 → 1~2초 (약 85% 단축) — 실시간 대화 / 정밀 평가 투트랙 분리 + 비동기 콜백(/api/chat/callback)
  • N+1 제거로 p95 4.12초 → 1.99초 (-52%), 쿼리 수 11개 → 4개 — IN 절 일괄 조회. 대조군 계측으로 개선 원인 검증
  • 오디오 처리 지연 추가 1초+ 단축 — 디스크 I/O 제거, io.BytesIO RAM-to-RAM 파이프라인
  • 음성 파이프라인 이원화 — STT는 Whisper, 발음 채점은 Azure Speech로 전문화

1. [Trouble Shooting] 레이턴시 병목 — 14초를 1.5초로 만든 비동기 투트랙 아키텍처

Problem

통합 테스트 단계에서 대화 답변 생성 + 문법 평가를 한 번에 동기 처리하면서 E2E 응답 지연이 최대 14초까지 늘어나는 병목이 드러났다. '실시간 통화'가 본질인 서비스에서 14초의 딜레이는 곧 사용자들의 이탈로 일어나게 된다.

원인 분해

지연을 구성 요소별로 분해하자, 유저가 지금 당장 필요로 하는 것과 나중에 봐도 되는 것이 하나의 파이프라인에 묶여 있다는 구조적 문제가 확인됐다.

  • 즉시 필요: 캐릭터의 대답(텍스트+음성), 행동 지문, 호감도 변동 → 대화 몰입의 생명선
  • 지연 허용: CEFR 레벨 채점, 정밀 문법 교정, IPA 기준 발음 상세 진단 → 리포트/오답노트에서 소비

Solution: 투트랙 분리 + 비동기 콜백 아키텍처

응답의 성격에 따라 파이프라인을 두 트랙으로 물리적으로 분리하고, 무거운 평가 트랙은 콜백(Callback) 기반 비동기 수신 구조로 백엔드를 전면 개편했다.

[User 발화] ──> POST /api/chat/message (Spring Boot)
                     │
     ┌───────────────┴────────────────────────────────┐
     │                                                │
 Track 1: 즉시 응답 (동기, 1~2초)             Track 2: 백그라운드 평가 (비동기)
     │                                                │
     ├─ Whisper STT (고정확도 텍스트 추출)                 ├─ Base GPT-4o: 정밀 문법 교정 + CEFR 채점
     ├─ GPT-4o-mini: 대사 + 행동 지문                    ├─ Azure Speech: 정확도·유창성·완전성·
     │                + affinity_delta 생성            │   억양 진단 (IPA 기준 100점)
     ├─ ElevenLabs TTS: 캐릭터 음성 합성                  │
     │                                                └─> POST /api/chat/callback
     └──> 즉시 응답 반환                                       │
          (text_content, audio_url,                         ├─ turn_id 매핑으로 세션 로그와 매치
           affinity_delta)                                  └─ Neon PostgreSQL(JSONB) 영속화

 

설계 포인트

  1. turn_id 기반 지연 결합(Late Binding): 평가 결과가 늦게 도착해도 turn_id로 정확히 해당 대화 턴에 꽂힌다. 유저가 기다리는 것은 Track 1뿐이고, Track 2는 보이지 않는 곳에서 처리된다.
  2. 모델 이원화의 비용 효과: 매 턴 호출되는 대화 트랙은 저비용·고속의 GPT-4o-mini, 정확도가 중요한 평가 트랙만 Base GPT-4o — 레이턴시와 토큰 비용을 동시에 최적화하였다.
  3. 프론트엔드 분할 수신: 1차 응답 수신 즉시 음성 재생 + 호감도 애니메이션을 기동하고, 2차 피드백 API를 chain 호출. 오답노트 북마크에는 Optimistic Update(pendingUnstar)를 적용해 체감 지연을 제로화했다.

추가 최적화: 디스크 I/O 전면 제거 (io.BytesIO)

유저 음성과 TTS 오디오를 파일 시스템에 임시 저장 후 재독취하던 로직이 1초 이상의 물리적 지연을 유발하고 있었다. 임시 파일 경유를 차단하고 io.BytesIO 메모리 버퍼로 오디오 패킷을 RAM-to-RAM으로 직결하여 해당 지연을 제거했다.

[Before]  STT 결과 ──> 디스크 임시 파일 write ──> read ──> TTS 전달   (+1초↑)
[After]   STT 결과 ──> io.BytesIO (RAM) ──────────────> TTS 전달   (물리 I/O 0)

결과

E2E 응답 지연: 최대 14초 → 1~2초 (약 85% 단축) 정밀 평가 데이터는 품질 저하 없이 백그라운드에서 100% 수집·영속화


2. [Trouble Shooting] N+1 쿼리 제거 — p95 4.12초 → 1.99초 (-52%)

Problem

캐릭터·스테이지 현황 조회(/characters/status) 요청 1건에 대해 Hibernate 로그를 확인하니, 스테이지마다 where stage_id = ? 쿼리가 반복 실행되어 요청당 총 11개의 쿼리가 나가고 있었다. 전형적인 N+1 패턴이다.

로컬 DB였다면 티가 덜 났겠지만, 우리 DB는 1편에서 선택한 원격 서버리스(Neon) — 쿼리 1개당 네트워크 왕복(RTT) 비용이 그대로 누적되어 p95 응답시간이 4.12초까지 치솟았다. 서버리스 전환으로 비용을 잡은 대가가 여기서 청구된 셈이다.

Solution: IN 절 일괄 조회

스테이지마다 따로 묻던 진행도를, 필요한 식별자를 모아 where stage_id in ( … ) 한 번으로 일괄 조회하도록 리팩터링했다.

[Before]  요청 1건 ──> SQL × 11  (스테이지마다 개별 조회, RTT 누적)
[After]   요청 1건 ──> SQL × 4   (IN 절 일괄 조회)

결과 

요청당 쿼리 수 11개 4개 -64%
p95 응답시간 4.12초 1.99초 -52%
대조군 (미변경 엔드포인트) 0.85초 0.82초 변화 없음

 

여기서 중요한 것은 마지막 행이다. 같은 기간 변경하지 않은 대조군 엔드포인트의 응답시간은 0.85초 → 0.82초로 사실상 동일했다. 즉 개선 효과는 네트워크나 DB 웜업 같은 환경 요인이 아니라 쿼리 수 감소 그 자체에서 왔음을 계측으로 검증했다. 최적화 주장에는 대조군이 있어야 제대로 비교가 된다는 사실을 깨달았다.


3. [Trouble Shooting] 음성 파이프라인 이원화 — STT는 Whisper, 발음 채점은 Azure Speech

Problem

초기에는 Azure Speech 하나로 STT와 발음 채점을 모두 처리했다. 그러나 한국어가 혼용되거나 발음이 부정확한 발화에서 텍스트 추출 정확도가 크게 떨어지는 문제가 확인됐다. 콩글리시 화자를 타겟으로 하는 서비스에서 이는 치명적이다 — 텍스트를 잘못 추출하면 이후의 대화·교정 파이프라인 전체가 오염된다.

Solution: 엔진별 전문화 (Dual Voice Engine)

Azure OpenAI Whisper STT (텍스트 추출) 전담 한글 혼용·부정확 발음에서도 높은 텍스트 추출 정확도
Azure Speech SDK 발음 채점 전담 정확도·유창성·완전성·억양 4개 축, IPA 기준 100점 정밀 진단

 

두 엔진은 FastAPI 오케스트레이터에서 1단계 병렬 분석으로 동시 기동되며, 2단계에서 GPT-4o가 응답과 함께 75점 미만 단어에 대한 IPA 발음 가이드를 생성해 반환한다. 적재된 발음 점수(accuracy, fluency, completeness, prosody)와 단어별 상세(word_details_json)는 리포트 화면의 레이더 차트로 재사용된다.


4. 데이터 모델 & 저장 계층 — 바이트 단위까지 통제하는 데이터 계약

1인 백엔드 체제에서 3개 파트의 병렬 개발을 성립시킨 핵심은 초기 데이터 계약의 엄격한 표준화였다.

4-1. 대화 로그 중심 설계 (chat_log)

모든 대화 턴은 chat_log 테이블을 허브로 적재되며, 무거운 분석 데이터는 위성 테이블로 분리했다.

chat_log (
  id             BIGINT PK,
  session_id     VARCHAR,
  turn_count     INT,
  role           ChatRoleType,        -- ENUM (user / assistant)
  input_type     ChatInputType,       -- ENUM (text / voice)
  text_content   TEXT,                -- 대사
  audio_url      VARCHAR,             -- Blob Storage 경로만 매핑
  grammar_feedback TEXT,
  is_penalty     BOOLEAN,
  affinity_delta INT,                 -- 호감도 변동량
  penalty_reason PenaltyReasonType    -- ENUM
)

-- 위성 테이블 (chat_log_id FK로 지연 결합)
pronunciation_evaluations ( accuracy, fluency, completeness, prosody, word_details_json )
corrections               ( original_sentence, corrected_sentence, corrections_json, translation, is_reviewed )
raw_ai_responses          ( response_data JSONB )   -- AI 원본 응답 전문 보존

이 구조 덕분에 Track 2의 콜백 데이터가 늦게 도착해도 chat_log의 해당 턴에 FK로 정확히 결합되고, raw_ai_responses에 원본 응답 전문을 JSONB로 보존해 디버깅·재처리 경로를 확보했다.

4-2. 저장 계층 최적화 3원칙

  • 정수형 식별자: user_id, stage_id 등 마스터 데이터를 무작위 문자열 대신 정수(BIGINT) 매핑 → 인덱스 크기 축소, 원격 DB 조회 성능 개선
  • 상태값 ENUM화: GenderType, CefrLevelType, StageType, ChatRoleType, PenaltyReasonType 등 반복 적재되는 상태값을 ENUM으로 코드화 → 수 바이트 수준 압축 + 조건절 검색 속도 확보
  • 대용량 오디오의 인프라 이원화: mp3/wav를 DB에 바이너리로 넣지 않고 Azure Blob Storage 적재 + DB에는 audio_url 경로만 매핑 → DB 저장 비용과 IOPS 리스크 격리

4-3. "AI 호출 없는" 리포트 서빙

리포트·복습 화면은 조회 경로를 둘로 분리했다.

경로 A (세션 종료 시 1회):  로그·점수 취합 ──> GPT-4o CEFR 채점 ──> 5각 레이더 + A1~C2 등급
경로 B (평상시 오답노트):   Neon JSONB 캐시 단순 조회 ──> AI 호출 0회, 딜레이 없이 즉시 반환

대화 중 Track 2가 미리 적재해 둔 교정·점수 데이터를 경로 B에서 그대로 재사용하므로, 복습 화면은 토큰 비용 0원 + 즉시 응답으로 서빙된다.

4-4. LLM 계층의 토큰·포맷 통제

  • 슬라이딩 윈도우 + 요약 엔진: 멀티턴 기억을 위한 과거 대화 주입 시, 무제한 누적으로 인한 토큰 폭증을 방지하고 필수 컨텍스트만 콤팩트하게 적재
  • 프롬프트 Skeleton화: 출력 지침을 장황한 자연어 대신 프롬프트 말미 [STRICT OUTPUT FORMAT] 구역에 한 줄짜리 순수 JSON 샘플로 제공 → 포맷 파손율 감소 + 지시 토큰 최소화
  • 세이프티 가드레일: 욕설·탈옥(Jailbreak) 시도를 차단하는 안전망 레이어를 오케스트레이터 파이프라인에 단일 모듈로 통합

5. 신뢰성 설계

데모가 아닌 서비스라면, 행복 경로(happy path)보다 실패 경로가 품질을 결정한다. 계층별로 방어선을 세웠다.

백엔드 / AI 파이프라인

  • 지수 백오프 재시도: AI API 실패 구간 진입 시 지수적 지연 기반 자동 재시도로 일시 장애를 흡수
  • 폴백 메시지: 서버 장애 시 기본 인터랙션 메시지로 대화 흐름 유지 (침묵 대신 안내)
  • FALLBACK_QUESTIONS: 문항 로딩 오류 시 사전 정의된 6문항으로 전환하여 레벨테스트 플로우 보존
  • 트랜잭션 원자성: DB 쓰기 에러로 인한 데이터 유실이 없도록 원자적 저장 보장

프론트엔드

  • isMounted 가드: 화면 이탈 후 늦게 도착한 응답이 언마운트된 컴포넌트를 갱신하며 발생하는 간헐적 크래시를 useEffect cleanup으로 차단
  • 403 에러 삼원인 분해: 하나의 403 뒤에 숨어 있던 세 가지 원인 — ① 빈 body 응답의 JSON 파싱 실패 → HTTP status별 에러 메시지 매핑, ② fallback 문항 ID의 서버 제출 → 제출 자체 차단, ③ answerType 대소문자 불일치 → multiple_choice 소문자 통일 — 을 각각 분리 해결
  • 빈 데이터 방어: 빈 배열 접근 시 CHARACTER_DATA[0] 폴백 + 비치명 오류는 console.warn으로 강등
  • 네이티브 의존성 정합화: Gradle 9.0 비호환으로 빌드가 깨지던 음성 라이브러리를 expo-av로 교체해 Expo 공식 빌드 플로우 안에서 해결

관측·보안

  • Azure App Insights: 앱 진입·종료 등 이벤트를 실시간 추적하여 이탈률 분석 기반 마련. 전체 대화 로그(발화·응답·점수)는 세션 단위로 DB에 보존해 문제 발생 시 추적 가능
  • BCrypt 단방향 해시: 비밀번호 평문 보관 금지, 보안 설정은 코드와 분리 관리. 약관·민감정보·마케팅 수신은 선택적 동의로 분리 설계

6. 마치며 (Retrospective)

5주라는 일정과 백엔드 1인이라는 제약 속에서, 이번 프로젝트가 남긴 인사이트는 네 가지로 정리된다.

첫째, 레이턴시는 최적화가 아니라 '분류'의 문제다. 14초 병목의 해법은 개별 연산의 가속이 아니라, 응답을 '즉시 필요한 것'과 '지연 허용 가능한 것'으로 분류하고 아키텍처 레벨에서 분리하는 것이었다. 동기/비동기의 경계를 어디에 긋느냐가 유저 체감 성능을 결정한다.

둘째, 성능 주장에는 대조군이 필요하다. N+1 개선에서 p95가 4.12초 → 1.99초로 떨어진 것보다 중요한 발견은, 미변경 엔드포인트가 0.85초 → 0.82초로 그대로였다는 사실이다. 대조군 없는 벤치마크는 환경 노이즈와 개선 효과를 구분하지 못한다.

셋째, 인프라 선택은 트레이드오프의 연쇄다. 서버리스 전환으로 비용을 잡았지만(1편), 그 대가로 커진 쿼리당 RTT가 N+1 문제를 증폭시켰다(2편). 완벽한 선택은 없고, 선택의 부작용을 예측하고 후속 대응하는 것이 아키텍처 운영이다.

넷째, 소수 인원 체제의 성패는 초기 계약 설계에서 갈린다. ERD·API 명세·ENUM 체계를 1주차에 고정한 덕분에 FE 3 / AI 3 / BE 1이라는 비대칭 팀이 병렬 개발 체제로 작동했고, 늦게 도착하는 비동기 데이터조차 turn_id 하나로 제자리를 찾아갔다. 백엔드 엔지니어의 역할은 코드 작성 이전에 팀 전체가 의존하는 데이터 계약의 아키텍트라는 점을 체감했다.

기술을 많이 쓰는 것보다 유저가 기술의 존재를 잊게 만드는 것이 좋은 아키텍처라는 확신을 얻었다. 유저는 1.5초 만에 돌아오는 캐릭터의 목소리에 몰입할 뿐, 그 뒤의 투트랙 파이프라인과 콜백 영속화, IN 절로 묶인 쿼리를 인지하지 못한다 — 그리고 그것이 우리가 추구한 서비스에 맞았다.

 

[1편 — 설계 편 보러가기]