← 프로젝트 목록

AI Commons

강의 설계부터 슬라이드·영상·음성·교수자 아바타까지 하나의 제작 과정으로 연결한 AI 교육 콘텐츠 플랫폼입니다. 외부 기관 10곳과 내부 검증 환경 2곳은 서로 다른 배포 구성으로 운영합니다.

2025.08 ~ 현재 · 주요 개발 담당 · 기획부터 운영까지
NestJSReactPostgreSQLPrismaOpenAIGemini/Vertex AIElevenLabsffmpegBullMQRedisAWS GPU

배경

대학 교수가 15주 분량의 강의 콘텐츠를 제작하려면 많은 시간과 반복 작업이 필요합니다. AI Commons는 참고자료에서 강의 구조를 설계하고 슬라이드와 영상을 생성해 LMS에 게시합니다. 이후 계약별 라이선스와 사용량, 교수·조교 공동 작업, 교수자 아바타 영상까지 범위를 넓혔습니다.


측정 근거 · 2026.08

−96.5%

탭 평균 Long Task · 136→4.7개

−94.7%

탭 평균 총 블로킹 · 10.85→0.57초

6개 탭 · 10분

CPU 4× · Slow 4G 이미지 생성 조건

10 + 2

외부 기관 10 · 내부 검증 환경 2


구조

ASYNC GENERATION PIPELINE

생성 요청과 무거운 미디어 작업을 분리한 제품 구조

사용자 요청은 도메인 API가 빠르게 접수하고, 긴 생성 작업은 BullMQ 워커·락·영속 상태를 통해 복구 가능한 흐름으로 처리합니다.

생성 요청과 무거운 미디어 작업을 분리한 제품 구조 사용자 요청은 도메인 API가 빠르게 접수하고, 긴 생성 작업은 BullMQ 워커·락·영속 상태를 통해 복구 가능한 흐름으로 처리합니다. 01 CLIENT · REACT 19 02 SERVER · NESTJS 11 03 STATE & ASSETS SSO REST ENQUEUE INFER Canvas LMS JWT SSO Creation workspace design · slide · video Client state Query cache · Zustand REST API auth · contract Domain modules content · license · collab Workers BullMQ Model adapters LLM · TTS · GPU PostgreSQL job · product state Redis queue · lock Media storage NAS · assets Vector store grounding sources
강의 설계, 슬라이드, 음성, 영상 도메인은 계약을 공유하되 독립적으로 변경할 수 있도록 모듈 경계를 나눴습니다.

도메인 기반 모듈 구조

강의 설계, 슬라이드, 영상, 음성 도메인을 클라이언트 기능과 서버 모듈의 쌍으로 나눴습니다. 공통 계약을 통해 연결하되 각 도메인을 독립적으로 변경할 수 있게 구성했습니다.

비동기 작업 파이프라인

수 초에서 수 분이 걸리는 슬라이드 이미지, 영상 인코딩, TTS 생성을 BullMQ 워커에서 처리합니다. 클라이언트는 작업 상태를 주기적으로 조회해 진행률과 실패 상태를 표시합니다.

모델 실행 경계

OpenAI, Gemini, ElevenLabs와 외부 GPU 추론을 별도 모듈로 분리했습니다. 요청·응답과 실패 특성의 차이는 이 경계에서 변환하고, 도메인 로직은 공통 인터페이스를 사용합니다.

LMS 연동

JWT SSO를 사용해 LMS 사용자가 별도 로그인 없이 접근합니다. 생성한 콘텐츠는 Commons를 거쳐 LMS에 직접 게시합니다.


배포와 운영

고객 환경의 차이를 배포 설계에 포함했습니다.

AICMS-917 · 995 · 1015 · PTTLIW-6234

전용 서버와 기존 서비스 동거 환경, 동일·교차 도메인, PostgreSQL 직접 연결과 PgBouncer, 단일·다중 노드는 같은 설치 명령으로 끝나지 않았습니다. preflight부터 execution boundary, smoke test와 rollback까지 하나의 deployment unit으로 다뤘습니다.

Topology

공존 조건부터 확인

