-
[2026 ABC 프로젝트 멘토링 8기] 프로젝트 4주차ABC 프로젝트 2026. 6. 27. 12:26ABC 프로젝트 멘토링 8기
프로젝트 4주 차 — 전체 기능 통합, 라즈베리파이 온프레미스 서버, 그리고 커피챗 멘토링
00들어가며
4주 차는 각자 개발했던 부분들을 통합하고, 프로젝트의 완성도를 높히는 주였다. 1~3주 차 동안 각자 만들어 온 RAG, 이미지 라벨링, 서버, 프론트엔드, 그리고 여러 기능 데모들이 따로따로 돌아가고 있었는데, 이번 주에는 이것들을 하나의 서비스로 합치는 작업에 집중했다. 개발 자체는 거의 마무리 단계에 접어들었다.
이번 주의 큰 줄기는 세 가지였다. 첫째는 모든 기능을 합치는 통합 작업, 둘째는 클라우드도 교내 동아리 서버도 아닌 직접 구축한 라즈베리파이 온프레미스 서버, 셋째는 멘토님과 멘토님 회사 동료분들까지 함께한 커피챗 겸 멘토링이었다.
이번 주 태스크: 전체 기능 통합 → 라즈베리파이 온프레미스 서버 구축 → 커피챗 & 멘토링
01전체 기능 통합 — 흩어진 조각을 하나의 서비스로
우리 프로젝트(
llm-job-support)는 처음부터 기능별 프로토타입을 따로 빠르게 만들어 보는 바이브 코딩 방식으로 진행됐다. 덕분에 각 기능은 빨리 나왔지만, 데모들이 제각기 독립적으로 돌아가다 보니 하나의 목표를 달성하는 프로덕트일 뿐이었다. 이번 주의 목표는 이 조각들을 공용 백엔드 + 단일 프론트엔드 아래로 모으는 것이었다.통합 아키텍처
최종적으로 잡은 구조는 이렇다. 프론트엔드(
web/)는 사용자의 자연어 질의와 이미지 입력을 받아 백엔드로 넘기고, 백엔드(backend/, FastAPI)가 입력 성격에 따라 처리 경로를 분기한다. 텍스트·문서 질의는 Hybrid RAG(벡터 검색 + BM25 키워드 검색)로, 이미지 입력은 VLM 기반 이미지 이해·라벨링 경로로 보낸다. 여기에 공공데이터포털(data.go.kr) 연계와 Anthropic SDK 기반 AI Agent(MCP 도구 호출)가 붙어, 질의 후 이미지 이해/검색/요약/보고서 생성’이 하나의 흐름으로 이어지도록 했다.FastAPI (backend)web 프론트엔드Hybrid RAG · ChromaDB/FAISS + BM25 VLM · SAM / YOLOeAnthropic SDK · MCPmemory MCPdata.go.kruv · ruff
웹 내부에서 데이터 흐름도를 블록 다이어그램으로 표현하면 다음과 같다.

