一覧へ戻る

이력서 RAG 채팅을 만들며 정리한 구조와 고민

개인 이력서에 질문할 수 있는 RAG 채팅을 만들면서 벡터 DB, 텍스트 검색, Codex app-server, 공개 채팅 보안, 실패 UI를 어떻게 나눠 생각했는지 기록합니다.
RAG
Architecture
Codex
NestJS
Next.js
UX
2026-07-0617 min read

이력서 RAG 채팅을 만들며 정리한 구조와 고민

이번 작업의 목표는 단순히 "이력서 페이지에 AI 채팅을 붙인다"가 아니었습니다.내 이력과 프로젝트 데이터를 한곳에 모아두고, 방문자가 자연어로 질문하면 그 데이터 안에서 근거를 찾아 답변하는 페이지를 만드는 것이 목표였습니다. 처음에는 당연히 벡터 DB를 중심에 두고 생각했습니다. RAG라고 하면 보통 문서를 chunking하고, embedding을 만들고, vector similarity로 검색한 뒤 LLM에 넘기는 그림을 먼저 떠올리기 때문입니다.그런데 실제 작업을 진행하면서 중요한 질문이 생겼습니다.
지금 데이터는 정말 벡터 검색이 먼저 필요한 데이터인가?
이번 글은 그 질문에서 출발해 구조를 다시 잡고, 기능을 운영 환경까지 붙이면서 했던 고민을 정리한 기록입니다.

1. 처음 목표: 이력 데이터를 런타임의 진실로 만들기

가장 먼저 정리해야 했던 것은 데이터의 위치였습니다.기존 이력 데이터는 MDX, Markdown, JSON에 가까운 형태로 흩어져 있었습니다. 사람이 읽기에는 좋지만, 채팅 API가 요청을 받을 때마다 파일을 직접 읽는 구조는 목표와 맞지 않았습니다. 사용자가 원한 구조는 명확했습니다.
  • 이력 정보는 DB에 모은다.
  • 질문 응답은 DB에 들어간 데이터에서 근거를 찾는다.
  • AI 모델은 자연어 답변 생성 역할만 맡긴다.
  • 모델은 아직 확정하지 않았으므로 교체 가능해야 한다.
그래서 런타임 구조는 "파일을 읽는 RAG"가 아니라 "DB를 읽는 RAG"로 잡았습니다.
이력 원본 파일
-> import pipeline
-> resume_source_items
-> chat API runtime retrieval
-> answer generation
-> web chat UI
여기서 핵심은 원본 파일을 버리는 것이 아니라 역할을 제한하는 것이었습니다. 원본 파일은 import input이고, 런타임 질문 응답의 source of truth는 DB row입니다.

2. 벡터 DB를 바로 쓰지 않은 이유

처음 설계에는 resume_vector_chunks와 embedding 설정도 포함했습니다. 하지만 운영 채팅을 붙이는 단계에서는 벡터 검색을 바로 중심에 두지 않았습니다.이유는 데이터 성격 때문입니다.이력서는 보통 다음과 같은 텍스트입니다.
  • 회사명
  • 프로젝트명
  • 기술 스택
  • 도메인 키워드
  • 성과와 역할
  • 업무 맥락
즉, "비슷한 의미의 긴 문단을 의미 공간에서 찾는 문제"이기도 하지만, 동시에 "정확한 키워드가 중요한 문제"이기도 합니다. 방문자는 보통 이렇게 묻습니다.
Oprimed에서 어떤 일을 했어?
프론트엔드 강점은 뭐야?
CI/CD 경험 있어?
이런 질문은 embedding이 없어도 회사명, 기술명, 경험/성과/강점 같은 키워드만으로 꽤 높은 품질의 후보를 뽑을 수 있습니다. 반대로 처음부터 벡터 DB를 필수로 두면 다음 부담이 생깁니다.
  • embedding provider와 모델을 지금 결정해야 한다.
  • 운영 환경에 API key와 dimensions 같은 설정이 추가된다.
  • vector index 재생성 절차가 필요하다.
  • 검색 품질이 나빠도 원인이 chunking인지 embedding인지 retriever인지 분리하기 어렵다.
그래서 결론은 이렇게 정리했습니다.
현재 운영 채팅:
DB text / keyword retrieval

미래 확장:
optional vector index pipeline
벡터를 완전히 부정한 것이 아니라, 지금 문제에 필요한 가장 작은 검색 구조를 먼저 운영에 올린 것입니다. 이 결정 덕분에 AI 모델 선정도 뒤로 미룰 수 있었습니다.

3. 자연어 처리는 모델이 아니라 경계로 먼저 정의했다