DNS·TLS chain·기존 vhost·port를 먼저 확인했습니다. 동일 도메인은 기존 443 vhost를 수정하고, 별도 도메인은 name-based vhost를 추가했습니다. 내부 API는 외부 주소가 아닌 127.0.0.1로만 proxy했습니다.

Execution boundary

모든 노드와 한 노드의 책임 분리

API·worker·Apache·Prisma Client는 모든 node에 적용했습니다. DB 생성·migration·LMS option·cron은 한 node에서만 실행해 duplicate side effect를 막았습니다. NAS path, Redis host와 FFmpeg CPU count는 서버별로 확인했습니다.

Zero downtime

기존 서비스를 멈추지 않는 설치

중단을 허용하지 않은 다중 노드 고객에서는 대상 노드를 load balancer에서 제외한 뒤 순차 설치했습니다. 고객 전용 locale과 기존 branch, 인증서를 보존하고 TLS·CORS·API·DB와 기존 LMS를 함께 검증했습니다.

Migration preflight

IN PROGRESS

정상 데이터만 가정하지 않기

라이선스 자동 전환이 교수 외 역할을 누락할 수 있어 기관별 역할·그룹·개별 권한을 배포 전에 대조하는 절차를 만들었습니다. LMS의 잘못된 주차 값은 게시 전체를 막지 않고 metadata만 제외하도록 fallback했습니다.

Degraded, not down

AICMS-995 · RESOLVED

API 생존만으로 배포 성공을 판단하지 않았습니다.

폐기된 모델이 404를 반환했지만 오류가 fallback으로 흡수되어 생성 자체는 성공했고, style inference만 계속 기본값을 사용했습니다. 대체 모델을 실제 호출로 검증해 교체하고, template hash 기반 startup sync로 각 운영 DB에 새 템플릿을 생성·활성화했습니다. 이 사례로 응답 성공과 생성 품질 경로를 별도로 확인해야 한다는 운영 기준을 남겼습니다.


구현한 내용

기능별 구현 범위상세 내용

과목 설계 파이프라인

▸교수가 PDF 참고자료를 업로드하면 OpenAI Vector Store에 저장하고, File Search로 관련 내용을 검색하여 강의 설계에 활용
▸프롬프트 체이닝으로 과목 개요 → 주차별 주제 → 차시별 학습요소 → 세부 콘텐츠를 단계적으로 생성
▸생성된 구조를 교수가 검토/수정 가능. 수정 시 하위 콘텐츠만 부분 재생성
▸과목 유형(온/오프/블렌디드)에 따라 중간/기말고사 주차 자동 배치
▸벡터 스토어 만료 기간(한 학기)을 설정하고, 만료 항목 자동 삭제 크론으로 스토리지 관리

슬라이드 제작 파이프라인

이미지 생성

▸3단계 프롬프트 체인: 덱 공통 디자인 지침(팔레트, 타이포, 레이아웃) → 페이지별 개별 지침(역할, 서사, 텍스트 볼륨) → 두 지침을 합성하여 Gemini가 최종 이미지 생성
▸페이지 역할(cover/content/closing)에 따라 지침 우선순위를 다르게 적용
▸비율 자동 조정, 컬러 팔레트 설정, 다국어 대응, AI 워터마크 삽입

스크립트 생성

▸OpenAI Responses API로 페이지별 순차 생성. 이전 페이지의 컨텍스트를 유지하여 일관된 흐름의 강의 스크립트 생성

기타

▸기존 PDF/PPTX 업로드 시 AI 스크립트를 자동 생성하는 가져오기 모드
▸웹 기반 편집기에서 텍스트, 이미지, 레이아웃 수정 및 개별 재생성
▸BullMQ 워커로 전체 차시 일괄 비동기 생성, 실시간 진행률 표시

영상 생성 파이프라인

▸슬라이드 이미지와 TTS 음성을 합성하여 강의 영상을 자동 생성. 슬라이드 간 무음 여백(short/medium/long)을 삽입하여 자연스러운 전환 제공
▸ElevenLabs TTS 요청 시 함께 반환되는 단어별 타임스탬프를 활용하여 VTT 자막을 자동 생성하고 영상과 동기화. 워터마크 위치 설정, 슬라이드 비율에 맞춘 영상 해상도 자동 조정 지원
▸완성된 영상, 자막, 메타데이터를 패키지로 묶어 Commons(자사 CMS)로 직접 내보내기

