shape

스타트허브 - 나르샤 2차 평가 회고하기

프로젝트 설명

FastAPI로 AI 서버를 구축하고 Pinecone으로 추천 알고리즘 구현하기

기술 스택

  • Kotlin
  • Spring Boot
  • FastAPI
  • AWS
  • GCP
  • Docker Compose
  • Pinecone

참여인원

백엔드 2명

기간

2025.07 ~ 2025.09

상세 내용

  1. 백엔드 플로우
    • GCP(Google Cloud Platform) 환경에 Docker로 배포하여 운영합니다. 전체 시스템은 GitHub에 소스코드를 저장하고, GitHub Actions를 통해 CI/CD 파이프라인을 구성하여 자동으로 Docker 이미지를 빌드하고 DockerHub에 업로드합니다.
    • 이후 GCP의 Compute Engine 인스턴스에서 해당 이미지를 pull 받아 Spring Boot 애플리케이션, FastAPI 서버, Nginx를 각각 Docker 컨테이너로 실행합니다. Nginx는 리버스 프록시 역할을 하여 외부 요청을 내부 Spring Boot 서버와 FastAPI 서버로 전달합니다.
    • Spring Boot 서버는 CloudSQL을 통해 데이터베이스와 통신하고, 정적 파일은 Google Cloud Storage에 저장되어 URL을 통해 접근할 수 있도록 구성되어 있습니다.
    • 또한 FastAPI 서버는 별도의 마이크로서비스로서 AI 연산을 담당하며 AWS의 Pinecone DB와 직접 통신하여 벡터 검색 기능을 수행합니다. 이와 같은 구조를 통해 안정적인 배포 환경과 확장성을 고려한 백엔드 인프라를 구축하였습니다.
    백엔드 플로우 1
  2. 트러블 슈팅 1
    • <문제 상황>
    • 서비스 고도화를 위해 공고의 상세 정보인 지원 분야, 대상 연령, 창업 업력 등을 수집해야 했습니다. 기존에는 목록 페이지만 순회하며 제한적인 정보만 수집했으나, 상세 페이지 접속을 추가하면서 HTTP 요청 수가 급격히 증가하여 스크래핑 성능이 크게 저하되었습니다.
    • 이로 인해 전체 데이터 수집 시간이 수십 배 이상 증가하였고, 외부 서버에 과도한 부하가 발생하며 IP 차단의 위험이 존재하게 되었습니다. 또한 요청 횟수 증가로 네트워크 오류나 타임아웃 발생률도 높아지는 문제가 발생하였습니다.
    • <원인 분석>
    • 목록 페이지에서 각 공고의 상세 페이지 URL을 얻은 후 공고마다 별도로 요청을 보내면서 요청 횟수가 기하급수적으로 증가한 것이 문제였습니다.
    • 예를 들어 10페이지의 목록에서 페이지당 20개의 공고가 있을 경우, 기존에는 10번만 발생하던 요청이 이제 200번 이상의 추가 요청으로 늘어나면서 데이터베이스에서 발생하는 N+1 쿼리 문제와 유사한 'N+1 스크래핑 문제'가 발생하였고, 모든 공고를 무조건 순회하다보니 불필요한 요청이 다수 발생하게 되었습니다.
    • <해결 방안>
    • 이를 해결하기 위해 먼저 DB 중복 체크를 통해 불필요한 상세 요청을 방지하도록 했습니다. 각 공고의 상세 페이지 URL을 먼저 확인하여 데이터베이스에 이미 존재하는 경우에는 스크래핑을 건너뛰고, 새로운 공고 일 경우에만 상세 페이지에 접속하여 정보를 수집하도록 로직을 변경했습니다.
    • 또한 공고 목록이 일반적으로 최신순으로 정렬되어 있다는 점을 활용하여 불필요한 페이지 탐색을 조기 종료하는 로직을 추가했습니다. 한 페이지 내에서 새로운 공고가 하나도 저장되지 않을 때마다 consecutivePagesWithNoNewSaves 변수를 증가시키고, 이 변수가 임곗값에 도달하면 루프를 종료하여 이후 페이지 탐색을 중단하도록 구현했습니다.
    • <기대 효과>
    • HTTP 요청 수를 신규 공고 수만큼 최소화할 수 있게 되었고, 전체 스크래핑 시간을 단축하면서 안정적인 데이터 수집이 가능해졌습니다. 또한 외부 서버에 주는 부하를 줄이고 네트워크 오류나 타임아웃 발생률을 낮출 수 있었고, 상세정보 수집이라는 새로운 요구사항을 충족하면서도 성능 저하 문제를 효과적으로 해결할 수 있었습니다.
    • 이번 경험을 통해, 단순히 기능 구현만 고려하는 것이 아니라 성능과 효율성을 동시에 고민하는 것의 중요성을 알게 되었습니다. 특히 N+1 문제처럼 눈에 보이지 않는 비효율적인 부분들이 실제 서비스에서는 서버에 큰 부담으로 이어질 수 있다는 것을 알게 되었습니다. 앞으로도 새로운 요구사항을 반영할 때는 반드시 성능과 효율성을 동시에 고려하며 불필요한 작업을 줄이는 최적화 방법을 항상 염두에 두어야겠다고 생각하였습니댜.
    트러블 슈팅 1 1
    트러블 슈팅 1 2
  3. 트러블 슈팅 2
    • <문제 상황>
    • 1. 벡터 업로드 성능 문제가 있었습니다. 초기 구현에서는 공고마다 ‘index.upsert(vectors=[(job_id, vector, metadata)])’를 호출하는 방식으로 데이터를 삽입했는데, 이 과정에서 수백 개의 공고를 업로드할 떄 1시간이 소요되었습니다.
    • 2. 거리 선택 오류가 있었습니다. 인덱스를 처음 생성할 때 metric=‘euclidean’(유클리드 거리)을 사용했는데, 이로 인해 긴 텍스트와 짧은 텍스트 간의 의미적 유사도가 왜곡되었습니다. 긴 설명에서 각각 다른 두 글은 의미적으로 유사하지만, 유클리드 거리 기준으로는 거리가 멀게 계산되었습니다.
    • 3. 스케줄링 과정에서 메모리 누수 문제가 있었습니다. interval 방식으로 업데이트 작업을 수행했는데, 시간이 지날수록 메모리 사용량이 증가하여 결국 OOM(Out of Memory) 에러가 발생했습니다.
    • <원인 분석>
    • 1. 벡터를 하나씩 업로드하는 방식은 업로드마다 새로운 HTTP 요청이 발생했습니다. TCP 연결 설정, HTTP 헤더 전송 , 서버 응답 대기 등의 오버헤드가 반복되며 성능이 저하되었고, Pinecone이 작동하는 AWS 서버도 수많은 요청 처리로 리소스를 비효율적으로 사용하게 되었습니다.
    • 2. SentenceTransformer 임베딩 벡터는 텍스트 길이에 따라 크기가 달라집니다. 유클리드 거리는 크기까지 반영하므로, 의미가 유사해도 길이가 길면 거리값이 커져 텍스트 길이가 결과에 불필요하게 개입하는 문제가 발생했습니다.
    • 3. 업데이트 스케줄러가 메인 프로세스와 같은 메모리 공간에서 동작하면서 대용량 데이터 처리 중 생성된 메모리가 즉시 해제되지 않았습니다. 임베딩 모델은 GPU 메모리와 RAM을 동시에 사용해 반환이 지연되었고, 누적된 사용량이 시간이 지날수록 증가했습니다. 또한 1시간마다 반복 되는 업데이트 주기 자체도 불필요한 부하를 주었습니다.
    • <해결 방안>
    • 1. Pinecone의 배치 업로드 기능을 활용해 여러 벡터를 한 번에 업로드하도록 개선하였습니다. ‘for i in range(0, len(vectors_to_upsert), 100): batch = vectors_to_upsert[i:i+100]; index.upsert(vectors=batch)’ 구조로 100개씩 묶어 업로드하도록 변경하였습니다.
    • 이 방식으로 네트워크 요청 횟수를 1/100로 줄이고, 각 요청에서 더 많은 데이터를 효율적으로 전송할 수 있게 되었습니다.
    • 2. 인덱스를 새로 생성하면서 metric='cosine'(코사인 거리)로 변경하였습니다. 코사인 거리는 벡터의 방향만 고려하므로 텍스트 길이에 영향받지 않고 의미적 유사성을 측정합니다.
    • 3. BackgroundScheduler로 업데이트 작업을 메인 프로세스와 분리하였습니다. 주기를 interval에서 cron으로 변경하여 하루에 한 번만 실행되도록 조정하였습니다.
    • <기대 효과>
    • 1. 벡터 업로드 속도가 향상되어 실시간에 가까운 데이터 반영이 가능해졌고 네트워크 사용량 감소로 인프라 비용을 절감할 수 있었습니다.
    • 2. 유클리드 대신 코사인 거리 기반 유사도 방식을 사용하여 텍스트 길이와 무관하게 의미를 정확히 반영하여 추천 정확성과 다양성이 향상되었습니다.
    • 3. 메모리 누수 문제가 해결되어 서버 중단 위험이 줄었습니다.
    • 4. 신규 공고 반영이 빨라지고 추천 품질이 좋아졌습니다.
  4. 후기
    • 이번 프로젝트를 통해 처음으로 벡터 데이터베이스와 임베딩 기반 AI 시스템을 서비스에 적용해보았습니다. 기존에는 RDBMS와 NoSQL만 사용했지만, Pinecone을 도입하면서 새로운 데이터 처리 방식을 배우게 되었습니다.
    • 벡터 검색은 SQL의 정확 매칭과 달리 유사도 점수를 기반으로 결과를 반환한다는 점이 인상적이었습니다. 처음에는 근사치 검색 개념이 낯설었지만, 점차 의미적 연관성을 찾는 특징을 이해할 수 있었습니다.
    • 또한 초기에는 벡터를 개별 업로드하면서 성능 문제가 있었으나, 배치 처리로 전환하여 속도를 개선할 수 있었고, 이를 통해 네트워크 오버헤드 최적화의 중요성도 깨닫게 되었습니다.
    • SentenceTransformer 모델을 선택할 때에는 성능, 처리 속도, 다국어 지원을 모두 고려해야 했습니다. 특히 한국어 공고에 영어가 섞여있는 경우가 많아, 다국어 모델인 paraphrase-multilingual-MiniLM-L12-v2를 사용하였습니다.
    • 이번 경험을 통해 기술을 선택할 때 단순히 최신성을 따르기보다는 비즈니스 요구와 제약을 종합적으로 고려하는 것이 중요하다는 점을 배울 수 있었습니다.