현재 메인 대시보드 UX/UI는 다음과 같이 잡혀있다.
통합하며 만난 이슈 3가지
합치는 과정은 단순 복사·붙여넣기가 아니었다. 따로 놀던 코드를 한곳에 모으자 경계면에서 문제가 줄줄이 나왔다. 그중 기억에 남는 세 가지를, 문제 → 선택지 → 결정 이유 → 해결 순으로 정리했다.
1기능별 데모(단일 파일)들이 하나의 백엔드로 안 합쳐지는 문제각 기능은 빠른 시연을 위해 독립 실행형 데모(한 파일에 UI·로직·상태가 다 들어간 형태)로 만들어져 있었다. 그러다 보니 각자 자기 포트와 상태를 들고 떠서, 그대로는 한 서비스의 메뉴로 묶이지 않았다. 데모를 그냥 끌어다 붙이면 기능마다 별도 프로세스가 떠 버리는 구조였다.
A안 · 기존 데모를 그대로 임베드작업량은 적지만, 기능마다 독립 앱이 떠서 상태·세션 공유가 안 되고 서버 자원이 낭비된다.
B안 · FastAPI 라우터로 재정리 채택로직만 떼어내 공용 백엔드의 엔드포인트로 재배치. 프론트는 화면만 담당. 상태·인증·AI 호출을 한곳에서 관리.
왜 B안인가데모를 그대로 붙이는 A안은 당장은 빨라도, 기능이 늘수록 프로세스가 늘고 상태가 흩어져 통합 서비스로서의 일관성을 잃는다. 라즈베리파이 한 대에서 돌려야 하는 우리 상황에선 자원 낭비도 문제였다. 그래서 데모에서 핵심 로직만 함수로 분리해 FastAPI 엔드포인트로 옮기고, 프론트엔드는 그 API를 호출해 화면을 그리는 역할로 한정했다. 결과적으로 모든 기능이 같은 백엔드 위에서 상태를 공유하게 되어, 메뉴만 바꿔도 끊김 없이 이어지는 단일 서비스가 됐다.
2RAG 검색 결과와 이미지 라벨링 결과를 한 응답으로 합치는 문제통합 후에는 ‘이미지를 보고, 관련 자료까지 찾아 답하는’ 복합 질의가 가능해야 했다. 그런데 Hybrid RAG의 검색 결과(텍스트 근거)와 VLM의 이미지 분석 결과(객체·영역 정보)가 서로 다른 형태로 나와서, 이 둘을 하나의 답변으로 자연스럽게 묶는 지점에서 충돌이 났다. 각 경로가 자기 컨텍스트만 들고 있어, 답변 생성 단계에서 한쪽 정보가 통째로 누락되기도 했다.
A안 · 두 결과를 그대로 이어 붙이기구현은 쉽지만 컨텍스트가 길어지고, 이미지 정보와 텍스트 근거가 따로 노는 답변이 나온다.
B안 · 공용 컨텍스트 스키마로 정규화 채택검색 근거와 이미지 분석을 같은 구조(근거·출처·신뢰도)로 정규화한 뒤 한 번에 LLM에 전달.
왜 B안인가단순히 이어 붙이는 A안은 컨텍스트만 비대해지고, 모델이 어느 정보를 근거로 답했는지 추적도 어려워졌다. 그래서 검색 결과든 이미지 분석이든 같은 컨텍스트 스키마로 한 번 정규화한 뒤 LLM에 넘기는 방식으로 바꿨다. 텍스트 근거와 이미지 영역 정보가 같은 틀에 담기니, 답변이 두 출처를 함께 인용하면서도 컨텍스트는 오히려 짧아졌다. 3주 차에 RAG에서 배운 ‘근거를 다루는 방식이 품질을 만든다’는 교훈이 통합 단계에서도 그대로 통했다.
3RAG 검색에서 정작 중요한 청크가 누락되는 문제통합 후 RAG 검색을 돌려보니, 질문과 의미는 분명히 같은데 표현이 조금 다른 문서가 검색 결과에서 통째로 빠지는 일이 잦았다. 원인을 추적해 보니 두 군데가 문제였다. 하나는 청크를 너무 크게 자른 것, 다른 하나는 검색을 벡터(의미) 검색 하나에만 의존한 것이었다. 청크가 크면 한 조각에 여러 주제가 섞여 핵심이 흐려졌고, 벡터 검색만으로는 같은 의미라도 단어가 다르면 점수가 떨어져 후보에서 밀려났다.
코드를 읽기 좋게 주석도 하나씩 달아두었다. :)
backend/rag/retriever.py — 문제가 된 코드# 청크가 너무 크고(겹침 없음), 벡터 검색 단독 + top_k가 작음 splitter = RecursiveCharacterTextSplitter( chunk_size=1500, # 한 조각에 여러 주제가 섞임 chunk_overlap=0, # 경계에서 문맥이 끊김 ) chunks = splitter.split_documents(docs) vectorstore = Chroma.from_documents(chunks, embedding=embeddings) retriever = vectorstore.as_retriever( search_kwargs={"k": 3} # 처음부터 좁게 3개만 → 놓친 문서 복구 불가 ) docs = retriever.invoke(query) # 의미는 같아도 표현 다르면 누락 answer = llm.invoke(build_prompt(query, docs))A안 · 청크 크기만 줄이기splitter 값만 손보는 임시 처방. 단어가 다른 문서가 빠지는 근본 문제(검색 방식)는 그대로 남는다.
B안 · 작은 청크 + 하이브리드 검색 + 리랭킹 채택청크를 의미 단위로 줄이고 겹침을 두며, 벡터 + BM25를 합치고 후보를 넉넉히 뽑은 뒤 리랭킹으로 추린다.
backend/rag/retriever.py — 수정한 코드# (1) 청크를 의미 단위로 줄이고 겹침을 둠 splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 한 조각 = 하나의 의미 단위에 가깝게 chunk_overlap=80, # 경계에서 문맥이 끊기지 않도록 겹침 separators=["\n\n", "\n", ". ", " "], # 문단·문장 경계 우선 ) chunks = splitter.split_documents(docs) # (2) 벡터(의미) + BM25(키워드) 하이브리드 검색 vector_ret = Chroma.from_documents(chunks, embedding=embeddings).as_retriever( search_kwargs={"k": 8} # 후보를 넉넉히 8개 ) bm25_ret = BM25Retriever.from_documents(chunks) bm25_ret.k = 8 hybrid = EnsembleRetriever( retrievers=[vector_ret, bm25_ret], weights=[0.5, 0.5], # 의미·키워드 균형 ) # (3) 넉넉히 뽑은 뒤 리랭킹으로 상위 4개만 근거로 사용 candidates = hybrid.invoke(query) docs = rerank(query, candidates)[:4] answer = llm.invoke(build_prompt(query, docs))왜 B안인가청크 크기만 줄이는 A안은 한 가지 증상만 가리는 임시 처방이었다. ‘표현이 다르면 누락된다’는 진짜 원인은 검색 방식에 있었기 때문이다. 그래서 ① 청크를 의미 단위로 줄이되 경계에서 맥락이 끊기지 않게 겹침(overlap)을 두고, ② 의미 검색(벡터)과 키워드 검색(BM25)을 EnsembleRetriever로 합쳐 단어가 달라도 누락되지 않게 했다. ③ 처음부터 좁게 뽑지 않고 후보를 넉넉히(8개) 가져온 다음 리랭킹으로 관련도 높은 4개만 답변 근거로 사용했다. 그 결과 같은 의미의 문서가 표현 차이로 빠지는 문제가 크게 줄었고, 답변 근거의 정확도도 눈에 띄게 올라갔다. 3주 차에 RAG에서 배운 ‘청크 설계와 검색 정확도가 곧 품질’이라는 교훈을 통합 단계에서 코드로 옮긴 셈이다.
전체 진행 상황과 남은 작업
통합이 마무리되면서 핵심 기능들은 하나의 서비스 위에서 대부분 동작하는 상태가 됐다. 자연어 질의, 이미지 이해·라벨링, Hybrid RAG 검색, 공공데이터 연계, 보고서 생성까지 한 흐름으로 이어진다. 개발은 거의 끝나가고, 남은 일은 크게 세 가지다.
- 대규모 이미지 라벨링 기능 보완 소량 데모 수준을 넘어, 많은 이미지를 한 번에 처리하는 배치 라벨링(SAM/YOLOe 활용)으로 확장. 처리량과 안정성을 끌어올리는 게 핵심.
- 각종 기능 다듬기 & 버그 제거 통합 과정에서 드러난 경계면 버그와 예외 처리를 정리해 시연에서 끊김이 없도록 안정화.
- AI API 키 교체 검증용 Gemini에서 Claude·Codex 정식 키로 전환. 어댑터 레이어 덕분에 설정 교체 중심으로 진행 예정.
02라즈베리파이 온프레미스 서버 구축
3주 차에 서버 선택지를 세 가지(교내 Xquare 온프레미스 / 클라우드 / 로컬·라즈베리파이) 놓고 고민했었다. 온라인 클라우드 서비스는 토큰 비용, 교내 데브옵스 동아리 친구들의 서버는 외부 시연 제약이 걸려, 결국 직접 구축한 라즈베리파이 온프레미스 서버로 방향을 잡았다. 그리고 이번 주, 감사하게도 멘토님께서 라즈베리파이 서버를 지원해 주신 덕분에 실제 구축까지 진행할 수 있었다.
nginx + HTTP 80, 그리고 외부 접속
서버에는 80번 HTTP 포트를 열고 nginx를 붙여 페이지를 띄웠다. 덕분에 교내망에 갇히지 않고 외부에서도 접속이 가능해졌고, 주소를 어디서나 입력하여 들어오면 실제 서비스 화면을 볼 수 있다. 주소는 보안상의 이유로 공개하지 않는다. 이렇게 3주 차에 가장 걸렸던 외부 시연 불가 문제가 이 구성으로 해결된 셈이다.
배포는 컨테이너로 통일했다.
.env와 Docker 구성을 서버에 올려, 환경변수(API 키 등)와 실행 환경을 한 번에 관리하도록 했다. 로컬에서 만든 환경을 그대로 서버로 옮길 수 있어, 통합한 서비스를 바로 배포·시연할 수 있는 토대가 됐다.
실제 서버가 잘 살아서 돌아가고 있다.
다음 과제 — 실시간 대시보드와 분산 환경의 이벤트 브로드캐스트
AI 기능을 서버에 올려 쓰는 건 당연한 수순이고, 여기에 더해 실시간 이벤트 대시보드도 서버에 올려야 한다. 그런데 이 지점에서 생각보다 까다로운 개념을 하나 마주했다.
실시간 대시보드는 서버에서 발생한 이벤트(처리 현황, 알람 등)를 클라이언트로 즉시 밀어줘야 한다. 문제는 서버를 안정적으로 돌리려고 여러 워커(프로세스)로 확장하는 순간 발생한다. 이벤트 구독 상태나 연결 정보를 각 프로세스의 메모리에만 들고 있으면, A 프로세스가 받은 이벤트를 B 프로세스에 붙은 사용자에게는 전달하지 못한다. 프로세스가 늘거나 재시작될 때마다 상태가 흩어지고 동기화가 깨지는 것이다. 즉, 여러 프로세스에 흩어진 실시간 연결에 동일한 이벤트를 일관되게 broadcast하는 문제가 핵심이다.
이를 풀기 위한 개념이 Pub/Sub(발행-구독)이다. 이벤트를 특정 채널에 데이터를 발행하면, 그 채널을 구독하는 모든 워커가 동시에 같은 이벤트를 받아 각자 붙어 있는 클라이언트에게 전달하는 구조다. 그 중간 허브로 Redis를 도입할 계획이다. Redis의 Pub/Sub로 모든 워커가 같은 이벤트 채널을 구독하게 하고, 더불어 Redis를 캐시·세션 저장소로도 활용해 실시간 대시보드가 프로세스 수와 무관하게 일관되게 동작하도록 만들 생각이다. 다음 주에 이 구조를 서버에 올리는 것이 목표다.
03커피챗 & 멘토링 — AI에 진심인 분들과의 대화
이번 주에는 우리 프로젝트 멘토님과 멘토님 회사 동료 두 분까지 함께 커피챗 겸 멘토링 자리를 가졌다. 두 분 모두 AI에 관심이 정말 많은 분들이라, 가벼운 커피챗으로 시작한 자리가 실제 실무에서 AI를 어떻게 굴리는지에 대한 세미나처럼 흘러갔다. 들은 내용 중 인상 깊었던 것들을 정리했다.