음성 생성

▸ElevenLabs TTS를 활용한 AI 음성 생성. 교수가 음성 샘플을 녹음/업로드하면 보이스 클로닝으로 개인화된 강의 음성 제공
▸내장 보이스와 클로닝 보이스를 구분 관리. 글로벌 기본 보이스와 과목별 보이스를 분리 설정 가능

라이선스·권한·사용량

AICMS-800 · 916 · 1022
▸기관의 계약 기간, 기능 범위, 슬라이드·영상 총량과 1인당 기본 사용량을 하나의 라이선스로 관리
▸역할·신분그룹의 권한을 조합하고 개별 사용자 설정을 최우선으로 해석하는 다층 권한 모델
▸진행 중인 예상 사용량까지 함께 판정해 동시 요청의 총량 초과를 차단하고, 필터·정렬·엑셀 내보내기로 운영 현황 제공

공동 작업과 LMS 직접 게시

AICMS-831 · 905
▸계정 공유 없이 교수가 조교를 프로젝트 단위로 초대하고, 소유·공유 프로젝트와 작업 주체를 명확히 구분
▸초대 범위 안에서는 이용 권한이 없는 조교도 참여할 수 있도록 예외 권한과 사용량 귀속 정책 설계
▸슬라이드·동영상을 LMS 과목/주차에 직접 게시하면서 CMS에는 자동 보관해 이중 작업 제거

교수자 AI 아바타 영상

AICMS-951
▸초상권·음성 동의와 본인 얼굴 확인을 포함한 아바타 자산 라이프사이클 구성
▸TTS → GPU 립싱크 → FFmpeg 크로마키 제거·PIP 합성 → 자막·Commons 게시 파이프라인
▸외부 GPU는 LatentSync 추론만 담당하도록 경계를 격리하고, 긴 영상은 세그먼트 단위로 재시도

분산 환경에서의 동시성·안정성 설계

▸작업 유형별 BullMQ 큐 분리 — 각 큐의 특성(CPU/IO)에 맞는 동시성 한도 설정
▸ffmpeg CPU 핀닝으로 영상 인코딩 프로세스의 자원 격리
▸Redis 분산 락, 원자적 버전 관리, 고아 Job 자동 복구
▸Graceful Shutdown + PM2 통합으로 무중단 배포
▸Circuit Breaker로 외부 AI API 장애 격리

인증 & 인프라

▸OpenAPI(Swagger) 기반 API 설계
▸JWT SSO — LMS 로그인 사용자가 별도 인증 없이 접근. 역할 기반 권한 체크
▸정적 파일 서빙 최적화 — Apache Alias로 미디어 파일 직접 서빙
▸PgBouncer 환경에서의 Prisma prepared statement 충돌 해결

문제 해결 사례

슬라이드 이미지 생성 방식 전환 — 템플릿에서 AI 네이티브 이미지 생성으로

문제 상황 개발 초기 2개월은 LLM이 만든 JSON을 HTML 템플릿에 매핑해 슬라이드를 렌더링했다. 레이아웃이 늘 때마다 템플릿을 추가해야 했고 시각적 다양성도 제한됐다.

문제 정의 출력 안정성과 시각 품질, 데모 일정과 템플릿 유지 비용을 동시에 만족할 생성 방식을 선택해야 했다.

가설 Gemini의 출력 편차를 공통·페이지별 디자인 지침으로 제한하면, 초기 불완전함을 감수하더라도 템플릿을 계속 추가하는 방식보다 제품 목표에 가까워질 수 있다고 봤다.

행동 외부 서비스, LaTeX, HTML 템플릿 고도화와 AI 이미지 생성을 비교하고 Gemini를 선택했다. 덱 공통 지침 → 페이지별 지침 → 이미지 합성의 3단계 프롬프트 체인을 적용했다.

상세: 네 가지 선택지 비교와 결정 과정

1. 외부 서비스 (Canva, Plus AI, SlideSpeak 등)

각각을 검토했지만, 서비스에 필요한 수준의 커스터마이징이 불가능하거나 비용 구조가 맞지 않았다.

2. LaTeX 기반 생성

