[AI] 설계 단계별 회고 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki
1. 모델 API 설계
a. 배운 점
-
시스템 설계에서 가장 중요한 건 역할과 책임의 경계를 명확히 하는 것임을 깨달았고, 백엔드 담당자와 소통이 중요하다는 것을 배움
- 프론트/백엔드/AI 서버 사이에서 데이터 전달 경로(URL, 이미지 자체 등)를 명확히 정의하는 것이 필수였음
-
API는 단순히 외부 요청을 처리하는 게 아니라, 서비스 흐름의 정책을 반영하는 경계선이라는 걸 처음 체감함
- “Embedding을 요청 받아야 하나? 내부에서 자동화해야 하나?” → 이런 결정 자체가 서비스 설계의 방향성을 반영함
-
AI 모델을 잘 활용하려면, 모델의 정확도뿐 아니라 “언제, 어떻게, 무엇을 입력받고 어떤 방식으로 결과를 전달할지”의 흐름이 핵심임을 체득함
-
초기 설계 단계에서 API만이 아니라 처리 흐름 전체(저장, 요청, 응답, 후속 처리)를 고려해야 한다는 걸 처음 체득함
-
특히, 병렬 요청 상황에서 메시지큐라는 개념을 알게 된 것이 가장 큰 수확 중 하나
→ FastAPI가 직접 GPU 서버를 호출하거나 모든 후속 작업을 동기적으로 수행하는 구조는 확장에 치명적이라는 사실을 처음 배움
→ 메시지큐를 도입하면 처리 흐름을 느슨하게 결합하면서도 순서를 보장하고, 재시도까지 가능하다는 점이 깊은 인사이트로 남음
b. 어려웠던 점
- FastAPI 서버가 어디까지 처리하고, 어떤 데이터를 받아야 하는지 경계가 모호했음
- 이미지 자체를 받을지? URL을 받을지?
- Stable Diffusion 결과를 파일로 저장할지? 그냥 이미지 바이너리로 응답할지?
- Embedding은 명시적으로 요청받아야 할지, 아니면 자동으로 내부에서 처리할지 결정이 어려웠음
- 기능 단위로 Endpoint를 나누면 비동기 요청이 많이 들어올 텐데, 부하 분산과 병렬성 확보를 어떻게 해야 할지 감이 안 잡혔음
c. 해결 방법
- 백엔드와 협의하여 이미지 데이터는 Cloud Storage에 저장 → FastAPI는 URL만 전달받는 구조로 정리
- Embedding은 백엔드의 명시적 요청에 의해 수행하되, 내부적으로 캐시 확인 후 없는 경우만 연산
- 기능마다 Endpoint를 분리하되, 후속 처리는 큐 기반 비동기로 정리해 API 부하와 처리 병렬성 문제를 모두 해결
2. 추론 성능 최적화
a. 배운 점
-
코랩 테스트 환경을 넘어 실제 서빙을 준비하는 과정에서 stable diffusion 모델의 리소스 이용량이 매우 많다는 것을 체감함
→ gcp VM에서의 테스트를 위해 도커 래핑을 시도했으나 모델 사이즈 때문에 도커 이미지 래핑이 어려웠고, 파일 전송 역시 rsync로 이어받기 기능이 필요했음
-
SOTA 모델이 항상 좋은 건 아님.
→ RAM++ 모델이 태깅 성능은 뛰어났지만, 멀티태스크 활용성(CLIP의 임베딩 공간 공유 구조) 면에서는 CLIP이 훨씬 효율적이었음
→ 결국 우리가 구현하려는 다양한 기능(중복 탐지, 하이라이트 점수 등)에는 범용성과 구조의 단순성이 더 중요하다는 것을 체감
-
성능 최적화 과정 자체가 학습의 기회였음
- 캐싱, 배치 처리, 스레드 제한 등을 시도하면서 연산 병목을 직접 해결해봄
- RAM++ 구조 분석을 통해 추론/임베딩 모듈을 분리하고 재사용성을 확보하는 경험을 했음
- face_recognition의 병렬 처리 한계를 이해하고 arcFace 대안 검토를 해본 것도 설계적 통찰로 이어졌음
b. 어려웠던 점
- CLIP도 RAM++도 처음 다뤄보는 모델이었고, CPU 환경에서 최적으로 돌리기 위한 세팅이 명확하지 않았음
- 모델마다 내부 구조가 달라서, 단순히 추론을 돌리는 것 이상의 분석이 필요했음 (RAM++는 추론과 태깅이 하나로 묶여 있었음)
- face_recognition 모델은 병렬 처리가 되지 않아 실시간 처리에 제약이 있었고, 대부분의 얼굴 검출 모델들은 영상 실시간 분석과 같이 하나의 프레임에 대한 작업에 최적화되어 있기에 테스크에 적합한 모델을 찾기 어려웠음
- Stable Diffusion의 설정값들이 달라졌을 때, 스타일 변환 성능이 크게 달라지는 것을 확인함
- 성능 테스트를 하려 할 때도 단순히 추론 시간만 측정하는 게 아니라, CPU 사용량, 스레드 점유율, peak 메모리 사용량 등 실측 데이터 확보가 의외로 까다로움
c. 해결 방법
- Stable Diffusion은 최적의 설정값을 찾기 위해 테스트 진행, img2img 프롬프트 생성을 위해 제미나이 호출 프롬프트 엔지니어링
- 태깅에는 CLIP을 사용하면서, 다양한 기능에 재활용 가능한 임베딩 구조를 활용해 하나의 모델로 중복/하이라이트/카테고리 분류까지 처리하도록 구성
- 캐시와 배치, 스레드 제한을 도입하면서 CPU 환경에서도 처리량이 올라갈 수 있도록 최적화
- face_recognition 대체로 arcFace를 도입한 결과, 성능과 실행 속도 모두 개선됨
- 성능 테스트를 Python
psutil
,time
,subprocess
등을 조합해서 별도 스크립트를 짜서 최대한 정확히 측정하려고 노력
3. 서비스 아키텍처 모듈화
a. 배운 점
-
처음으로 서비스 전체 흐름을 아키텍처 다이어그램으로 그려보면서, 단순한 코드 설계가 아니라 실제 배포 환경(컨테이너 구성 등)을 염두에 두고 구성 요소 간 관계를 시각적으로 설계해 봤음
-
"임베딩만 따로 떼서 모듈화해야 하나?" → 이게 MSA(Microservice Architecture)의 일부 개념이라는 걸 알게 되었고, 향후 확장성과 독립 배포 가능성을 고려한 분리 설계가 왜 필요한지 체감함
-
아키텍처를 버전별(v1~v3)로 나눠서 계획하면서, "언제까지 무엇을 구현할지", "어떤 기능은 MVP에 포함/제외되는가"를 구조적으로 정리할 수 있었음
-
평소 고민이 많았던 폴더 구조 문제도 자연스럽게 해결됨
→ 기능 중심(모듈) → 서비스 중심(클러스터) → 공통 로직 중심(core)으로 나뉘면서 명확한 기준이 생김
-
여러 기능을 나누다 보니 자연스럽게 단일 책임 원칙(SRP)에 대한 감각이 생김
→ “이 기능은 여기까지만 책임져야 한다”는 경계를 고민하게 되었고, 각 모듈이 API 처리/연산/결과 정리에 어떤 책임을 갖는지 구조적으로 분리할 수 있었음
b. 어려웠던 점
-
처음에는 아키텍처 다이어그램 자체를 어떻게 시작해야 할지 조차 몰랐음
→ 어떤 걸 그려야 하고, 선은 어디로 이어야 하며, 어떤 컴포넌트가 있는지 막막했음
-
임베딩만 따로 컨테이너로 분리해야 하나? GPU 서버는 별도 인스턴스가 되는 건가?
→ MSA 개념과 클라우드 상의 배포 구조에 대한 지식이 부족해서 판단이 어려웠음
-
각 기능을 어디까지 나누고, 그 기능이 어떤 책임을 가져야 하는지 결정이 어려웠음
→ 특히 “단일 책임 원칙”을 처음 실감했지만, 어디까지 세분화해야 할지 균형을 잡기 힘들었음
-
아키텍처를 그리고 구조를 짜다 보니, “캐싱을 어디에 둘지” 같은 세부 구현 논점도 같이 고민하게 되었음
→ In-memory로 두면 인스턴스 간 공유가 안 되고, Redis로 두자니 운영/비용/성능 고려가 필요했던 상황
c. 해결 방법
-
구조를 단순화하는 데서 시작해서 → 기능 단위 → 서비스 단위로 확장해가며 점차 정제된 아키텍처로 다듬음
→ 처음에는 하나의 서버에 모든 기능을 두되 내부 모듈로 분리하고, 이후 확장에 따라 마이크로서비스 분리를 고려할 수 있는 기반을 마련
-
폴더 구조는 실제 라우팅/서비스 흐름과 맞춰
api
,services
,core
,models
로 구성→ 기능별 책임과 재사용성을 고려한 구조 정립
-
FastAPI → 서비스 → core 구조로 고정화하여 책임 단위를 통일하고, 추후 배포 단위로 나눌 수 있는 기준도 함께 정립
-
캐시/메시지큐 도입 방식 고민은 가장 단순한 구조로 구현한 후, 실질적인 병목이나 확장 이슈를 경험하면서 Redis를 도입하기로 계획
→ 문제가 생겼을 때 직접 체감을 했을 때 필요성을 명확히 판단할 수 있을 것이라 생각
4. LangChain / RAG / MCP 도입 검토
a. 배운 점
- LangChain은 복잡한 추론 흐름이나 도구 연동에 적합한 프레임워크라는 것을 이해했음. 단일 요청 구조에는 과설계가 될 수 있음을 체감함.
- RAG는 단순한 벡터 검색이 아니라, 최신성이나 질문 다양성이 요구되는 구조에 필요한 아키텍처임을 알게 되었음.
- MCP는 처음엔 잘 몰랐으나, 이번 검토를 통해 LLM이 도구를 상황에 따라 호출·제어하기 위한 표준 프로토콜이라는 점을 제대로 이해하게 되었음.
b. 어려웠던 점
- LangChain은 “LLM 쓰면 무조건 써야 하나?” 하는 막연한 불안이 있었고, 도입 기준을 세우는 게 어려웠음
- RAG는 왜 필요하지 않은지 구조적으로 설명하려면 고민이 필요했음
- MCP는 개념이 낯설고, 적용 조건도 복잡해서 실제 구조에 대입하며 판단하는 과정이 까다로웠음
c. 해결 방법
- LangChain은 단일 요청 흐름, 고정된 도구 호출 방식으로 충분해 제외
- RAG는 외부 지식이 필요 없는 이미지 기반 구조이므로 도입하지 않았음
- MCP는 우리 서비스가 LLM 기반이 아니고, 도구 호출도 고정되어 있어 적용 조건을 충족하지 않아 제외
5. 확장성과 안정성 확보를 위한 인프라 설계 회고
a. 배운 점
- 단일 인스턴스 기준으로 구성하던 구조가 다중 인스턴스로 확장되면 어떻게 달라져야 하는지를 처음으로 체계적으로 고민
- 예를 들어 인메모리 캐시(Redis)가 각 인스턴스에 따로 존재한다면, Load Balancer를 통해 후속 작업 요청이 다른 인스턴스로 전달될 수 있다는 문제를 인식하게 되었고, 이를 통해 공유 가능한 Redis 서버의 필요성을 스스로 체감
- 지금까지는 거의 고려하지 않았던 시스템 모니터링의 필요성과 관점(무엇을 보고, 언제 경고를 받아야 하는가?)에 대해 처음으로 관심을 갖게됨
- VPC 외부로 나가는 트래픽(NAT)의 구조를 처음 이해하게 되었고, NAT는 모든 요청이 하나의 출구로 나가는 게 아니라, 각 연결마다 NAT 세션이 분리된다는 사실, 그리고 Connection Pool은 애플리케이션 계층의 문제라는 점을 분리해서 이해하게 됨
- GCP 환경에 직접 배포해서 테스트하는 과정에서, 로컬 테스트로는 느낄 수 없었던 네트워크 병목이나 이미지 로딩 속도 문제를 체감했고, URL 기반 이미지 요청 자체가 병목이 될 수 있어 이미지 로딩조차 병렬로 처리해야 한다는 교훈을 얻음
b. 어려웠던 점
- 기존 설계를 단일 인스턴스 기준으로 했기 때문에, 스케일링 시 인메모리 Redis가 무력화된다는 문제를 뒤늦게 깨달았음
- 메시지큐나 캐시의 위치를 “그냥 Redis 쓰면 되겠지” 정도로만 생각했는데, 실제로 어디에 두고 어떤 방식으로 연결해야 하는지가 훨씬 복잡하다는 것을 실감했음
- NAT, 연결 수, 메시지큐 병목 등 운영 환경에서 실제로 문제가 될 수 있는 계층적 병목 요소들을 처음엔 구분하기 어려웠음
- 시스템 모니터링 역시, 막연히 “CPU 사용률 보면 되지”라고 생각했지만, 큐 길이, 처리 지연 시간, 서버별 부하 같은 실시간 상태를 어떻게 수집하고 경고할지를 고민하게 되었음
c. 해결 방법
- Redis는 캐시 및 메시지큐 역할을 동시에 수행하도록 하고, 모든 인스턴스가 접근 가능한 중앙 서버 구조로 설정
- NAT는 Connection Pool이나 메시지큐로 제어할 대상이 아니라는 것을 이해하고, 병목이 발생하더라도 하나의 Cloud NAT 인스턴스가 관리할 수 있는 최대 연결 수, 포트 할당률 등을 제어할 예정
- GCP 환경에 실제로 올려 테스트를 진행하며, 요청 단위 성능 병목이나 이미지 I/O 처리 속도를 직접 측정 → 병렬 처리로 성능 개선
- Prometheus, Grafana, Alertmanager를 도입해, 단순 리소스 사용률뿐 아니라 작업 큐 상태, 응답 지연 시간 등 서비스 흐름 전반을 실시간으로 관찰 가능한 구조를 마련