실제로 팀원들과 멘토님들과 회의를 진행하였다.
AI 개발 도구와 MCP 생태계
대화의 큰 줄기는 AI를 단발성 채팅으로 쓰지 않고, 외부 도구를 직접 다루는 에이전트로 확장한다는 것이었다. 그 핵심에 MCP가 있었다.
- Claude 플러그인 & Claude Connect — Claude Code에 다양한 기능을 붙이는 플러그인 생태계, 그리고 Docker로 MCP를 간단하게 구성해 외부 도구를 연결하는 방식을 소개받았다.
- Context Mode — AI가 도구(tool)를 호출하면 부가적인 출력이 잔뜩 따라와 컨텍스트가 급격히 커지고, 이게 곧 토큰 비용 문제로 이어진다. Context Mode는 이런 긴 출력을 압축·요약해 핵심만 전달함으로써 비용을 줄여 준다.
- Memory Palace — AI가 세션이 바뀌면 이전 맥락을 잃는데, 이를 디스크 기반 장기 기억으로 보완한다. 서버 정보나 작업 이력을 저장해 새 세션에서도 맥락을 이어간다. (우리 프로젝트도 memory MCP로 비슷한 걸 쓰고 있어 특히 와닿았다.)
- OpenCode & CCS — 여러 AI를 붙여 쓰는 OpenCode, 그리고 여러 계정·프로바이더를 묶어 라우팅하는 CCS 이야기가 나왔다. CCS는 쉽게 말하면 프록시(proxy) 같은 개념으로, 사용량 한계에 대응하는 용도다.
- Notion · Obsidian을 RAG로 — 전통적인 벡터 DB가 아니라 Notion이나 Obsidian 같은 문서 도구를 RAG의 지식 소스처럼 사용하는 방식. 우리가 만든 RAG와 발상이 맞닿아 있어 흥미로웠다.
- 바이브 코딩의 사이드 이펙트 — 바이브 코딩으로 만든 코드는 중복이 좀 있을 수 있어도, 모듈이 강하게 분리돼 있어 한 부분을 고쳐도 다른 곳에 사이드 이펙트가 비교적 적다는 평가가 인상적이었다.
실제로 우리가 진행하는 프로젝트에는 다양한 MCP & Skills가 구성되어있고, 이를 활용하여 개발하고 있다.
또한 RAG를 정갈하게 짜여진 코드로 생성된 DB에서만 동작한다고 한정지어 생각했었는데, Notion이나 Obsidian으로 DB역할을 충분히 할 수 있다는 점이 신기했다.
가장 중요하게 와닿은 것 — "큰 프로젝트는 Ground Truth부터"
여러 이야기 중 내게 가장 크게 남은 건 AI에게 큰 프로젝트를 맡길 때의 방법이었다. 큰 작업을 한 번에 던지면 AI가 ‘체한다’는 표현이 나왔는데, 정확히 그 느낌을 통합 작업 내내 겪었기 때문에 단번에 공감이 가는 말이었다.
핵심은 Ground Truth(기준이 되는 표준 문서)를 먼저 세우는 것이다. 요구사항을 정리한 이 문서를 진짜 기준으로 삼아 명세서를 쓰고, 구현 중에도 계속 업데이트하면서 AI가 따라올 기준점을 만들어 준다. 도구(Notion, Markdown 등)는 무엇이든 상관없고, 계속 관리되는지가 핵심이다.
정리하면, AI를 잘 쓰는 비결은 모든 걸 즉흥적으로 맡기는 게 아니라 요구사항·진행상황·결정사항을 문서로 남겨 기준점을 만들어 주는 것이라는 결론이었다. 다음 단계에서 남은 기능을 다듬을 때, 우리 프로젝트의 요구사항 문서부터 Ground Truth로 다시 정돈해야겠다는 생각이 들었다.
4주 차를 마치며이번 주는 ‘만드는’ 단계에서 ‘합치고 올리는’ 단계로 넘어간 한 주였다. 따로 돌던 기능들을 하나의 서비스로 묶으면서, 통합은 단순히 코드를 모으는 게 아니라 경계면을 설계하는 일이라는 걸 배웠다. 호출 레이어를 추상화해 둔 과거의 선택이 모델 교체에서 그대로 보답으로 돌아온 것도 인상 깊었다.
또 라즈베리파이 온프레미스 서버를 직접 세워 외부 시연의 길을 열었고, 멘토님과 동료분들과의 커피챗을 통해 실무에서 AI를 굴리는 관점과 Ground Truth부터 세워야 한다는 원칙을 얻었다.
다음 주부터는 대규모 이미지 라벨링 보완과 버그 안정화, 정식 API 키 전환, 그리고 Redis 기반 실시간 대시보드까지 올리며 프로젝트를 마무리 단계로 끌고 갈 예정이다.
'ABC 프로젝트' 카테고리의 다른 글
[2026 ABC 프로젝트 멘토링 8기] 프로젝트 6주차 (1) 2026.07.12 [2026 ABC 프로젝트 멘토링 8기] 프로젝트 5주차 (0) 2026.07.05 [2026 ABC 프로젝트 멘토링 8기] 프로젝트 3주차 (0) 2026.06.20 [2026 ABC 프로젝트 멘토링 8기] 프로젝트 2주차 (0) 2026.06.14 [2026 ABC 프로젝트 멘토링 8기] 프로젝트 1주차 (0) 2026.06.08