분명한 장점이 있었다. AI를 활용하면 안정적이고 예측 가능한 포맷으로 슬라이드가 나오고, 학계에서 익숙한 형식이기도 했다. 다만 디자인 자유도가 제한적이고, 교수들이 기대하는 현대적인 시각 품질을 달성하기 어려웠다.

3. HTML 템플릿 고도화

기존 방식의 연장선이라 레이아웃 다양성과 템플릿 유지 비용 문제가 남는다.

4. AI 이미지 생성 (Gemini NanoBanana)

그 무렵 Gemini의 새로운 이미지 생성 모델이 공개되었다. LLM으로 슬라이드 이미지를 직접 생성한다는 것이 당시에는 생소한 접근이었고, 실제로 초기 버전에는 미세한 글자 깨짐이나 이미지 수정 시 원하는 대로 반영되지 않는 문제가 있었다.

결정

당시에는 LaTeX이 출력 안정성에서 유리했지만, 데모 일정 안에서 시각 품질을 높이고 템플릿 유지 비용을 줄이는 목표를 함께 만족하기 어려웠다. 그래서 출력 편차를 감수하고 Gemini 이미지 생성 방식을 선택했으며, 공통 디자인 지침과 페이지별 지침으로 편차를 줄이는 데 집중했다.

일관성 확보

결과 일관성은 3단계 프롬프트 체인(덱 공통 디자인 지침 → 페이지별 지침 → Gemini 이미지 합성)으로 보완했다. 후속 모델에서 글자 깨짐과 수정 정밀도가 개선되면서 현재 제품 요구에는 이 방식이 더 잘 맞았다.

성과 생성 경로를 AI 이미지 방식으로 전환했고, 후속 모델에서 글자 깨짐과 수정 정밀도가 개선되면서 현재 제품 요구에 맞는 흐름으로 유지하고 있다.

회고 당시 선택의 이유는 설명할 수 있지만 템플릿 방식과의 시각 품질·수정 성공률 비교는 남아 있지 않다. 다음 모델 전환에서는 고정 프롬프트셋으로 글자 정확도, 수정 반영률과 생성 비용을 함께 측정해야 한다.

Vertex AI migration 이후 distributed rate limiting

문제 상황 Gemini API에서 Vertex AI로 전환한 뒤, 여러 사용자가 동시에 이미지를 생성하면 429 응답이 반복됐다.

문제 정의 worker가 여러 서버에 분산되어 있어 프로세스별 sleep이나 semaphore로는 서비스 전체의 RPM을 제어할 수 없었습니다. 병목은 개별 요청이 아니라 모든 서버가 공유해야 할 global limit이었습니다.

가설 Redis에서 permit을 원자적으로 관리하고 429 신호에 따라 AIMD 방식으로 허용량을 조절하면, 모든 워커가 같은 제한 상태를 공유하며 점진적으로 복구할 수 있다고 봤다.

행동 permit 획득·반환·429 report용 Lua script, 중복 감속을 막는 wave ID, 4-state machine과 Circuit Breaker를 구현했습니다. Redis 장애 시에는 local concurrency limit으로 fallback하게 했습니다.

상세: 4-Layer 아키텍처와 AIMD 알고리즘

4단계 호출 레이어

Worker
  │  로컬 병렬 제어 (동시 이미지 생성 수 제한)
  ▼
Distributed Rate Limiter
  │  Redis 기반 전역 permit + 429 적응 제어
  ▼
Circuit Breaker
  │  5xx/네트워크 장애 감지 → 서비스 차단
  ▼
API Client → Vertex AI (retry 비활성화, 직접 제어)

분산 Rate Limiting (AIMD)

TCP 혼잡 제어의 AIMD(Additive Increase/Multiplicative Decrease) 알고리즘을 차용한 4-State FSM을 설계했다.

NORMAL (permit 40) → 429 발생 → COOLDOWN (5s→10s→20s→30s)
COOLDOWN → 타이머 만료 → PROBE (permit 1개로 테스트)
PROBE → 3회 연속 성공 → DEGRADED (1→2→4→8→16→32→40)
DEGRADED → baseLimit 도달 → NORMAL

Redis Lua 원자적 연산