처음에는 "OpenAI embedding을 어떻게 붙였냐"는 질문이 나왔지만, 실제 목표는 특정 모델을 붙이는 것이 아니었습니다. 모델은 바뀔 수 있고, 지금 당장 확정하지 않아도 되는 영역이었습니다.그래서 구조를 모델 중심이 아니라 provider boundary 중심으로 잡았습니다.
ResumeRagService
-> ResumeRagRetrieverService
-> ChatProvider
   -> CodexAppServerProvider
   -> future OpenAI-compatible provider
현재 자연어 답변 생성은 Codex app-server를 통해 처리하도록 했습니다. API 서버는 Codex app-server에 ephemeral thread를 만들고, 검색된 이력 context와 질문을 넘긴 뒤 답변만 받습니다.중요한 점은 Codex가 검색을 직접 하는 구조가 아니라는 점입니다.검색은 API가 합니다. Codex는 검색된 근거 안에서 자연어 답변을 만듭니다. 이 경계를 두지 않으면 나중에 문제가 생겼을 때 원인을 분리하기 어렵습니다.
  • 검색 결과가 틀렸는가?
  • 검색 결과는 맞는데 답변 생성이 과장됐는가?
  • 모델 설정이 없는가?
  • Codex app-server가 죽었는가?
이 질문에 답하려면 retriever와 answer generator가 분리되어 있어야 합니다.

4. 공개 채팅에서 로그인은 강제하지 않는다

이 기능은 "나를 궁금해하는 사람이 들어와서 질문하는 페이지"입니다. 그래서 로그인 강제는 사용자 흐름과 맞지 않았습니다.처음에는 인증을 붙이는 선택지도 있었지만, 곧 방향을 바꿨습니다.
로그인 강제: 하지 않음
운영 웹 origin 제한: 함
CSP connect-src 정리: 함
실패 UI: 명확히 보여줌
물론 Origin 제한이 완전한 보안 장치는 아닙니다. 서버 간 요청이나 조작된 요청까지 막는 인증 수단은 아닙니다. 하지만 브라우저 기반 공개 채팅에서 "운영 사이트에서 들어온 정상 흐름"만 받도록 줄이는 1차 가드로는 의미가 있습니다.여기서 중요한 것은 보안을 명목으로 사용자 흐름을 망치지 않는 것이었습니다.방문자는 질문을 하러 온 사람이지, 로그인하러 온 사람이 아닙니다.

5. AI를 부르기 전에 질문 범위를 먼저 본다

운영에 붙인 뒤 또 하나의 고민이 생겼습니다.이 채팅은 이력서 질문을 위한 기능입니다. 그런데 사용자는 얼마든지 날씨, 코인 가격, 일반 지식 같은 질문을 보낼 수 있습니다. 이걸 모두 AI까지 보내는 것은 비용과 품질 양쪽에서 좋지 않았습니다.그래서 키워드 기반 scope gate를 먼저 두었습니다.
question
-> keyword scope gate
-> in scope: retrieval + answer generation
-> out of scope: fixed message
처음 문구는 "이력과 관련된 키워드가 없어 답변하지 않았습니다"에 가까웠습니다. 하지만 이 문구는 사용자 입장에서 너무 내부 구현을 드러냅니다. 방문자는 "키워드 판정"을 보러 온 것이 아니라 "이력에 대해 질문"하러 온 사람입니다.그래서 문구를 의도 중심으로 바꿨습니다.
이 질문은 제 이력 범위를 벗어난 것 같아요. 프로젝트, 기술 경험, 업무 성과, 강점처럼 이력과 관련된 내용으로 다시 물어봐 주세요.
작은 문구 변경이지만, 방향은 중요했습니다.
  • 내부 구현: 키워드가 없다
  • 사용자 관점: 이력 범위를 벗어났다
RAG 품질은 검색 알고리즘만의 문제가 아닙니다. 사용자가 왜 답변을 받지 못했는지 이해할 수 있어야 전체 경험이 성립합니다.

6. 실패 UI는 나중에 붙이는 장식이 아니었다

AI 기능은 실패 가능성이 많습니다.
  • 운영 origin이 아니어서 막힘
  • Codex app-server 설정이 없음
  • rate limit
  • API 응답 contract 불일치
  • 네트워크 실패
  • 검색 근거 부족
  • 질문 범위 벗어남