permit 획득·반환·429 보고를 3개의 Lua script로 구현했습니다. Redis의 single-thread 실행으로 여러 서버에서도 race condition이 발생하지 않게 했습니다. 같은 burst에서 발생한 여러 429가 limit을 과도하게 줄이지 않도록 wave ID 기반 중복 제거도 구현했습니다.

Circuit Breaker

외부 API 호출에 Circuit Breaker(CLOSED→OPEN→HALF_OPEN)를 적용했습니다. 429는 서비스 장애가 아니므로 Circuit Breaker 대상에서 제외하고, distributed rate limiter가 별도로 처리하도록 분리했습니다.

Redis 장애 대비

Redis 자체가 장애가 나는 경우를 대비하여 로컬 동시성 제어로 자동 폴백. 10초 복구 윈도우 후 Redis 재시도.

성과 429가 발생하면 새 요청량을 줄이고, 대기 후 소수의 요청으로 상태를 확인한 뒤 단계적으로 허용량을 복구하도록 구성했다. 429와 5xx·네트워크 장애도 다른 경로로 분리했다.

회고 제어 구조의 동작은 확인했지만 429 발생률과 복구 시간의 전후 수치는 아직 제시하지 못했다. 다음에는 동시 사용자 수별 429 비율, 대기 시간과 처리량을 함께 기록해야 한다.

영상 생성 파이프라인 성능 개선

문제 상황 여러 사용자가 영상을 생성하면 ffmpeg이 CPU를 점유해 같은 서버의 API와 다른 워커까지 느려졌다. 슬라이드가 길수록 음성·영상 싱크도 어긋났다.

문제 정의 하나의 최적화 문제가 아니라 CPU 자원 경합, 단계별 동시성, 스트림 길이 오차가 겹친 파이프라인 문제로 정의했다.

가설 ffmpeg 코어를 격리하고 I/O 작업과 CPU 작업의 동시성을 다르게 제한하며 실제 재생 시간을 기준으로 정렬하면, 서버 영향을 제한하면서 싱크 오차를 줄일 수 있다고 봤다.

행동 taskset CPU 핀닝, TTS·세그먼트별 mapLimit, ffprobe 기반 길이 측정과 오디오 패딩을 적용했다.

상세: CPU 격리, 병렬 처리, 싱크 드리프트 해결

1. ffmpeg CPU 핀닝으로 자원 격리

ffmpeg이 모든 CPU를 점유하는 것이 블로커 이슈였다. Linux의 taskset을 사용하여 ffmpeg 프로세스를 특정 CPU 코어에 고정(pinning)하여, 나머지 코어는 API 서버와 다른 워커가 사용할 수 있도록 격리했다.

taskset -c 0-1 ffmpeg -y -loop 1 -i slide.webp -i audio.mp3 ...

환경변수로 사용할 CPU 수를 설정 가능하게 하여, 서버 사양에 따라 유연하게 조정.

2. TTS/세그먼트 병렬 처리

영상 생성 과정에서 TTS 음성 생성과 세그먼트 인코딩을 순차적으로 처리하고 있었다. async.mapLimit으로 병렬 처리를 도입하되, 각 단계의 특성에 맞게 동시성을 다르게 설정했다.

TTS 생성:     mapLimit(pages, ttsConcurrency=10)      // I/O 바운드 (API 호출)
세그먼트 생성: mapLimit(pages, segmentConcurrency=2)   // CPU 바운드 (ffmpeg)
비디오 큐:     concurrency: 2~4                        // 전체 영상 작업 동시 수

TTS는 외부 API 호출이라 I/O 바운드이므로 10개까지 병렬, 세그먼트 인코딩은 CPU 바운드이므로 2개로 제한.

3. 오디오/비디오 싱크 드리프트 수정

슬라이드가 많아질수록 오디오와 비디오의 싱크가 서서히 어긋나는 현상이 있었다. 원인은 ffmpeg 인코딩 시 비디오 스트림(5fps, 200ms 프레임 간격)과 오디오 스트림의 길이가 미세하게 달라서, concat 시 오차가 누적되는 것이었다.

ffprobe로 실제 재생 시간을 측정하고, -itsoffset과 오디오 패딩으로 밀리초 단위 정렬을 적용했다. 50장 이상으로 구성한 테스트 영상에서는 눈에 띄는 싱크 드리프트가 관찰되지 않았다.

4. 슬라이드 간 음성 여백

슬라이드 전환 시 오디오에 포즈가 없이 다음 슬라이드 첫 문장이 바로 이어져서 부자연스러웠다. ffmpeg의 anullsrc 필터로 무음 오디오를 생성하고, 동일한 포맷/길이의 무음은 캐시하여 반복 생성을 최소화하면서 자연스러운 전환을 확보.

성과 50장 이상으로 구성한 테스트 영상에서 눈에 띄는 싱크 드리프트가 관찰되지 않았다. 다만 인코딩 시간과 API 지연의 전후 수치는 남아 있지 않다.

회고 다음에는 동시 작업 수별 API 지연과 영상 처리 시간을 기록하고, 별도 변환 서버로 인코딩을 옮겼을 때의 효과를 비교해야 한다.

설계 가설 — 대용량 참고자료를 지식 구조로 먼저 정리하기

문제 상황 과목 설계는 OpenAI Vector Store와 File Search로 구현했지만, 참고 파일이 20개 이상일 때 강의 구성 품질이 불안정해졌다.

문제 정의 파일이 많아질수록 검색 청크를 예측하기 어렵고, 여러 문서의 개념과 선후관계를 종합하는 데 검색 결과만으로는 부족했다.

가설 업로드 단계에서 개념·출처·선후관계를 먼저 추출하고, 설계 단계에서는 이 구조를 중심으로 사용하면 원문 검색만 사용할 때보다 누락을 줄일 수 있다고 가정했다.

행동 계획 읽기, 강의 설계, 세부 내용 생성을 세 단계로 나누고 기존 File Search 방식과 같은 자료·평가 기준으로 비교하도록 실험을 설계했다.

상세: 검토 중인 Knowledge Structure First 설계

출발점: 검색과 설계를 한 호출에서 분리할 수 있는가

기존 구조는 검색한 원문을 곧바로 강의 설계에 사용한다. 대안은 업로드 단계에서 교육 가능한 개념과 관계를 먼저 추출하고, 설계 단계에는 이 구조를 우선 제공하는 방식이다. 원문은 세부 내용을 채울 때 다시 참조한다.

검증할 가정

  • 강의의 순서와 학습 목표를 잡는 단계에서는 원문 전체보다 개념과 선후관계가 더 유용한가.
  • 문서별 중요도와 출처를 함께 보존하면 검색 청크만 사용할 때보다 누락을 줄일 수 있는가.
  • 읽기, 설계, 세부 내용 생성을 나누는 비용이 품질 향상으로 상쇄되는가.

목표 표현

입력 문서에서 개념, 출처, 선후관계, 난이도와 교육 단위를 구조화하고, 강의의 학습 목표와 순서를 설계할 때 이 표현을 사용한다.

제안: Knowledge Structure First

기존: 문서 → (검색) → LLM → 강의. LLM이 읽기와 사고를 동시에 수행.

제안: 문서 → 지식 구조 → LLM → 강의. 읽기와 사고를 분리.

  • Phase 1 (읽기): 업로드 시점에 각 문서의 지식 구조를 비동기로 추출 — 개념, 개념 간 선후관계, 난이도, 교육 가능 단위(teachable units)
  • Phase 2 (설계): 지식 구조(JSON)를 중심으로 강의를 설계하고, 필요한 경우 출처를 다시 조회
  • Phase 3 (채우기): 특정 레슨의 세부 콘텐츠 생성 시에만 source_ref로 해당 문서의 해당 부분을 가져와서 원본 기반으로 생성

다음 검증

아직 구현 전 설계 가설이다. 문서 규모별 개념 수와 누락률, 출처 정확도, 토큰 사용량, 강의 구성 품질을 기존 File Search 방식과 같은 평가셋에서 비교해야 한다.

성과 아직 구현 전이므로 성과는 없다. 문서 규모별 누락률, 출처 정확도, 토큰 사용량과 강의 구성 품질을 비교할 평가 항목까지 정의한 상태다.

회고 설계 가설과 구현 성과를 같은 수준으로 제시하지 않는다. 실험 결과가 나온 뒤 유효했던 자료 규모와 실패 조건까지 기록할 예정이다.