그래서 UI를 만들 때 성공 응답만 보지 않았습니다. 실패 상태를 먼저 나눴고, 사용자가 무엇을 할 수 있는지도 다르게 처리했습니다.예를 들어 origin 차단은 사용자가 재시도한다고 해결되지 않습니다. 반면 네트워크 실패나 일시적인 서비스 오류는 재시도 버튼이 의미가 있습니다.또한 사용자 메시지는 일반 입력 박스처럼 보이면 채팅 흐름이 깨졌습니다. 마지막에는 사용자 질문을 오른쪽 말풍선으로 정리하고, 입력부도 큰 textarea가 아니라 일반적인 채팅 composer처럼 바꿨습니다.이 작업은 기능의 본질과 직접 연결됩니다.사용자는 "폼을 제출했다"가 아니라 "대화를 시작했다"고 느껴야 합니다.

7. 최종 구조

현재 구조를 단순화하면 다음과 같습니다.
Web
  /resume/question
  - public chat UI
  - suggested questions
  - failure states
  - source citations

API
  POST /resume-rag/chat
  - public origin guard
  - keyword scope gate
  - DB text retriever
  - Codex app-server chat provider
  - grounded answer response

Data
  resume_source_items
  - runtime retrieval source
  - imported from raw resume/career docs

Optional Future
  resume_vector_chunks
  - embedding/vector search path
  - not required for current production chat
이 구조에서 각 계층의 책임은 꽤 명확합니다.
  • Web은 DB를 모른다.
  • API는 파일을 직접 읽지 않는다.
  • Retriever는 답변을 생성하지 않는다.
  • Chat provider는 검색하지 않는다.
  • Codex는 근거 밖의 답변을 만들지 않아야 한다.
  • Vector index는 현재 운영 필수가 아니라 확장 옵션이다.
이렇게 나눠두니 문제를 작게 볼 수 있었습니다.

8. 배포하면서 확인한 것들

운영에 반영하면서는 로컬 테스트만으로 끝내지 않았습니다.
  • API unit test
  • web typecheck / lint / build
  • smoke E2E
  • GitHub self-hosted runner 기반 API 배포
  • 운영 API 직접 호출
  • 운영 웹에서 실제 질문 전송
  • 실패 UI 캡처 확인
특히 공개 채팅은 "로컬에서는 된다"가 충분하지 않습니다. 운영 origin, CSP, API CORS, 서버 환경 변수, Codex app-server 연결이 모두 맞아야 실제 사용자가 질문을 보낼 수 있습니다.이번 작업에서 운영 테스트를 계속 같이 한 이유도 여기에 있습니다.

9. 남은 고민

아직 끝난 구조는 아닙니다. 오히려 이제 운영 가능한 첫 형태에 가깝습니다.남은 고민은 몇 가지가 있습니다.
out-of-scope와 no-evidence 분리
현재는 grounded=false라는 값이 여러 의미를 가질 수 있습니다. 검색 근거가 부족한 경우와 질문 범위가 벗어난 경우는 UI에서 다르게 보여주는 것이 맞습니다.다음에는 API 응답에 reason 같은 필드를 추가해 상태를 더 명확히 나눌 수 있습니다.
벡터 검색 재도입 시점
지금은 텍스트/키워드 검색으로 충분하지만, 질문이 더 추상화되면 벡터 검색이 필요해질 수 있습니다.예를 들면 이런 질문입니다.
복잡한 도메인을 제품화한 경험이 있어?
이 질문은 특정 회사명이나 기술명보다 문맥 유사도가 중요합니다. 이런 유형의 질문이 많아지면 resume_vector_chunks를 다시 활성화할 이유가 생깁니다.
모델 교체 가능성
현재는 Codex app-server를 사용하지만, 구조상 다른 OpenAI-compatible provider로 바꿀 여지를 남겨두었습니다. 중요한 것은 어떤 모델을 쓰느냐보다 모델 교체가 전체 구조를 흔들지 않게 하는 것입니다.

마치며

이번 작업에서 가장 크게 느낀 것은 RAG라는 이름에 끌려 구조를 과하게 만들 필요는 없다는 점입니다.벡터 DB, embedding, LLM은 모두 강력한 도구입니다. 하지만 먼저 정해야 하는 것은 도구가 아니라 경계입니다.
  • 어떤 데이터가 진실인가?
  • 검색은 어디서 책임지는가?
  • 자연어 생성은 어디까지 허용하는가?
  • 사용자는 실패를 어떻게 이해하는가?
  • 운영 환경에서는 무엇이 실제로 검증되어야 하는가?
이 질문에 답하고 나니 구현은 오히려 단순해졌습니다.개인 이력서 RAG 채팅은 화려한 AI 기능이라기보다, 내 이력 데이터를 제품처럼 다루는 작은 시스템에 가까웠습니다. 그리고 그 시스템에서 가장 중요한 일은 AI를 붙이는 것이 아니라, AI가 움직일 수 있는 안전한 범위를 정하는 것이었습니다.