스크립트 생성 워크플로우 재설계 — 속도와 정확도의 절충

문제 상황 슬라이드가 30장 이상이면 스크립트 생성이 타임아웃됐고, 70장 사례에서는 10분 이상 기다린 뒤 실패했다.

문제 정의 한 번의 큰 요청은 길이에 취약했고, 요청을 병렬 배치로 나누면 배치 사이의 맥락이 끊겼다. 속도, 안정성, 맥락 유지가 서로 충돌했다.

가설 페이지를 순서대로 생성하면서 previous_response_id로 맥락을 넘기면 완료 시간은 늘지만 시간 정확도와 실패 복구는 나아질 것이라고 봤다.

행동 페이지 단위 순차 생성, 페이지별 최대 2회 재시도, 완료 즉시 표시와 명시적 오류 상태를 구현했다.

상세: 배치 → 순차 전환과 트레이드오프

1차 대응: 배치 사이즈 축소 + 병렬 처리

전체 페이지를 한 번에 보내던 것을 20페이지씩 나누어 병렬로 요청. 각 배치에 전체 맥락(시스템 프롬프트)을 첨부하고, 유저 프롬프트에서 현재 생성할 페이지 인덱스를 지정. 타임아웃은 줄었지만, 배치 간 컨텍스트가 공유되지 않아 스크립트의 일관성이 떨어지는 문제가 남았다.

재설계: 순차 생성 + Responses API

발상을 전환했다. "한 번에 많이" 대신 "한 페이지씩 순차적으로" 생성하되, OpenAI Responses API의 previous_response_id를 활용하여 이전 페이지의 컨텍스트를 자동으로 유지하는 방식이다.

트레이드오프 분석

배치 방식 대비 순차 방식은 전체 완료 시간이 길어진다는 단점이 있었다. 하지만 다른 측면에서의 이점이 그 단점을 충분히 상쇄했다:

  • 안정성: 한 번에 처리하는 컨텍스트를 줄여 초과 위험을 낮춤. 오류가 난 페이지만 최대 2회 재시도하고, 이후에는 명시적 오류 상태를 표시하고 중단
  • 품질: previous_response_id로 컨텍스트가 연속 유지되므로, 앞 페이지의 내용을 참고한 일관성 있는 스크립트 생성. 배치 방식에서는 불가능했던 부분
  • 사용성: 완료되는 스크립트를 순서대로 즉시 표시. 사용자는 전체 완료를 기다릴 필요 없이 첫 페이지부터 확인하고 검토 가능

성과 10페이지 테스트에서 68초·목표 강의시간 정확도 62%였던 배치 방식이 순차 방식에서는 162초·99%가 됐다. 완료 시간 2.4배를 감수하고 정확도를 37%p 높였다.

회고 이 수치는 한 가지 10페이지 사례의 비교다. 페이지 길이와 주제별 반복 측정, 첫 결과가 표시되기까지의 시간과 실패율을 추가해야 일반화할 수 있다.

클라이언트 성능 최적화

문제 상황 데모 운영 중 "AI Commons를 여러 탭으로 열면 PC가 느려진다", "슬라이드 편집 시 버벅인다"는 피드백이 들어왔다. 전체적인 성능 테스트를 진행하면서 원인 조사에 착수했다.

문제 정의 비활성 탭 폴링, 넓은 캐시 무효화, 불필요한 렌더링, 순차 DB 조회와 중복 상태 조회가 함께 메인 스레드와 서버 연결을 점유하는 문제였다.

가설 각 원인을 분리해 불필요한 작업과 네트워크 왕복을 줄인 뒤 같은 부하 조건에서 다시 측정하면, 탭별 Long Task와 총 블로킹 시간이 함께 감소할 것이라고 봤다.

행동 폴링 조건, 캐시 무효화 범위, 컴포넌트 참조, DB 조회와 상태 수신 경로를 각각 수정했다.

상세: 5가지 원인 분석 및 해결 과정

1. 비활성 탭의 불필요한 폴링

작업 상태를 확인하기 위한 폴링이 React Query 외부에서 setInterval로 구현되어 있어서, 탭이 비활성 상태여도 5초마다 서버에 요청을 계속 보내고 있었다. 탭을 여러 개 열면 이 요청이 누적되어 DB 커넥션 풀이 포화되는 핵심 원인이었다.

해결: React Query의 refetchInterval로 전환하여, AI 작업이 실제로 진행 중일 때만 폴링이 동작하고 비활성 탭에서는 자동으로 멈추도록 변경했다.

2. 캐스케이드 캐시 무효화

React Query의 mutation 성공 콜백에서 관련 데이터의 캐시를 무효화할 때, 무효화 대상이 특정되지 않으면 슬라이드, 영상, 과목 등 모든 도메인의 캐시를 한꺼번에 날리고 있었다. React Query의 partial key matching 특성상 실제 refetch 수는 그보다 훨씬 많았고, 이 burst가 불필요한 API 재호출과 컴포넌트 리렌더링을 유발했다.

해결: 무효화 대상을 해당 리소스 도메인으로 명확히 한정하고, 공통 훅에서 무효화 로직을 직접 정의하지 않고 사용처에서 필요한 만큼만 옵션으로 추가하도록 구조를 개선했다.

3. 슬라이드 에디터 리렌더링

대량의 슬라이드를 편집할 때 불필요한 컴포넌트 리렌더링이 발생했다. 이벤트 핸들러나 배열 참조가 매 렌더마다 새로 생성되면서 하위 컴포넌트가 전부 다시 그려지고 있었다.

해결: React.memo와 커스텀 비교함수를 적용하고, 불안정한 참조들을 안정화하여 실제로 변경된 컴포넌트만 리렌더링되도록 개선했다.

4. 서버 순차 조회 → 병렬화

작업 상태를 조회하는 API에서 여러 종류의 Job 테이블을 순차적으로 조회하고 있었다. 최대 5번의 DB 왕복이 필요했고, 응답이 길어지면 커넥션 점유 시간이 늘어나 풀 포화를 악화시켰다.

해결: Promise.all로 최대 5개의 순차 조회를 하나의 병렬 구간으로 묶었다.

5. SSE와 폴링 로직 통일

작업 상태를 받는 경로가 SSE(Server-Sent Events)와 API 폴링으로 이원화되어 있어서, 같은 데이터를 두 경로로 중복 요청하는 경우가 있었다.

해결: 두 경로를 하나로 통일하여 중복 요청을 제거했다.

검증 결과

Chrome DevTools (CPU 4× throttling + Slow 4G)에서 슬라이드 에디터 6개 탭과 전체 이미지 생성을 동시에 실행하며 10분간 측정했습니다.

지표 개선 전 개선 후 개선
Long Tasks (탭 평균) 136개 4.7개 96.5% ↓
총 블로킹 시간 (탭 평균) 10.85초 0.57초 94.7% ↓

단일 통제 실행에서 얻은 결과다. CPU 4배 감속과 느린 네트워크를 가정했으며, 실제 기기 전반이나 반복 실행의 분산을 뜻하지는 않는다.

성과 같은 조건에서 탭 평균 Long Task는 136개에서 4.7개로, total blocking time은 10.85초에서 0.57초로 줄었습니다.

회고 여러 원인을 한 번에 바꿔 전체 효과는 확인했지만 각 변경의 기여도는 분리하지 못했다. 다음 측정에서는 변경별 프로파일과 실제 저사양 기기 결과를 함께 남겨야 한다.


회고

분산된 실패는 별도의 상태로 다뤄야 했습니다. rate limit, CPU contention과 Long Task의 부분 실패를 하나의 exception handling으로 묶지 않고 각각의 control state와 recovery path로 분리했습니다.

모델과 제품 사이의 경계가 교체 비용을 결정했습니다. 이미지 생성과 GPU 립싱크 추론을 외부 실행 경계로 격리해 모델이 바뀌어도 기존 TTS·FFmpeg·게시 흐름을 재사용했습니다.

정책은 코드보다 먼저 충돌했습니다. 라이선스와 공동 작업의 계약·권한·사용량 규칙을 문서로 먼저 고정하고, 구현과 테스트 근거를 같은 이슈 체계에 연결했습니다. 다음 단계에서는 이 구조의 효과를 429 비율, 복구 시간과 재작업량으로 비교해야 합니다.