LLM 서비스나 데이터베이스 중심의 애플리케이션을 개발할 때 성능 최적화의 핵심은 바로 비동기 프로그래밍입니다. 특히 사용자가 늘어날수록 서버가 버벅거리지 않게 하려면 Asyncio에 대한 이해가 필수적입니다.

오늘은 파이썬의 Asyncio를 요리에 비유하여 기초부터 심화 패턴까지 알기 쉽게 정리해 보겠습니다.

1. 동기(Sync) vs 비동기(Async): 라면 끓이기 비유

가장 먼저 동기와 비동기의 차이를 명확히 이해해야 합니다. 라면을 끓이는 과정에 비유해 보겠습니다.

동기(Sync) 방식은 물을 올리고 물이 끓을 때까지 가스불 앞에서 아무것도 하지 않고 기다리는 것과 같습니다. 물이 다 끓어야 비로소 파를 썰기 시작합니다. 매우 비효율적입니다.

비동기(Async) 방식은 물을 올려두고, 물이 끓는 시간 동안 놀지 않고 파를 썰거나 김치를 꺼내는 것입니다. 물이 다 끓으면 그때 면을 넣습니다. 시간을 훨씬 효율적으로 사용할 수 있습니다.

파이썬의 async와 await는 바로 기다리는 시간 동안 다른 작업을 할 수 있게 해주는 문법입니다.

2. 기초 문법: async, await, coroutine

파이썬에서 비동기 함수를 정의할 때는 def 앞에 async를 붙입니다. 이를 코루틴(Coroutine)이라고 부릅니다. 그리고 await는 여기서 잠시 기다릴 테니 그동안 다른 일을 하라고 제어권을 넘겨주는 명령어입니다.

Python
 
import asyncio
import time

# 일반 함수 (동기)
def sync_cooking():
    print("물 올리기 (3초 대기)")
    time.sleep(3) 
    print("물 끓음!")

# 비동기 함수 (코루틴)
async def async_cooking():
    print("물 올리기 (3초 대기)")
    # 3초를 기다리지만 컴퓨터가 멈추지 않고 다른 일을 할 수 있음
    await asyncio.sleep(3) 
    print("물 끓음!")

핵심은 time.sleep은 컴퓨터 전체를 멈추게 하지만, asyncio.sleep은 제어권을 다른 작업에게 양보한다는 점입니다.

3. gather: 멀티 태스킹의 정석

여러 작업을 동시에 처리해야 할 때 asyncio.gather를 사용합니다. 예를 들어 식단 분석과 수면 분석을 순서대로 하면 시간이 오래 걸리지만, 동시에 실행하면 가장 오래 걸리는 작업 시간만큼만 소요됩니다.

Python
 
import asyncio

async def chop_onion():
    print("양파 썰기 시작 (2초)")
    await asyncio.sleep(2)
    print("양파 완료!")
    return "양파"

async def boil_water():
    print("물 끓이기 시작 (3초)")
    await asyncio.sleep(3)
    print("물 완료!")
    return "물"

async def main():
    print("--- 요리 시작 ---")
    # 두 함수를 동시에 실행
    results = await asyncio.gather(chop_onion(), boil_water())
    print(f"--- 요리 끝! 결과물: {results} ---")

if __name__ == "__main__":
    asyncio.run(main())

이 코드를 실행하면 2초와 3초가 더해져 5초가 걸리는 것이 아니라, 가장 긴 시간인 3초 만에 모든 작업이 완료됩니다.

4. asyncpg: 데이터베이스도 비동기로

파이썬의 기본 DB 라이브러리는 동기식이라 DB 응답을 기다리는 동안 서버가 멈춥니다. 이를 해결하기 위해 비동기 전용 드라이버인 asyncpg를 사용해야 합니다.

Python
 
import asyncpg

class UserRepository:
    def __init__(self, dsn):
        self.dsn = dsn
        self.pool = None

    async def connect(self):
        self.pool = await asyncpg.create_pool(self.dsn)

    async def get_user(self, user_id):
        async with self.pool.acquire() as connection:
            row = await connection.fetchrow(
                "SELECT * FROM users WHERE id = $1", user_id
            )
            return row

    async def save_user(self, name):
        async with self.pool.acquire() as conn:
            async with conn.transaction():
                await conn.execute("INSERT INTO users (name) VALUES ($1)", name)

핵심은 with 대신 async with를 사용하고, execute 대신 await execute를 사용한다는 점입니다.

5. AsyncGenerator: 스트리밍 응답 처리

LLM 서비스에서 답변이 한 글자씩 실시간으로 나오는 타다닥 효과를 구현하려면 비동기 제너레이터를 사용해야 합니다. async def와 yield를 조합하여 구현합니다.

Python
 
async def llm_stream_simulator():
    sentence = "안녕하세요"
    for char in sentence:
        await asyncio.sleep(0.5)
        yield char 

async def main():
    print("답변: ", end="")
    async for char in llm_stream_simulator():
        print(char, end="", flush=True)

if __name__ == "__main__":
    asyncio.run(main())

일반적인 for문 대신 async for를 사용하여 데이터가 생성되는 즉시 받아볼 수 있습니다.

6. 동시성 패턴: Semaphore와 Lock

너무 많은 요청이 한꺼번에 몰리는 것을 방지하기 위해 안전장치가 필요합니다.

 

Semaphore(세마포어)는 동시 실행 가능한 작업 수를 제한합니다. 예를 들어 API 요청을 동시에 3개까지만 보내도록 제한할 수 있습니다.

Lock(락)은 여러 작업이 동시에 하나의 변수를 수정할 때 데이터가 꼬이지 않도록 잠그는 역할을 합니다.

Python
 
import asyncio

sem = asyncio.Semaphore(3)

async def access_api(i):
    async with sem: 
        print(f"User {i} 접속 중...")
        await asyncio.sleep(2)
        print(f"User {i} 완료!")

async def main():
    tasks = [access_api(i) for i in range(10)]
    await asyncio.gather(*tasks)

이렇게 하면 10개의 요청이 한 번에 실행되지 않고, 3개씩 끊어서 안정적으로 실행됩니다.


통합 예제: 비동기의 흐름 완벽 이해하기

마지막으로 위에서 배운 개념을 종합하여, await가 기다리는 동안 실제로 다른 작업이 어떻게 끼어드는지 보여주는 예제입니다.

오래 걸리는 물 끓이기(Task)와 그 사이에 쉴 새 없이 진행되는 양파 썰기(Streaming)를 동시에 실행해 보겠습니다.

Python
 
import asyncio

# 3초 걸리는 무거운 작업
async def boil_water():
    print("물: 불 올렸다! (3초 대기 시작)")
    # 여기서 await를 만나면 CPU 제어권을 놓아줍니다.
    await asyncio.sleep(3)  
    print("물: 3초 지났다! 물 다 끓음!")
    return "뜨거운 물"

# 실시간으로 데이터를 주는 스트리밍 작업
async def chop_onion():
    for i in range(1, 4):
        await asyncio.sleep(0.5) 
        yield f"양파 {i}개 썰음" 

async def main():
    print("--- 주방 가동 ---")
    
    # 1. 물 끓이기 작업을 예약 (실행은 시작하되 완료를 기다리지 않고 넘어감)
    task1 = asyncio.create_task(boil_water())
    
    # 2. 물이 끓는 3초 동안, 멍하니 있지 않고 양파를 썹니다 (스트리밍 처리)
    async for onion in chop_onion():
        print(f"   -> {onion} (바로 받음!)")
        
    # 3. 양파를 다 썰고 나서 물 끓이기 작업이 끝났는지 확인하고 결과를 받습니다
    result = await task1
    print(f"--- 요리 끝: {result} ---")

if __name__ == "__main__":
    asyncio.run(main())

실행 결과:

Plaintext
 
--- 주방 가동 ---
물: 불 올렸다! (3초 대기 시작)
   -> 양파 1개 썰음
   -> 양파 2개 썰음
   -> 양파 3개 썰음
물: 3초 지났다! 물 다 끓음!
--- 요리 끝: 뜨거운 물 ---

결과를 보면 물 끓이기(boil_water)가 시작된 후, 3초가 지나기 전에 양파 썰기 작업들이 중간중간 실행되는 것을 확인할 수 있습니다. 이것이 바로 비동기 프로그래밍이 시스템 자원을 효율적으로 사용하는 방식입니다.

핵심 요약

  1. I/O 작업(DB, API)에는 무조건 async/await를 사용합니다.
  2. 여러 작업을 동시에 할 때는 asyncio.gather를 활용합니다.
  3. 스트리밍 데이터는 async for로 처리합니다.
  4. 비동기는 작업을 건너뛰는 게 아니라, 기다리는 시간에 다른 일을 하는 것입니다.

 

🧐 잠깐, 파이썬은 멀티스레드가 안 된다던데?

오늘 비동기 프로그래밍을 배우면서 혹시 이런 의문이 들지 않으셨나요?

"어? 파이썬은 GIL(Global Interpreter Lock) 때문에 진정한 멀티스레딩이 안 된다고 들었는데, 어떻게 동시에 여러 일을 처리하는 거지?"

정말 날카로운 지적입니다. 결론부터 말씀드리면 파이썬의 비동기는 하나의 스레드 안에서 작업 순서를 기가 막히게 교체하는 기술이지, 물리적으로 여러 개의 뇌(CPU)를 동시에 쓰는 것은 아닙니다.

상황 : I/O 바운드 (기다리는 일) - ★우리가 하는 것★

  • 작업: DB 조회, API 요청, 파일 읽기/쓰기.
  • Python: A 스레드가 "DB에 쿼리 날리고 올게" 하고 마이크(GIL)를 내려놓고 나감. 그 사이에 B 스레드가 마이크 잡고 일함.
  • 결과: CPU는 쉬지 않고 계속 일함. 

Asyncio는 이 "기다리는 시간(I/O)"을 극단적으로 효율화한 거야. 스레드를 운영체제 레벨에서 여러 개 만드는 비용조차 아까워서, 단 하나의 스레드 안에서 작업만 휙휙 바꾸는 기술입니다.

 

그렇다면 파이썬은 영원히 한 번에 하나밖에 처리를 못 하는 걸까요? CPU를 100% 활용해서 복잡한 연산을 하려면 어떻게 해야 할까요?

다음 포스팅에서는 파이썬 개발자라면 반드시 알아야 할 GIL(글로벌 인터프리터 락)의 정체와, 이를 극복하고 진정한 병렬 처리를 가능하게 하는 Multiprocessing(멀티 프로세싱)에 대해 다뤄보겠습니다.

LangGraph는 최근 LLM 애플리케이션 개발에서 가장 중요한 프레임워크로 자리 잡고 있습니다. 복잡해 보일 수 있는 개념들을 이해하기 쉬운 예시와 함께 정리하여, 기초부터 고급 멀티 에이전트 패턴까지의 로드맵을 공부하려 합니다.

1. LangGraph의 기초: StateGraph와 구조

LangGraph의 핵심은 상태(State)를 정의하고, 노드(Node)들이 이 상태를 주고받으며 작업을 수행하는 것입니다. 그래프는 크게 상태, 노드, 엣지(Edge)로 구성됩니다.

 

이를 공유 화이트보드에 비유할 수 있습니다. 회의실(Graph)에 여러 사람(Node)이 모여 있고, 화이트보드(State)에 적힌 내용을 보고 각자 할 일을 처리한 뒤 결과를 다시 화이트보드에 적는 방식입니다.

  • 개념: 상태는 모든 노드가 공유하는 기억 공간이며, 노드는 실제 작업을 수행하는 일꾼입니다.
  • 예시 상황: 챗봇이 사용자의 질문을 받으면, 먼저 처리 노드가 질문 내용을 분석해 상태에 저장하고, 답변 생성 노드가 그 내용을 보고 답변을 작성하여 다시 상태에 업데이트합니다.
  • State (상태): 애플리케이션의 현재 상황(대화 기록, 변수 등)을 저장하는 공유 메모리입니다.
  • Node (노드): 실제 로직(LLM 호출, 함수 실행)을 수행하고 상태를 업데이트하는 단위입니다.
  • Edge (엣지): 노드 간의 연결 경로를 정의합니다.
Python
 
from typing import TypedDict
from langgraph.graph import StateGraph, END

class GraphState(TypedDict):
    question: str
    answer: str

def chatbot_node(state: GraphState):
    return {"answer": "응답 메시지입니다."}

workflow = StateGraph(GraphState)
workflow.add_node("chatbot", chatbot_node)
workflow.set_entry_point("chatbot")
workflow.add_edge("chatbot", END)

app = workflow.compile()

2. 조건부 라우팅 (Conditional Routing)

모든 대화가 똑같은 순서로 진행되지는 않습니다. 마치 회사의 안내 데스크처럼, 방문자의 목적에 따라 다른 부서로 안내해 주는 역할이 필요합니다.

  • 개념: 사용자의 입력이나 현재 상태에 따라 다음에 실행할 노드를 동적으로 결정하는 기술입니다.
  • 예시 상황: 사용자가 "오늘 날씨 어때?"라고 물으면 검색 도구 노드로 보내고, "안녕, 반가워"라고 인사하면 일반 대화 노드로 보내는 방식입니다.
Python
 
def route_decision(state: GraphState):
    if "검색" in state["question"]:
        return "search_tool"
    else:
        return "chatbot"

workflow.add_conditional_edges(
    "router",
    route_decision,
    {
        "search_tool": "search_tool",
        "chatbot": "chatbot"
    }
)

3. 메모리 관리와 영속성 (Reducer & Checkpointer)

AI가 이전 대화를 기억하려면 뇌에 해당하는 메모리 기능이 필요합니다. 단순히 정보를 덮어쓰는 것이 아니라, 일기장처럼 내용을 계속 이어 적어야 합니다. 또한 게임의 세이브 기능처럼 프로그램이 종료되어도 기억을 유지해야 합니다.

  • 개념: Reducer는 새로운 메시지를 기존 대화 목록에 추가(Append)하는 역할을 하며, Checkpointer는 이 대화 기록을 데이터베이스에 영구적으로 저장합니다.
  • 예시 상황: 사용자가 어제 "나 내일 소개팅해"라고 말하고 앱을 껐다가 오늘 다시 켰을 때, AI가 "어제 말한 소개팅은 잘 했어?"라고 먼저 물어볼 수 있게 됩니다.

 

  • Reducer: 상태를 덮어쓰지 않고 기존 리스트에 추가하는 로직입니다.
  • Checkpointer: thread_id를 기준으로 대화 상태를 영구 저장하여 세션이 끊겨도 대화를 이어나갈 수 있게 합니다.

 

Python
 
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState

workflow = StateGraph(MessagesState)

memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

config = {"configurable": {"thread_id": "user_session_1"}}
app.invoke({"messages": [HumanMessage(content="안녕하세요")]}, config=config)

4. 휴먼 인 더 루프 (Human-in-the-loop)와 스트리밍

중요한 결정을 내릴 때는 AI 혼자 처리하게 두지 않고 사람이 검토해야 할 때가 있습니다. 또한, 답변이 생성되는 과정을 실시간으로 보여주어 기다리는 지루함을 줄여야 합니다.

  • 개념: 특정 단계 직전에 시스템을 일시 정지시켜 사람의 승인을 기다리거나 데이터를 수정할 수 있게 합니다. 스트리밍은 생성된 글자를 실시간으로 화면에 뿌려주는 기술입니다.
  • 예시 상황: AI가 고객에게 보낼 사과 이메일 초안을 작성한 뒤, "전송" 버튼을 누르기 전에 담당자에게 팝업을 띄워 내용을 확인받습니다. 담당자가 "승인"을 눌러야만 실제 메일이 발송됩니다.

 

  • interrupt_before: 특정 노드 실행 직전에 그래프를 멈추고 대기합니다.
  • Streaming: astream_events를 사용하여 토큰 단위로 생성되는 응답을 실시간으로 보여줍니다.

 

Python
 
app = workflow.compile(
    checkpointer=memory,
    interrupt_before=["send_email"]
)

async for event in app.astream_events(inputs, version="v2"):
    if event["event"] == "on_chat_model_stream":
        print(event["data"]["chunk"].content, end="")

5. 멀티 에이전트 패턴 1: Supervisor (감독관)

복잡한 프로젝트를 진행할 때 프로젝트 매니저(PM)가 팀원들에게 업무를 지시하는 것과 같습니다.

  • 개념: 중앙 관리자(Supervisor) 에이전트가 사용자의 요청을 듣고, 식단 전문가, 운동 전문가 등 적절한 하위 에이전트에게 작업을 지시하고 결과를 보고받습니다.
  • 예시 상황: 사용자가 "다이어트 계획 짜줘"라고 하면, 감독관이 먼저 식단 전문가에게 "식단표 짜와"라고 시키고, 그 다음 운동 전문가에게 "운동 루틴 짜와"라고 시킨 뒤, 마지막에 이를 종합하여 사용자에게 전달합니다.
Python
 
def supervisor_node(state: TeamState):
    response = llm.invoke(messages)
    return {"next": "DietExpert"} 

workflow.add_conditional_edges(
    "Supervisor",
    lambda state: state["next"],
    {
        "DietExpert": "DietExpert",
        "SleepExpert": "SleepExpert",
        "FINISH": END
    }
)

6. 멀티 에이전트 패턴 2: Handoff (업무 이관)

종합병원에서 진료를 보다가 의사가 "이건 제 분야가 아니니 저쪽 전문의에게 가보세요"라고 환자를 바로 연결해 주는 것과 비슷합니다.

  • 개념: 중앙 관리자를 거치지 않고, 에이전트가 스스로 판단하여 다른 전문 에이전트에게 직접 제어권을 넘깁니다. 도구(Tool) 기능을 활용해 다른 에이전트를 호출합니다.
  • 예시 상황: 고객센터 챗봇이 상담하다가 기술적인 문제가 나오면 "잠시만요, 기술 지원팀 연결해 드릴게요"라고 말하며 대화의 주도권을 기술 지원 에이전트에게 바로 넘깁니다.
Python
 
@tool
def transfer_to_sleep_expert():
    return "이동"

def should_continue(state):
    if tool_called:
        return "sleep_expert"
    return END

workflow.add_conditional_edges("diet_expert", should_continue)

7. 멀티 에이전트 패턴 3: Swarm (병렬 처리)

자동차 경주에서 피트인(Pit-in)을 했을 때, 여러 정비공이 달라붙어 타이어 교체와 주유를 동시에 진행하는 것과 같습니다.

  • 개념: 서로 관련 없는 독립적인 작업들을 동시에 실행하여 전체 처리 시간을 단축하는 패턴입니다. 하나의 시작점에서 여러 갈래로 흩어졌다가(Fan-out) 나중에 하나로 합쳐집니다(Fan-in).
  • 예시 상황: 사용자가 "내 건강 상태 종합 분석해줘"라고 요청하면, 식단 분석가와 수면 분석가가 동시에 각자 데이터를 분석합니다. 순차적으로 하면 10초 걸릴 일을 동시에 5초 만에 끝내고 결과를 합칠 수 있습니다.
Python
 
workflow.set_entry_point(["diet_expert", "sleep_expert"])

workflow.add_edge("diet_expert", "summarizer")
workflow.add_edge("sleep_expert", "summarizer")

멘탈 케어 앱을 개발하면서 사용자의 식사, 수면, 신체, 정신 건강 데이터를 분석하고 조언해 줄 RAG(검색 증강 생성) 시스템을 구축하고 있습니다. 처음에는 단순히 단계별로 기능을 붙여 나가면 성능이 좋아질 것이라 생각했습니다. 하지만 구현을 위해 깊이 공부할수록, 우리가 흔히 사용하는 벡터 데이터베이스 구조에 치명적인 함정이 있다는 것을 알게 되었습니다.

오늘 포스팅에서는 제가 처음에 구상했던 1차 고도화 전략과, 그 과정에서 발견한 문제를 해결하기 위해 진행한 2차 심화 학습(벡터 DB 필터링 이슈)의 과정을 위한 공부 및 정리를 하나로 정리해 봅니다.


1. 첫 번째 설계: RAG 성능 고도화 5단계 전략

처음 시스템을 기획할 때는 RAG의 품질을 단계적으로 높이는 로드맵을 그렸습니다. 사용자의 질문 의도를 정확히 파악하기 위해, 가장 단순한 검색부터 복잡한 추론까지 5가지 케이스로 나누어 설계했습니다.

  • 'Case 1: Basic Vector Search (Baseline)' 메타데이터 필터링 없이, 사용자의 질문을 벡터로 변환하여 가장 유사한 문서를 찾는 기본 방식입니다. 성능 비교의 기준점이 됩니다.
  • 'Case 2: Metadata Pre-filtering + Vector Search' 가장 핵심이라고 생각했던 단계입니다. "수면 카테고리에서만 검색해줘"처럼 SQL의 WHERE 절을 이용해 데이터를 먼저 좁히고 검색하는 방식입니다. 정확도와 속도를 모두 잡을 수 있는 정석적인 방법이라 생각했습니다.
  • 'Case 3: Hybrid Search (BM25 + Vector)' 벡터 검색이 약한 고유명사나 전문 용어를 잡기 위해, 전통적인 키워드 검색(BM25)을 결합하는 방식입니다.
  • 'Case 4: Re-ranking' 1차로 검색된 결과들을 Cross-Encoder 모델을 사용해 다시 정밀하게 줄 세우는 과정입니다. 정확도는 높아지지만 연산 비용이 듭니다.
  • 'Case 5: Graph RAG' 단순 검색을 넘어, "스트레스가 수면 장애를 일으키고, 이것이 식욕 변화로 이어진다"는 식의 인과관계를 지식 그래프로 추론하는 최종 단계입니다.

이 설계대로라면 'Case 2'의 메타데이터 필터링이 효율성의 핵심 열쇠였습니다. 하지만 이 방식에 대해 더 깊이 파고들면서, 벡터 DB의 구조적인 문제점에 부딪히게 되었습니다.


2. 문제의 발견: "필터링을 했더니 길을 잃었다"

우리가 흔히 사용하는 대부분의 벡터 DB(PostgreSQL의 pgvector 포함)는 검색 속도를 높이기 위해 'HNSW(Hierarchical Navigable Small World)'라는 인덱스 알고리즘을 사용합니다. 이것은 데이터들이 서로 비슷한 녀석들끼리 이웃처럼 연결된 '거미줄(그래프)' 같은 구조를 하고 있습니다.

여기서 문제가 발생합니다. "SQL로 필요 없는 데이터를 먼저 다 걸러내고(Pre-filtering), 남은 것 중에서 벡터 검색을 하면 빠르고 정확하겠지?"라고 생각하기 쉽지만, HNSW 구조에서 무작정 데이터를 걸러내면 '그래프 분절(Graph Disconnection)' 현상이 일어납니다.

 

HNSW 분절 문제: 데이터 관점에서의 기술적 분석

① 데이터 상황 설정 사용자가 "커피를 많이 마셔서 잠이 안 와"라는 질문을 던졌다고 가정해 봅시다. 이 질문은 임베딩 모델을 통해 벡터값(숫자 배열)으로 변환되어 공간상에 위치합니다.

  • '문서 A (정답)': "수면의 질을 높이려면 카페인 섭취를 줄여야 합니다." (Category: 수면)
  • '문서 B (연결고리)': "커피에 든 카페인은 각성 효과가 있어 뇌를 깨웁니다." (Category: 식사/영양)
  • '문서 C (오답)': "베개 높이는 경추 지지에 중요합니다." (Category: 수면)

②  HNSW 그래프의 연결 구조 (필터링 전) HNSW 인덱스는 의미적으로 유사한(거리가 가까운) 벡터끼리 선(Edge)으로 연결합니다.

  • 사용자의 질문("커피... 잠이 안 와")은 의미적으로 '문서 B("커피... 각성 효과")'와 가장 가깝습니다. 둘 다 '커피'와 '각성'이라는 문맥을 공유하기 때문입니다.
  • '문서 B'는 다시 '문서 A("카페인 줄이기... 수면")'와 연결되어 있습니다. '카페인'이라는 공통 주제가 있기 때문입니다.
  • 즉, 검색 알고리즘은 [질문] -> [문서 B] -> [문서 A] 순서로 타고 넘어가서 정답을 찾게 됩니다. 여기서 '문서 B'는 질문과 정답을 이어주는 중요한 '의미적 허브(Hub)' 역할을 합니다.

③  필터링 적용 시 발생하는 문제 (Pre-filtering) 여기서 사용자가 "수면 카테고리에서만 찾아줘"라며 SQL 필터링(WHERE category = '수면')을 걸었다고 합시다.

  • '연결고리 차단': 검색 대상에서 '식사/영양' 카테고리인 '문서 B'가 제외되어 투명 인간 취급을 받습니다.
  • '그래프 단절': HNSW 알고리즘은 질문과 가장 가까운 이웃인 '문서 B'로 넘어가려 하지만, 필터 조건 때문에 접근이 불가능합니다.
  • '고립': 이제 질문 벡터 주변에 남은 것은 같은 수면 카테고리인 '문서 C(베개)'뿐입니다. 하지만 '문서 C'는 질문(커피)과 의미적 거리가 너무 멉니다.
  • '탐색 실패': 알고리즘은 '문서 B'라는 다리를 건너지 못했기 때문에, 그 너머에 있는 진짜 정답인 '문서 A'의 존재를 파악하지 못하고 검색을 종료하거나, 엉뚱한 '문서 C'를 결과로 내놓게 됩니다.

이처럼 벡터 공간에서는 의미적으로 긴밀하게 연결되어 있어도, 메타데이터(카테고리)가 다르면 필터링 과정에서 '중간 연결 노드'가 사라지게 됩니다. 이것이 바로 HNSW 그래프가 끊어져 정답(Recall)을 놓치게 되는 데이터적 원리입니다.

이때 데이터베이스에는 다음과 같은 3개의 문서 청크(Chunk)가 저장되어 있습니다.

이 문제를 정확히 이해하려면, 텍스트가 벡터 공간(Vector Space)에 어떻게 배치되고 HNSW가 이들을 어떻게 연결하는지 살펴봐야 합니다.

 

실제로 필터링 후 남은 데이터가 전체의 1% 미만일 때, 일반적인 방식은 길이 끊어져서 정답을 찾지 못할 확률이 매우 높습니다. 재현율(Recall)이 20% 이하로 급락한다는 연구 결과도 있습니다. 즉, 10개의 정답 중 8개는 길이 끊겨서 못 찾게 되는 셈입니다.

 

기술적 예시

'차이값(거리)이 제거된다'는 표현은 '거리를 계산할 중간 다리가 끊어진다'는 의미로 이해하시면 아주 정확합니다.

HNSW와 같은 벡터 검색 알고리즘은 '모든 데이터와의 거리를 다 계산하는 것(전수 조사)'이 아닙니다. 그랬다간 데이터가 많을 때 너무 느리니까요. 대신, '내 친구(이웃)와의 거리만 계산해서 야금야금 이동하는' 방식을 씁니다.

이 과정에서 메타데이터 필터링이 어떻게 문제를 일으키는지, [MindBudget]의 실제 상황을 예로 들어 수학적인 거리 개념을 섞어 설명해 드릴게요.


실제 사례: "스트레스와 단 음식" 검색

사용자가 "스트레스 받으면 왜 초콜릿이 당길까?"라고 검색했고, 필터 조건으로 '카테고리: 정신(Mental)'만 걸었다고 가정해 봅시다.

1. 벡터 공간의 데이터 배치 (좌표로 상상해 보세요)

설명을 돕기 위해 복잡한 1024차원 벡터를 단순한 (x, y) 좌표로 표현해 보겠습니다. 거리가 가까울수록 내용이 비슷한 겁니다.

  • [Q] 질문: "스트레스 받으면 초콜릿 당김" (좌표: 10, 10)
  • [A] 연결고리 문서 (Body): "코르티솔 호르몬은 식욕을 자극함" (좌표: 11, 11)
    • 질문과 아주 비슷함 (거리: 1.4)
    • *하지만 카테고리가 '신체(Body)'
  • [B] 정답 문서 (Mental): "정신적 불안은 당분 섭취 욕구를 높임" (좌표: 12, 12)
    • 연결고리 문서와 아주 비슷함 (거리: 1.4)
    • *카테고리는 '정신(Mental)'
  • [C] 엉뚱한 문서 (Mental): "정신 건강을 위한 명상법" (좌표: 50, 50)
    • 질문과 전혀 다름 (거리: 56.5)
    • *카테고리는 '정신(Mental)'

2. 필터링 없을 때의 정상적인 검색 (계산 과정)

HNSW 알고리즘은 [Q] 질문 위치인 (10, 10)에 떨어집니다. 그리고 "내 근처에 누구 있어?"라고 주변을 살핍니다.

  1. 1단계 점프: 주변을 보니 [A] 연결고리 문서(Body)가 (11, 11)에 있습니다.
    • 계산: "오, 거리가 1.4밖에 안 되네? 이쪽으로 가보자." -> [A]로 이동
  2. 2단계 점프: 이제 [A] 위치인 (11, 11)에서 다시 주변을 살핍니다.
    • 계산: "옆에 [B] 정답 문서(Mental)가 (12, 12)에 있네? 거리 1.4로 아주 가깝군." -> [B]로 이동
  3. 결과: [B] 정답 문서를 찾아냈습니다!

핵심: '신체' 카테고리인 [A]가 징검다리 역할을 해서 [B]로 안내해 준 것입니다.


3. 'Pre-filtering(선 필터링)' 적용 시 발생하는 문제

이제 사용자가 "정신(Mental) 카테고리에서만 찾아줘"라고 명령했습니다. SQL이 먼저 작동해서 '신체' 카테고리인 [A] 연결고리 문서를 투명하게(검색 대상 제외) 만들어 버립니다.

  1. 1단계 시도: 알고리즘이 [Q] 질문 위치 (10, 10)에 떨어집니다. "내 근처에 '정신' 카테고리인 친구 있어?"라고 묻습니다.
  2. 계산 실패:
    • 원래 가장 가까웠던 [A] (11, 11)는 필터링되어 보이지 않습니다.
    • 진짜 정답인 [B] (12, 12)는 **[A]**를 거쳐야만 보이는(연결된) 위치에 있어서, [Q] 자리에서는 직접 보이지 않거나 연결선이 없습니다.
  3. 엉뚱한 연결: HNSW는 억지로라도 '정신' 카테고리인 이웃을 찾습니다. 그나마 멀리 희미하게 연결된 [C] 엉뚱한 문서 (50, 50)가 보입니다.
    • 계산: "거리가 56.5나 되지만... 얘 말고는 갈 곳이 없네."
    • 결과: 엉뚱한 [C]를 정답으로 내놓거나, "결과 없음"을 반환합니다.

4. [A] 가 없어지면 왜 [B]가 아닌 [C]가 결과로 나올까?

HNSW의 치명적 맹점: "나는 내 친구밖에 모릅니다"

많은 분들이 "벡터 DB는 모든 데이터와의 거리를 계산해서 가장 가까운 것을 찾아준다"고 오해하곤 합니다. 만약 전수 조사(Flat Index) 방식이라면 그 말이 맞습니다. 하지만 우리가 사용하는 HNSW는 속도를 위해 '전체 지도'를 보지 않는, 마치 장님 코끼리 만지기와 같은 방식을 사용합니다.

왜 바로 옆에 있는 정답을 찾지 못하고 엉뚱한 곳으로 가게 되는지, 친구 연락처에 빗대어 그 내부 메커니즘을 들여다보겠습니다.

1. 인덱스(친구 관계)가 만들어진 상황

HNSW는 데이터를 저장할 때, 효율성을 위해 모든 관계를 다 저장하지 않습니다. 나랑 가장 친한 친구 몇 명만 연락처에 저장해 두는 식입니다.

데이터가 입력될 때, HNSW는 효율을 위해 다음과 같이 관계를 연결해 두었다고 가정해 봅시다.

  • [Q] 질문(좌표 10, 10): "제 연락처엔 [A]만 저장되어 있습니다. 걔가 저랑 제일 비슷하거든요." (참고: B도 가깝지만, 이미 A와 연결해 두었으므로 굳이 중복해서 B까지 저장하지 않습니다. 이것이 HNSW의 효율적인 압축 방식입니다.)
  • [A] 연결고리(좌표 11, 11): "제 연락처엔 [B]가 저장되어 있습니다."
  • [C] 엉뚱한 데이터(좌표 50, 50): "전 왕따인데, 어쩌다 보니 Q랑 연결된 '먼 친구' 하나입니다." (참고: HNSW는 탐색 도중 멀리 점프하기 위해, 가끔 이렇게 거리가 먼 데이터도 연결해 둡니다.)

2. 필터링 발생 (Pre-filtering)

사용자가 "수면 카테고리만 찾아줘(A는 제외해 줘)!"라고 필터 조건을 걸었습니다. 이 순간, 질문 [Q]와 가장 가까웠던 **[A]**가 필터링에 걸려 투명 인간이 되어 버립니다.

3. 검색 시작 (비극의 시작)

이제 알고리즘이 [Q] 위치에서 출발하여 탐색을 시작합니다.

  • [Q]의 탐색: "자, 내 연락처(이웃 목록)를 한번 볼까?"
    • 원래 목록: [A, C]
    • 필터 적용 후 목록: [C] (A는 제외되어 보이지 않음)
  • 알고리즘의 판단: "어라? A는 조건에 안 맞아서 갈 수가 없네. 목록에 남은 건 C뿐이야."
  • 이동: 알고리즘은 울며 겨자 먹기로 아주 멀리 있는 [C]로 점프하여 이동합니다.
  • [B]의 운명: 진짜 정답인 [B]는 좌표 (12, 12)라는 아주 가까운 곳에 서 있습니다. 하지만 [Q]의 연락처에는 [B]가 없습니다. [Q]는 오직 [A]를 통해서만 [B]를 소개받을 수 있었는데, 유일한 소개팅 주선자인 A가 사라져 버린 것입니다.

결론: "보이지 않으면(연결되지 않으면) 거리를 잴 기회조차 없다"

우리가 흔히 생각하는 "B가 좌표상 더 가까우니까 당연히 B가 나와야지"라는 논리는 "알고리즘이 B의 존재를 알고 있다"는 전제하에만 성립합니다.

하지만 HNSW에서는 연결된 선(Edge)이 끊어지면, 바로 옆에 정답이 있어도 그 존재 자체를 모르게 됩니다. 아예 거리 계산 후보군에 B를 올리지도 못하고 탐색이 끝나버리는 것입니다.

이것이 바로 그래프 분절의 무서움입니다. 그래서 Qdrant와 같은 최신 벡터 DB들이 필터 조건별로 "[Q]랑 [B]를 직접 연결해 주는 비상 연락망"을 미리 만들어두는 기술(Filterable HNSW)을 개발한 것입니다.


2.1 기존 방식의 딜레마: 먼저 거를까, 나중에 거를까?

벡터 DB에서 필터링을 수행하는 전통적인 방식은 크게 두 가지가 있는데, 징검다리 문제와 연결해 보면 둘 다 명확한 한계가 존재합니다.

① Pre-filtering (선 필터링, 후 검색) SQL의 WHERE 절처럼 조건을 먼저 적용해 데이터를 걸러낸 뒤, 남은 데이터 안에서 벡터 검색을 하는 방식입니다.

  • '작동 방식': "수면 데이터만 남기고 다 지워!"라고 명령한 뒤, 남은 데이터끼리 유사도를 비교합니다.
  • '문제점': 앞서 설명한 징검다리 문제가 바로 여기서 발생합니다. 필터링으로 인해 중간 연결 노드들이 사라지면서 HNSW 그래프가 섬처럼 조각나 버립니다. 결국 검색 알고리즘이 건너갈 다리가 끊어져 정답에 도달하지 못하게 됩니다.

② Post-filtering (선 검색, 후 필터링) 일단 벡터 유사도로 상위 N개를 뽑은 뒤, 그중에서 조건에 안 맞는 것을 버리는 방식입니다.

  • '작동 방식': 일단 질문과 가장 비슷한 데이터 100개를 가져옵니다. 그 후 "여기서 수면 데이터가 아닌 건 다 빼!"라고 필터링합니다.
  • '문제점': 만약 상위 100개가 전부 '식사'나 '운동' 데이터라면 어떨까요? 필터링을 거치고 나면 결과가 0개가 되어버립니다. 사용자는 분명 데이터가 있는데도 "검색 결과 없음"을 보게 되는 셈입니다.

결국 기존의 두 방식 모두 정확도(Recall) 측면에서 치명적인 약점을 가지고 있었고, 이를 해결하기 위해 2026년의 벡터 DB들은 새로운 길을 모색하게 된 것입니다.

 


3. 해결책: 2026년 벡터 DB 트렌드와 5가지 대안의 장단점

HNSW의 구조적 한계인 '그래프 분절'을 해결하기 위해, 각 데이터베이스는 저마다의 독창적인 방식으로 길을 연결하고 있습니다. 각 기술의 원리와 효과, 그리고 고려해야 할 단점까지 가감 없이 비교해 보겠습니다.

① Qdrant: 'Filterable HNSW' (비상 연락망 구축)

  • '원리': HNSW 인덱스를 구축하는 시점부터 메타데이터를 고려합니다. 벡터의 유사성뿐만 아니라, 필터 조건별로 이동할 수 있는 '추가적인 연결 고리(Edge)'를 미리 생성해 둡니다.
  • '예시 상황': [Q]의 연락처에 원래는 [A]만 있었지만, Qdrant는 "혹시 A가 차단당할 경우를 대비해, B 직통 번호도 적어두자"라며 미리 추가 연결선을 만들어 둡니다.
  • '효과': 필터링으로 [A]가 사라져도, 미리 준비해 둔 [B] 직통 번호를 통해 탐색을 이어갈 수 있습니다. 필터 조건이 매우 까다로워 남은 데이터가 1% 미만이어도 95% 이상의 높은 재현율(Recall)을 유지합니다.
  • '단점': 모든 필터 조건에 대해 연결 고리를 추가로 만들어야 하므로, 인덱스 용량이 커지고 메모리를 더 많이 사용합니다.

② Weaviate: 'ACORN' 알고리즘 (징검다리 허용)

  • '원리': 검색 도중 필터 조건에 맞지 않는 노드를 만나면, 이를 결과 목록에는 포함시키지 않지만 다음 노드로 넘어가기 위한 '징검다리'로는 활용합니다.
  • '예시 상황': 탐색 도중 조건에 안 맞는 [A]를 만났습니다. 기존 방식은 "에이, 길이 막혔네" 하고 포기하지만, ACORN은 "A는 답이 아니니까 챙기진 말자. 대신 A를 밟고 건너가서 그 뒤에 있는 B를 확인하자"라고 판단합니다.
  • '효과': 필터된 노드를 단순히 삭제하는 것이 아니라 탐색 경로로 활용하기 때문에 유연성이 높습니다. 하이브리드 검색(BM25+Vector)에도 유리합니다.
  • '단점': 필터 조건이 너무 강력해서 대부분의 노드가 정답이 아닌 경우(예: 100만 개 중 5개만 정답), 정답을 찾기 위해 너무 많은 징검다리를 건너뛰어야 해서 검색 속도가 느려질 수 있습니다.

③ MyScale: 'Single-stage Filtering' (MSTG 알고리즘)

  • '원리': 벡터 검색과 필터링을 분리하지 않고, 하나의 수식으로 합쳐서 계산합니다. 트리를 탐색할 때 벡터 유사도 점수와 필터 일치 여부를 실시간으로 통합하여 판단합니다.
  • '예시 상황': 스마트 내비게이션이 길을 찾을 때 "유료도로를 다 지우고 시작해(Pre)"라고 하지 않습니다. 대신 "이 길은 빠르지만(+100점) 유료니까 페널티(-50점)"와 같이 점수를 매겨, **"속도와 비용을 합산했을 때 가장 이득인 길"**을 한 번에 계산하며 나아갑니다.
  • '효과': 그래프가 끊어질 걱정이 아예 없습니다. 특히 복잡한 SQL 조건(JOIN 등)이 섞여 있을 때도 속도 저하 없이 정확한 결과를 찾아냅니다.
  • '단점': 실시간으로 필터 조건을 함께 연산해야 하므로, 검색 시 CPU 연산량이 많아져 리소스 소모가 클 수 있습니다.

④ Milvus: 'Partition Key' (방 나누기)

  • '원리': 물리적인 데이터를 '파티션(Partition)' 단위로 나누어 관리합니다. 검색 시 필터 조건에 맞는 파티션만 지정해서 그래프를 탐색합니다.
  • '예시 상황': 친구들을 아예 '수면 반', '식사 반'처럼 다른 교실(파티션)에 배정합니다. "수면 데이터 찾아줘" 하면 '수면 반' 교실 문만 열고 들어가서 그 안에서 짜인 친구 관계도만 봅니다. 옆 반 친구가 사라지든 말든 상관없습니다.
  • '효과': 특정 카테고리 내에서만 검색할 때 그래프 분절 문제가 원천적으로 발생하지 않습니다. 대용량 데이터를 다룰 때 매우 효율적입니다.
  • '단점': 여러 파티션을 넘나들며 검색해야 하는 경우(예: '수면'과 '식사' 동시 검색)에는 성능이 떨어지거나 관리가 복잡해집니다. 파티션별 데이터 불균형이 심하면 효율이 저하됩니다.

⑤ 전략적 대안: 'Flat Index' (전수 조사)

  • '원리': HNSW 같은 근사 알고리즘(ANN)을 쓰지 않고, 무식하지만 확실하게 모든 데이터와 일일이 거리를 계산하는 방식입니다.
  • '예시 상황': 친구 연락처를 타고 넘어가는 게 아니라, 전화번호부 책을 펴고 1번부터 끝번까지 모든 사람에게 전화를 걸어 확인합니다. "너 나랑 얼마나 친해?"
  • '효과': 길 찾기 자체가 아니므로 '길이 끊어질' 일이 없습니다. 무조건 재현율(Recall) 100%를 보장합니다. 데이터가 적거나(수십만 건 이하), 필터링 후 남는 데이터가 극소수일 때 가장 빠르고 정확합니다.
  • '단점': 데이터가 많아질수록 계산량이 기하급수적으로 늘어나 속도가 매우 느려집니다. 빠른 속도를 유지하려면 고가의 GPU 장비가 필요할 수 있습니다



4. 아키텍처 조합별 비교 분석

단순한 기능 구현을 넘어, 실제 운영 시의 복잡도와 비용 효율성을 따져보기 위해 총 5가지의 아키텍처 조합을 비교 분석했습니다.

 

조합 A: 일반 RAG (Qdrant + PostgreSQL) 가장 표준적이고 강력한 조합입니다. 관계형 데이터는 PostgreSQL이, 벡터 검색과 필터링은 Qdrant가 전담합니다.

  • '구성': PostgreSQL(메타데이터, 원본) + Qdrant(벡터 저장, 필터링) + KURE v1(임베딩)
  • '평가': 필터링 성능과 확장성에서 별 5점을 줄 만큼 우수합니다. 오픈소스라 비용 효율도 좋지만, 두 개의 시스템을 운영해야 하므로 복잡도는 중간 정도입니다.

조합 B: 하이브리드 검색 (Weaviate + PostgreSQL) 키워드 검색(BM25)이 중요할 때 선택하는 조합입니다.

  • '구성': PostgreSQL(관계형) + Weaviate(벡터+BM25) + KURE v1(임베딩)
  • '평가': 필터링과 하이브리드 검색 성능은 최상이지만, GraphQL을 익혀야 하는 러닝커브가 존재합니다.

조합 C: 단순 통합 (PostgreSQL + pgvector) 관리 포인트가 하나라는 점이 최대 강점인 조합입니다.

  • '구성': PostgreSQL + pgvector(모든 기능 통합) + KURE v1(임베딩)
  • '평가': 운영 복잡도가 가장 낮고 관리형 옵션이 다양해 시작하기 좋습니다. 다만 필터링 성능은 조건이 느슨할 때만 유효하며(별 3점), 확장성 면에서는 전용 DB보다 부족합니다.

조합 D: Graph RAG (Neo4j 단일) 데이터 간의 관계가 핵심일 때 사용하는 조합입니다.

  • '구성': Neo4j(지식 그래프 + 벡터 통합) + PostgreSQL(선택) + KURE v1(임베딩)
  • '평가': 관계 탐색과 다중 홉 추론 능력은 독보적입니다. 다만 운영 복잡도와 비용이 다소 높은 편입니다.

조합 E: 풀 하이브리드 (Qdrant + Neo4j + PostgreSQL) 모든 장점을 다 취하는 끝판왕 조합입니다.

  • '구성': PostgreSQL + Qdrant(벡터) + Neo4j(그래프) + KURE v1(임베딩)
  • '평가': 기능과 유연성은 최고지만, 세 개의 시스템을 동시에 운영해야 하므로 복잡도가 매우 높고 비용 부담이 큽니다.

💡 오늘의 교훈:

"RAG 한다고 무지성으로 벡터 DB 아무거나 쓰지 말자. '필터링'이 어떻게 동작하는지 모르면, 사용자는 영원히 답변을 못 받을 수도 있다."

오늘의 삽질과 공부 끝에 내린 결론은 다음과 같다.

공부한 내용을 바탕으로 'MindBudget'을 위한 DB 후보를 추렸다.

다행히 2026년 현재, 이 문제를 해결한 똑똑한 알고리즘들이 나와 있었다. 오늘 비교해 본 핵심 기술들이다.

1. Graph RAG란 무엇인가?

AI 개발자라면 "RAG (검색 증강 생성)"는 이제 익숙한 개념일 것입니다. 하지만 단순 RAG가 키워드 중심의 "단어 찾기"라면, Graph RAG는 데이터 사이의 연결 고리를 파악하는 "맥락 이해하기"의 영역입니다.

🔍 기존 RAG vs Graph RAG 비교

구분 기존 RAG (Vector Search) Graph RAG (Knowledge Graph)
비유 도서관 사서: 키워드가 있는 페이지만 찾아줌 탐정의 수사 보드: 인물과 사건을 실로 연결해 관계를 파악함
작동 방식 텍스트 유사도(Similarity) 기반 검색 개체(Entity)와 관계(Relation) 기반 연결
장점 구축이 쉽고 빠름 복잡한 인과관계 파악, 전체 요약에 강력함
단점 "전체적인 주제가 뭐야?" 같은 질문에 약함 구축 비용이 비싸고 인덱싱 시간이 오래 걸림

2. RAG 구축의 양대 산맥: LlamaIndex vs LangChain

Graph RAG를 시작하기 전에 반드시 짚고 넘어가야 할 것이 있습니다. 바로 "무엇으로 만들 것인가?"입니다. 많은 개발자가 LangChain과 LlamaIndex 사이에서 고민하지만, 사실 이 둘은 "RAG 시스템 내에서 담당하는 역할(Role)"이 완전히 다릅니다.🔍 RAG 관점에서의 결정적 차이

구분 LlamaIndex 🦙 LangChain 🦜🔗
RAG에서의 역할 전문 사서 (The Librarian) 총괄 매니저 (The Manager)
핵심 집중 "데이터를 얼마나 잘 찾을 것인가?" "찾은 데이터로 무엇을 할 것인가?"
데이터 처리 텍스트 청킹, 임베딩, 그래프 구조화에 특화됨 기본적인 로더는 제공하나 깊이 있는 최적화는 부족함
Graph RAG PropertyGraphIndex 등 전용 모듈 제공 (구축 쉬움) 그래프 체인을 직접 구성해야 함 (구축 어려움)
추천 상황 고품질의 검색 결과가 최우선일 때 챗봇의 대화 흐름이나 도구 사용이 중요할 때

1️⃣ LlamaIndex: 데이터의 깊이를 파고들 때 (Data-Centric)

  • Graph RAG에서의 강점: 복잡한 텍스트에서 개체(Entity)와 관계(Relation)를 추출하고 저장하는 과정이 자동화되어 있습니다.
  • 예시: 수천 페이지의 의학 논문이나 법률 문서에서 정확한 인과관계를 찾아야 한다면 LlamaIndex가 압도적으로 유리합니다.

2️⃣ LangChain: 서비스의 흐름을 만들 때 (Application-Centric)

  • RAG에서의 강점: 단순히 검색만 하는 게 아니라, 검색 결과가 없으면 구글링을 하거나, 사용자에게 되묻는 식의 유연한 대처가 가능합니다.

💡 결론: 멘탈 케어 앱에는 무엇을 써야 할까? (Hybrid Strategy)

  • LlamaIndex의 역할 (RAG 엔진):
    • 사용자의 1년 치 일기 데이터를 분석하여 "스트레스(원인) → 불면증(결과)"과 같은 지식 그래프를 구축하고, 질문에 대한 정확한 맥락을 찾아냅니다.
  • LangChain의 역할 (서비스 뼈대):
    • LlamaIndex가 찾아낸 원인을 바탕으로, 사용자의 감정 상태에 맞춰 위로의 말을 건네거나 명상 음악 API를 실행하는 워크플로우를 관리합니다.

요약: "데이터를 씹고 뜯고 맛보고 즐기는 건 LlamaIndex에게, 그 데이터를 가지고 손님(사용자)을 접대하는 건 LangChain에게 맡기세요."

  • 성공적인 서비스를 위해서는 두 도구의 장점만 뽑아서 섞어 쓰는(Hybrid) 전략이 가장 효율적입니다.
  • LangChain은 "애플리케이션의 로직"을 연결하는 데 강합니다. 검색된 데이터를 가지고 번역을 하거나, 요약을 하거나, API를 호출하는 등 '행동(Action)'을 제어합니다.
  • LlamaIndex는 "LLM을 위한 데이터 프레임워크"를 표방합니다. 즉, 엉망인 데이터를 LLM이 이해하기 좋게 정리하고(Indexing), 질문에 딱 맞는 정보를 꺼내오는(Retrieval) 기술에 올인한 도구입니다.
  • RAG(검색 증강 생성)는 크게 '검색(Retrieval)'과 '생성/제어(Generation & Flow)' 두 단계로 나뉩니다. 이 두 프레임워크는 각각의 단계에서 강점을 보입니다.
  • 왜 이 두 도구를 비교해야 할까요? Graph RAG의 핵심인 '데이터 구조화'를 누가 더 잘하느냐가 성공의 열쇠이기 때문입니다.

3. 프레임워크 없이 Graph RAG 직접 구현하기 (Custom Implementation)

LlamaIndex 같은 도구 없이, 순수 파이썬과 DB만으로 Graph RAG를 구축하려면 어떻게 해야 할까요? 이 과정을 이해하면 Graph RAG의 본질을 더 잘 알 수 있습니다. 이 방식은 "내 마음대로 커스터마이징"이 가능하지만, 구현 난이도가 높습니다.

🛠️ 구축 4단계 프로세스

1단계: 프롬프트 엔지니어링 (추출기 만들기)

LLM에게 텍스트를 주고 JSON 형태로 관계를 뽑아내도록 강력한 시스템 프롬프트를 짜야 합니다.

Python
 
# System Prompt 예시
system_prompt = """
당신은 데이터 분석가입니다. 아래 텍스트에서 'Entity(개체)'와 'Relation(관계)'를 추출하여 JSON으로 반환하세요.
형식: {"head": "원인/주체", "relation": "관계", "tail": "결과/대상"}
예시: "팀장님이 화를 내서 슬펐다" -> {"head": "팀장님", "relation": "화를 냄", "tail": "나"}, {"head": "팀장님의 화", "relation": "유발함", "tail": "슬픔"}
"""

2단계: 그래프 데이터베이스(Graph DB) 저장

추출된 JSON 데이터를 Neo4jNetworkX 같은 그래프 DB에 저장합니다. 이때 벡터 임베딩도 함께 저장해야 검색 성능이 올라갑니다.

  • 사용 도구: Neo4j Python Driver, NetworkX
  • 작업: 노드(Node) 생성, 엣지(Edge) 연결, 중복 제거(Entity Resolution) 로직 직접 구현.

3단계: 검색 (Retrieval) - Cypher Query 작성

사용자가 질문하면, 이를 DB 언어(Cypher 등)로 변환하거나, 미리 정의된 룰에 따라 그래프를 탐색해야 합니다. 자연어 질문을 쿼리로 바꾸는 과정이 매우 까다롭습니다.

Cypher
 
MATCH (p:Person)-[r:CAUSES]->(s:Symptom)
WHERE p.name = '팀장님'
RETURN s

4단계: 답변 생성 (Synthesis)

그래프 DB에서 찾아온 연결 관계(트리플)를 자연어 문장으로 바꿔서 다시 LLM에게 넘겨줍니다.

  • Context: "팀장님 --(유발)--> 스트레스 --(유발)--> 불면증"
  • LLM에게 전달: "위의 관계를 참고하여 사용자의 불면증 원인을 설명해줘."

⚖️ LlamaIndex 사용 vs 직접 구현 비교

구분 LlamaIndex (PropertyGraphIndex) 직접 구현 (Custom)
구현 난이도 (코드 5~10줄) (전처리, DB연동, 쿼리 작성 필요)
데이터 추출 내장된 추출기 자동 실행 프롬프트 직접 설계 및 파싱 필요
유지 보수 라이브러리 업데이트만 하면 됨 데이터 스키마 변경 시 코드 전체 수정
추천 대상 빠르게 검증하고 싶은 스타트업/개발자 초대형/특수 목적의 그래프 최적화가 필요한 팀

4. LlamaIndex로 저비용 고효율 Graph RAG 구축하기

그렇다면 현실적으로 가장 효율적인 방법은 무엇일까요? LlamaIndex를 활용해 비용은 낮추고 성능은 높이는 전략을 소개합니다.

🧠 실전 시나리오: 멘탈 케어 AI 만들기

사용자의 일기 데이터를 분석해 불면증의 원인을 찾아주는 AI를 만든다고 가정해봅시다.

  • 일반 RAG의 한계
    • 질문: "나 요즘 왜 잠을 못 자지?"
    • 검색: '잠', '불면' 키워드 검색.
    • 결과: "오늘 잠이 안 와서 따뜻한 우유를 마셨다." (현상만 찾고 원인은 못 찾음)
  • Graph RAG의 해결책
    • 그래프 연결: [새 프로젝트] → (유발) → [야근] → (유발) → [팀장님의 압박] → (결과) → [스트레스] → (증상) → [불면증]
    • AI 답변: "최근 시작한 새 프로젝트로 인한 야근과, 그 과정에서 팀장님과의 갈등이 스트레스가 되어 불면증으로 이어진 것 같습니다."
    • 핵심: 떨어져 있는 정보들을 **'연결 고리'**를 통해 찾아냅니다.

💰 저비용 구축 전략 (Cost Optimization)

Graph RAG는 LLM 사용량이 많아 비용이 비쌀 수 있습니다. 이를 해결하는 3가지 전략입니다.

  1. 모델 이원화 전략:
    • 추출(Extraction): 단순 노동인 관계 추출은 저렴한 GPT-4o-mini나 로컬 모델(Ollama, GLiNER) 사용.
    • 답변(Generation): 최종 추론은 똑똑한 GPT-4o 사용.
  2. LlamaIndex 활용: PropertyGraphIndex를 사용하여 복잡한 그래프 구축 과정을 코드 몇 줄로 자동화.
  3. 무료 DB 활용: Neo4j Community Edition (Docker)을 사용하여 저장소 비용 절감.

5. 핵심 코드 (Python + LlamaIndex)

LlamaIndex를 사용하여 벡터 검색과 그래프 검색을 동시에 수행하는 하이브리드 검색 구현 예시입니다.

Python
 
from llama_index.core import PropertyGraphIndex, SimpleDirectoryReader
from llama_index.llms.openai import OpenAI

# 1. 가성비 모델 설정 (추출용 - 비용 절감 핵심!)
cheap_llm = OpenAI(model="gpt-4o-mini")

# 2. 데이터 로딩 (내 일기장 데이터)
documents = SimpleDirectoryReader("./my_diary_data").load_data()

# 3. 그래프 인덱스 생성 (자동으로 관계 추출 및 저장)
# 내부적으로 LLM이 텍스트를 읽고 그래프(노드+엣지)를 생성함
index = PropertyGraphIndex.from_documents(
    documents,
    llm=cheap_llm,
    show_progress=True
)

# 4. 검색 엔진 생성 (질문하기)
query_engine = index.as_query_engine()

# 5. 질문 및 답변 출력
response = query_engine.query("내가 요즘 스트레스 받는 근본적인 원인이 뭐야?")
print(response)

 

6. 마치며

Graph RAG는 AI가 인간처럼 '맥락'을 이해하게 만드는 핵심 기술입니다. 초보자나 1인 개발자라면 LlamaIndex로 빠르게 프로토타입을 만들고, 서비스가 고도화되어 아주 디테일한 튜닝이 필요할 때 직접 구현으로 넘어가는 로드맵을 추천합니다.

특히 멘탈 케어처럼 "감정의 흐름"이나 "사건의 인과관계"가 중요한 서비스라면, Graph RAG는 선택이 아닌 필수입니다.

1. 핵심 개념: 메타데이터 필터링 (Metadata Filtering)

1.1 왜 필요한가? (Pure Vector Search의 한계)

벡터 검색(Vector Search)은 데이터의 '의미'를 찾아주는 강력한 기술이지만, 데이터 양이 많아질수록 치명적인 단점이 발생합니다.

  • 속도 저하: 수십만, 수백만 개의 벡터와 일일이 거리를 계산해야 하므로 연산 비용이 높습니다.
  • 정확도 이슈: 문맥은 비슷하지만 내가 원하지 않는 조건의 데이터(예: 품절된 상품, 옛날 뉴스 등)까지 검색될 수 있습니다.

이 문제를 해결하는 것이 바로 메타데이터 필터링(Metadata Filtering), 즉 **"조건(Postgres)으로 먼저 거르고, 의미(Vector)로 찾는 기술"**입니다. 이를 기술적으로는 **사전 필터링(Pre-filtering)**이라고도 부릅니다.

 

1.2 쉬운 예시: 옷 가게에서 옷 찾기

내가 **"파란색 여름 셔츠 중 내 스타일인 것"**을 찾는 상황을 가정해 봅시다.

  1. 1차 필터링 (PostgreSQL 담당 - 메타데이터):
    • 매장 전체(1,000벌)를 하나하나 대보지 않습니다.
    • 먼저 **'여름 코너'**의 **'셔츠 진열대'**에서 '파란색' 옷만 싹 골라냅니다.
    • 결과: 후보군이 1,000개에서 10개로 확 줄어듭니다. (연산 대상 최소화)
  2. 2차 벡터 검색 (Vector 담당 - 유사도):
    • 딱 그 10벌 중에서만 내가 입고 있는 옷과 스타일(벡터)이 가장 비슷한 옷을 찾습니다.
  3. 결론:
    • 무거운 벡터 연산 대상을 획기적으로 줄여주므로 속도가 빨라집니다.
    • 엉뚱한 계절이나 색상의 옷이 추천될 확률을 0%로 만들어 정확도가 높아집니다.

2. 도구의 선택: 왜 pgvector인가?

벡터 검색을 위해 별도의 DB(Pinecone, Milvus 등)를 도입하는 대신, 기존 RDB인 PostgreSQL에 확장 플러그인인 **pgvector**를 사용하는 것이 효율적입니다.

pgvector 방식 (All-in-One)

  • SQL 한 줄로 **필터링(WHERE)**과 **유사도 검색(ORDER BY)**을 동시에 처리할 수 있습니다.
  • 데이터 관리 포인트가 하나로 통합되어 유지보수가 쉽고, 트랜잭션 관리가 용이합니다.
SQL
 
-- pgvector를 사용한 하이브리드 검색 쿼리 예시
SELECT content, category
FROM mental_care_tips
WHERE category = '수면'              -- [1] 메타데이터 필터링 (Pre-filtering)
ORDER BY embedding <-> '[0.12, ...]' -- [2] 벡터 거리 계산 (필터링된 데이터 내에서만 수행)
LIMIT 3;

3. 실전 RAG 워크플로우: 검색 및 답변 (Retrieval & Generation)

사용자가 **"잠이 안 오는데 차 추천해줘"**라고 물었을 때, 메타데이터를 어떻게 확보하느냐에 따라 두 가지 방식으로 구현됩니다.

CASE A. 사용자가 직접 카테고리를 선택한 경우 (Explicit)

UI에서 '수면' 버튼을 누르고 질문을 입력한 상황. 가장 빠르고 정확합니다.

  1. 사용자 입력: 카테고리(수면) + 질문 텍스트("차 추천해줘")
  2. 임베딩 생성 (Server): 질문 텍스트만 임베딩 모델에 넣어 벡터로 변환합니다.
  3. DB 검색 요청:
    • 서버가 DB에 쿼리를 전송합니다: WHERE category='수면' AND Vector Similarity
  4. DB 내부 작동 (Pre-filtering):
    • 인덱스를 타서 '수면' 데이터만 먼저 남긴 후, 벡터 거리를 계산합니다.
  5. 답변 생성: 검색된 전문 지식을 바탕으로 LLM이 답변을 작성합니다.

CASE B. 사용자가 말로만 질문한 경우 (Implicit / LLM Extraction)

UI 선택 없이 채팅창에 바로 "수면에 좋은 차 추천해줘"라고 입력한 상황.

  1. 사용자 입력: "수면에 좋은 차 추천해줘"
  2. 메타데이터 추출 (LLM):
    • 질문을 먼저 가벼운 LLM에게 보냅니다.
    • 프롬프트: "이 질문의 의도를 파악해서 카테고리를 태깅해줘."
    • 결과: LLM이 질문을 분석하여 category='수면'이라는 조건을 뽑아냅니다. (Function Calling 활용)
  3. 임베딩 생성 (Server): 질문 텍스트를 벡터로 변환합니다.
  4. DB 검색 요청:
    • LLM이 뽑아준 category='수면' 조건과 벡터를 합쳐서 DB에 쿼리를 전송합니다.
  5. 답변 생성: 이후 과정은 CASE A와 동일하게 진행됩니다.

4. 마무리

결국 효율적인 RAG 시스템의 핵심은 **"얼마나 똑똑하게 검색 범위를 줄여주느냐"**에 달려 있습니다. PostgreSQL의 pgvector를 활용하면 기존의 강력한 SQL 필터링 기능과 최신 벡터 검색 기술을 하나의 DB에서 간편하게 결합할 수 있습니다.

🛠️ 트러블슈팅 리포트: localhost 연결 거부 및 Hyper-V 포트 충돌 이슈

1. 문제 상황(Symptoms)

로컬 개발 환경에서 프론트엔드 또는 백엔드 서버를 평소와 같이 실행(예: 3000번 포트)했으나, 브라우저에서 접속 시 다음과 같은 오류가 발생하며 접속이 불가능함.

브라우저 오류 메시지:

  • 사이트에 연결할 수 없음
  • localhost에서 연결을 거부했습니다. (ERR_CONNECTION_REFUSED)
  • 연결 확인, 프록시 및 방화벽 확인 필요

 

2. 원인 분석 (Root Cause)

Windows의 Hyper-V(및 WSL2, Docker) 가 사용하는 네트워크 서비스인 WinNAT이 부팅 시 동적 포트 범위(Dynamic Port Range) 를 무작위로 예약(Reservation)하는 과정에서 발생한 충돌.

  1. 동적 포트 할당: Windows는 부팅 시 Hyper-V나 컨테이너 통신을 위해 일정 범위의 포트를 미리 '예약'함.
  2. 범위 중첩: Windows의 기본 동적 포트 범위는 1024 ~ 15001로 설정되어 있음. 이 범위 내에 개발에 주로 사용되는 3000, 8000, 8080 포트가 포함됨.
  3. 랜덤 예약: WinNAT 서비스가 하필 개발용 포트(예: 3000)가 포함된 구간(예: 2997~3096)을 랜덤으로 선점해버림.
  4. 결과: 개발 서버가 3000번 포트를 요청했으나, 이미 시스템(Hyper-V)이 예약해 둔 상태라 접근이 거부됨.

 

3. 해결 방법 (Solution)

시스템이 사용하는 동적 포트의 범위를 개발자가 잘 쓰지 않는 49152번 이후로 강제 변경하여 충돌을 영구적으로 방지함.

Step 1. 관리자 권한으로 터미널(CMD/PowerShell) 실행

Step 2. 동적 포트 범위 변경 명령어 입력

PowerShell
 
netsh int ipv4 set dynamicport tcp start=49152 num=16384
  • start=49152: 동적 포트 시작점을 49152번으로 설정.
  • num=16384: 포트 개수를 16,384개로 설정 (49152 ~ 65535 범위).

Step 3. 시스템 재부팅 (필수) 설정 적용을 위해 재부팅 진행.

(참고: 당장 재부팅이 불가능할 경우 net stop winnat -> net start winnat으로 임시 해결 가능하나 재발 가능성 있음)

 

4. 기술적 검토: 이 방법이 안전한 이유 (Safety & Standards)

포트 범위를 뒤로 미루는 것은 시스템 오류를 유발하지 않으며, 오히려 국제 표준을 준수하는 권장 설정임.

  1. IANA 표준 준수:
    • 인터넷 할당 번호 관리기관(IANA)의 포트 표준 구분:
      • 0 ~ 1023: Well-known Ports (시스템용)
      • 1024 ~ 49151: Registered Ports (사용자/개발자/서버 애플리케이션용 - 3000, 8080 등)
      • 49152 ~ 65535: Dynamic/Private Ports (임시 할당용)
    • Windows의 기본 설정(1024~)은 오히려 'Registered Ports' 구역을 침범하고 있었음. 변경된 설정(49152~)이 표준에 부합함.
  2. 리소스 충분성:
    • 변경 후에도 약 16,000개의 포트를 동적으로 사용할 수 있음.
    • 일반적인 개발 PC나 워크스테이션에서 동시에 16,000개 이상의 외부 네트워크 연결(Socket)을 맺는 경우는 극히 드물므로 성능 이슈나 포트 고갈(Port Exhaustion) 문제는 발생하지 않음.

 

5. 추가 질문

Q1. Hyper-V 외에 포트를 이렇게 잡는 프로세스가 더 있나?

  • 주범 (WinNAT): Hyper-V, WSL2, Docker for Windows, Windows Sandbox 등 가상화 관련 기술은 모두 WinNAT 서비스를 통해 네트워크를 처리하며, 이 녀석이 대량의 포트를 '블록 단위'로 예약하는 유일한 주범임.
  • 기타 (RPC 등): Windows RPC 등 시스템 내부 통신도 동적 포트를 쓰지만, 필요할 때 하나씩 가져다 쓰고 바로 반납하는 형태라 Hyper-V처럼 거대한 '알박기' 문제는 거의 일으키지 않음.

 

Q1-1. Hyper-V, WSL2, Docker for Windows, Windows Sandbox 은 무엇을 하는 프로세스인가?

🏛️ 1. 사건의 주범들: 가상화 4인방 정체

이들은 모두 "내 컴퓨터(Windows) 안에 또 다른 컴퓨터를 만드는 기술"이라는 공통점이 있어.

① Hyper-V (대장님)

  • 정체: 마이크로소프트가 만든 하드웨어 가상화 기술의 핵심 엔진.
  • 하는 일: 윈도우 위에 가상의 컴퓨터(VM)를 여러 대 만들 수 있게 해줘.
  • 비유: 네 컴퓨터가 큰 '집'이라면, Hyper-V는 집 안에 방음벽을 세워서 독립된 '원룸'들을 만드는 건축가야.
  • 특징: 아래 나오는 모든 애들(WSL2, Docker, Sandbox)은 사실 뒤에서 Hyper-V 기술을 기반으로 돌아가. 즉, 얘가 뿌리야.

② WSL2 (Windows Subsystem for Linux 2)

  • 정체: 윈도우에서 리눅스를 '찐'으로 돌려주는 시스템.
  • 하는 일: 예전(WSL1)에는 흉내만 냈다면, WSL2는 Hyper-V를 이용해서 진짜 리눅스 커널을 가볍게 돌려.
  • 왜 써?: 개발자들은 리눅스 환경(Ubuntu 등)이 필수인데, 듀얼 부팅하기 귀찮으니까 윈도우 안에서 리눅스 터미널을 쓰려고 사용해.
  • 너와의 관계: 네가 AI 개발을 한다면 99% 확률로 이걸 쓰고 있거나 쓰게 될 거야.

③ Docker Desktop for Windows

  • 정체: 애플리케이션을 '컨테이너'라는 상자에 담아 배포하는 도구.
  • 하는 일: 내 컴퓨터에서 짠 코드가 서버에서도 똑같이 돌아가게 만들어줘.
  • 작동 원리: 리눅스 기반 컨테이너를 돌려야 하는데, 윈도우는 리눅스가 아니잖아? 그래서 WSL2(또는 Hyper-V)를 빌려서 그 위에서 리눅스를 띄우고 컨테이너를 돌려.
  • 결국: Docker를 켜면 -> WSL2가 켜지고 -> 결국 Hyper-V가 일하는 구조야.

④ Windows Sandbox

  • 정체: 쓰고 버리는 일회용 윈도우.
  • 하는 일: 의심스러운 파일(.exe)을 실행해보고 싶을 때, 바이러스가 걸려도 내 본체에는 영향이 없는 격리된 공간을 띄워줘. 끄면 싹 사라짐.
  • 원리: 이것도 Hyper-V 기술로 아주 가벼운 가상 윈도우를 순간적으로 만드는 거야.

 

Q1-2. 왜 다들 WinNAT을 쓰고 포트를 훔쳐가? (핵심 원리)

이제 중요한 건 "왜 얘네가 포트를 뭉텅이로 가져가느냐"지.

상황: "아파트 단지(가상화)"와 "우편물 관리실(WinNAT)"

  1. 가상 세계의 고립: Hyper-V가 만든 가상 컴퓨터(Docker 컨테이너, WSL2 리눅스 등)들은 자기들만의 사설 IP를 가지고 있어. 바깥세상(인터넷)과는 직접 연결이 안 돼.
  2. 통역사가 필요해: 이 가상 컴퓨터들이 인터넷을 하려면, 네 진짜 컴퓨터(호스트)의 IP를 빌려 써야 해. 이때 "내부 IP ↔ 외부 IP" 주소를 변환해주는 기술이 NAT(Network Address Translation)이야.
  3. WinNAT의 역할: 윈도우에서 이 NAT 역할을 해주는 서비스가 바로 WinNAT이야. 가상 컴퓨터들이 밖으로 나가는 관문이지.
  4. 포트 예약(알박기)의 이유:
    • 가상 컴퓨터(컨테이너)가 갑자기 수십 개 생길 수도 있잖아?
    • WinNAT은 "아, 저 안쪽 세상(가상환경)에서 트래픽이 엄청 쏟아질 수도 있겠네?" 라고 생각해서, 통신 효율을 위해 미리 외부 포트 수백~수천 개를 '전용 통로'로 예약(Reservation) 해버리는 거야.
    • 마치 아파트 관리실이 "101동 사람들이 택배 많이 시킬지 모르니까 택배 보관함 100칸은 미리 비워둬!" 하고 일반 주민(너의 3000번 포트)이 못 쓰게 막아버린 셈이지.

서론

안녕하세요. 블로그 주인장입니다! 오랜만에 글을 올립니다. 

과거 부트캠프 다닐 당시 블로그 활동을 그래도 열심히 했었습니다(매주 회고록도 올리고 잠깐이지만 코테 공부 정리도 했습니다). 하지만 수료후 취업 준비로 바쁘기도 했고, 귀찮기도 하여 블로그 활동을 안했었습니다. 확실히 강제성이 사라지고 한번 안하기 시작하니 계속 안하게 되더라고요... 결론부터 말씀 드리자면 다시 블로그 활동을 하려고 합니다. 다시 열심히 살고자 기록하는 습관도 들이고 내용 정리를 하기 위해 결심했습니다. 제가 블로그 쓰지 않는 기간에 어떻게 살았는지 간단하게 설명 드리고, 앞으로 블로그는 무엇을 할지 간단하게라도 정리 하는 내용을 아래에 작성하겠습니다.

 

주인장의 근황 및 과거

저는 수료후 바로 공채시장에 뛰어들었습니다. 물론 결과는 처참했습니다. 대부분의 대기업들은 서류탈락... 작은 기업에서는 몇번의 서류합격 연락이 왔었습니다. 서류 합격은 체험형 인턴 포함 총 6번 했습니다. 그중 직무가 맞지 않아 면접 포기 1회, 과제 전형 탈락 1회, 면접 탈락 3회, 면접 합격 1회 로 마무리 되었습니다. 결국 최종 합격은 1회... 하지만 지역이 멀어 제가 사는 곳에서는 2시간이 넘게 걸립니다... 출퇴근 왕복 4~5시간... 그러하여 며칠을 고민한 끝에 한번 더 도전해보자고 하여 그 회사는 가지 않는것으로 결정하였습니다. 그러하여 현재도 취업 준비생 상태입니다... ㅎㅎ;; 

하지만 완전히 성과가 없는 기간은 아니였습니다! 몇번의 면접을 통해 내가 무엇이 부족한지 알게 되었으며(자존감은 낮아진거 같습니다), 내일일경험 인턴 2달도 진행하여 사회생활을 맛을 조금이나마 경험했습니다. 이를 토대로 앞으로 취준을 열심히 하려고 합니다.  또한 개인프로젝트 1개와 팀프로젝트 1개를 진행중입니다. 

운동고 처음엔 꾸준하게 잘 했지만... 팔꿈치를 다치는 바람에 2달이 조금 넘는 시간 동안 운동을 쉬었습니다... 그러하여 체중도 다시 빠지게 되어 암울한 상황입니다. 하지만 이제 슬슬 방치 할 수 없고 팔꿈치도 나은거 같아 오늘(2025.12.22)일부터 다시 운동도 시작하였습니다. 지금 하체운동 후 카페에서 이 글을 작성중입니다!

 

앞으로의 블로그 방향

앞으로 대부분은 공부한거 정리하는 방향으로 작성하려고 합니다. 현재 공부 계획으로는 cs지식 공부, 파이썬 및 ai 관련 지식 공부, 코딩테스트 공부 이렇게 크게 3카테고리로 진행하려고 합니다. 그러하여 이 부분에 대한 블로그 글을 작성 할거 같습니다. 하지만 이중 코딩테스트 글 작성은 현재 고민중입니다.

무슨 요일에 무슨 공부를 하여 글을 올릴지는 아직 미정입니다. 코딩테스트는런 인프런에서 구매한 강의를 보며 따라갈 생각입니다. 2~3바퀴 돌린 후 스스로 학습으로 전향할 생각입니다.  cs지식 공부는 친구가 추천해준 책을 토대로 공부 할 생각입니다(이미 배달 와있습니다). 파이썬 및 ai관련 지식 공부는 면접에서 대답하지 못했던 부분을 먼저 공부 한 후 저의 포트폴리오 기술들 위주 공부를 할 생각힙니다. 또한 관심있는 최신 기술이 생길때마다 공부하거나 새로 알게된 기술들을 정리할 생각입니다. 

일상적인 글은 심심하면 올리거나, 한주 회고정도 작성 할지 말지는 고민해겠습니다.  개인 프로젝트나 팀 프로젝트 마무리 후 정리하는 글도 써보는 방향으로 가겠습니다!

 

마무리

사실 이 글도 제가 다짐을 하기 위한 글입니다(다짐을 남겨둬야 더 열심히하고 조금이지만 강제성이 생길거 같았습니다). 혹시 여기까지 읽어 주셨다면 감사합니다. 앞으로 열심히 공부하며 성장해 나가겠습니다. 

 

오늘의 이모티콘

6개월간의 부트캠프 교육이 끝난지 어느덧 3주가 지났습니다. 이번엔 순수하게 부트캠프를 위한 회고를 작성해볼까 합니다(다른 기수 지원에 관심이 있으신 분들에게 도움이 됐으면 좋겠습니다).

 

커리큘럼

처음으로 커리큘럼에 대한 이야기입니다. 저는 전반적으로 만족합니다. llm, rag, docker 등 현재 취업 준비를 하면서 회사들이 많이들 요구하는 기술들을 배웠습니다. 또한 이 기술들을 메인으로 한 프로젝트들을 수행함으로써 자신의 능력을 기르며 취업에 유리한 경험을 한거 같습니다. 옥에 티로 크롤링과 django는 너무 과거 기술이라 큰 도움은 되지 않았던 거 같습니다. 하지만 완전히 의미 없는 시간은 아니었던 거 같습니다.

 

오프라인의 장점

많은 부트 캠프가 온라인인 경우가 많고, 많은 사람들이 온라인을 선호하기도 합니다(편하기 때문이죠). 하지만 저는 일부러 오프라인을 선택했습니다. 학부생 시절 코로나로 인해 온라인 수업을 할 때 오프라인 수업에 비해 집중력이 현저히 떨어진다는 사실을 느꼈습니다. 또한 혼자 공부하는 것이기 때문에 스스로 기준을 정하고 스스로 타협하여 능률도 좋지 않습니다. 이를 방지하고자 오프라인을 선택하였습니다. 이 선택은 매우 매우 중요한 선택이었던 거 같습니다. 심지어 좋은 학우분들과 친해지게 되어 남아서 공부하는 습관을 들이고, 모르는 것을 함께 고민하며 이겨나가게 되었습니다. 아마 온라인이었으면 흐지부지 수료하고 지금처럼 크게 성장하진 못했을 거라 확신합니다.

 

팀프로젝트

1~4차, 최종 프로젝트까지 총 5번의 프로젝트를 하게 됩니다. 이는 배운 것을 실제로 사용해 볼 수 있는 좋은 기회이며, 사실 이 기간에 많이 성장하는 거 같습니다. 나와 다른 배경의 사람들과 함께 프로젝트를 하며, 그로 인해 다양한 변수가 나옵니다. 이를 해결하는 것 또한 프로그래밍적 실력과 별개로 매우 중요한 경험이고 기술인 거 같습니다. 이 모두를 얻을 수 있는 좋은 기회가 5번이나 있습니다. 심지어 마지막 최종 프로젝트는 거의 2달간 진행하며 수업이 없고 프로젝트만 진행합니다. 이때 프로젝트의 과정 및 협업 방법에 대해 잘 알 수 있을것입니다. 

화목한 저희 최종프로젝트 팀 사진입니다. 저희 서비스 사진에 각 조원들의 사진을 넣었습니다!

 

기타 장점

그 외 SKN AI 부트 캠프의 장점으로는 노트북 및 리소스 자원(open ai, 런팟, aws)를 지원해 줘 빈손으로 와도 모든 과정을 수행할 수 있게 해줍니다. 그리고 매우 중요한 점으로 강사님, 매니저님들이 매우 매우 친전하고 많이 신경 써주십니다. 공적인 분위기가 아닌 친근한 분위기로 대해주셔서 항상 편한 마음으로 다닐 수 있던 거 같습니다. 10시까지 공부할 장소를 제공해 주는 것도 매우 좋은 거 같습니다. 다들 아시다시피 집에 가면 공부를 하지 않습니다. 같은 분야를 공부하는 사람들끼리 10시까지 남아서 공부할 장소를 제공해 주어 매우 좋았던 거 같습니다.

 

교육과정 동안 매주 회고를 작성했습니다. 더 궁금하시면 한번 참고해서 보세요~

 

마지막으로!

강의실에서 찍은 하늘 사진과 옥상에서 찍은 안양천 사진입니다. 가끔 코딩하다 기분전환하고 싶을 때 보면 기분이 좋아집니다.

 

금주의 이모티콘(금주의 나의 상태를 제일 잘 표현하는 이모티콘)

최종 프로젝트 내용

 

1. 금주에 작업한 내용

금주에는 프로젝트 마감 기간으로 마무리를 하는 단계였습니다. 작은 버그와 오류를 찾아 수정하고 배포 후 QA 문서를 작성하는 기간이었습니다. 저희 서비스는 상당히 잘 마무리된 거 같아 기쁩니다.

 

2. 최종 프로젝트 소감

진짜 솔직하게 말해서 부트 캠프 과정 6개월 기간 중 최종 프로젝트 기간이 제일 재미있고 의미 있던 기간이었던 거 같습니다. 조원들 모두 좋은 사람들이 매칭되어 개발하는 기간 내내 웃음과 함께 코딩했던 거 같습니다. 클로드 결제를 통해 진정한 바이브 코딩이 무엇인지도 깨닫고, 그로 인해 능률도 많이 올라갔던 거 같습니다. 또한 협업에 대해서도 느끼고, 다른 조들의 불화를 보고 협업의 무서움 또한 느꼈습니다. 좋은 멘토님과의 만남을 통해 프로젝트 방법뿐만 아니라 인생 및 사회, 취업에 대한 좋은 정보들도 얻을 수 있는 기회였던 거 같습니다.

 

최종 소감(6개월 부트캠프 소감)

처음 부트 캠프에 왔을 때만 해도 6개월이라는 기간이 길게 느껴졌고, 수료하는 앞 기수 사람들을 보면서 나도 저런 순간이 오겠지라는 생각을 했습니다. 열심히 수업을 듣고 코딩 테스트도 연습하며, 여기 사람들과 잘 어울리다 보니 순식간에 최종 프로젝트 기간이 되고, 프로젝트 끝난 후 수료식을 하고 있던 거 같습니다. 6개월 동안 미친 듯이 열심히 살았다고는 말하지 못하지만, 나름 의미 있는 시간이었고 뿌듯한 6개월이 되게끔 열심히 산거 같습니다. 원래는 앞 기수로 지원을 했었는데 졸업식을 하고 들어가고 싶어 미뤘던 거였습니다. 이게 정말 중요한 선택이었을 수도 있을 거 같습니다. 덕분에 매우 좋은 학우분들과 강사님, 매니저님, 멘토님과 함께 작업을 할 수 있었던 거 같습니다. 저는 이 부분이 정말 다행이고 큰 행운이라고 생각합니다. 덕분에 큰 탈 없이 잘 수료하였고 많이 성장된 모습으로 나가는 거 같습니다. 사실 조금 슬픕니다. 제 인생에서 이런 좋은 분위기와 좋은 사람들과 함께할 기회가 많이 남지 않았을 수도 있다는 생각이 듭니다. 수료를 했으니 취업 준비를 하고 회사를 가면 아무래도 지금과는 다른 분위기와 느낌이지 않을까 합니다. 그래도 졸지 않고 열심히 나아가 볼 생각입니다. 앞으로도 블로그는 계속 써볼 생각입니다. 지켜봐 주십쇼.

 

금주의 이모티콘(금주의 나의 상태를 제일 잘 표현하는 이모티콘)

좋았던 점 

슬슬 개발이 마무리가 되어가고 있습니다. 큰 버그들도 거의 다 잡혔습니다.  또한 몸무게가 많이 증가 하였습니다. 이제 bmi가 20을 넘었습니다. 잘 유지해야 할거 같습니다. 

 

아쉬웠던 점

배포 후 tts 사운드가 안나오는 버그가 발생하였습니다. 제가 작성한 코드가 아니라 함부러 만들기 좀 머하여 다음주에 손 보기로 했습니다. 다음주 월요일 주간 발표자를 정하는 핀볼에 제가 당첨되어 발표를 하게 되었습니다. 발표를 하면 얼굴이 빨개지는것이 있어 발표를 싫어하지만 공정한 방법으로 결정된 일이니 준비 하고 있습니다. 

 

인상적이었던 점

몸무게가 늘었지만 먼가 찝찝합니다. 요즘 바뻐서 운동을 쉬는중인데 늘었습니다. 잦은 야식과 아이스크림 할인점 지나갈때마다 먹어서 증량된거 같아 살짝 걱정입니다. 

이번주 주말이 수료전 마지막 주말이였습니다. 처음에는 6개월이라는 시간이 길거 같이 느껴졌는데 막상 다음주가 수료라는걸 생각하면 시간이 금방 간거 같습니다. 힘든 순간도 많았고 재밌는 순간도 많았고 배운것도 많았던거 같습니다. 좋은 인연들도 많이 만났고 행복한 시간이였던거 같습니다. 앞으로는 취업 준비를 해야하고, 그 후는 취업후는 회사 생활을 해야하기 때문에 이번 부트캠프가 마지막 학생 생활이고 같은 신분의 사람들과 웃고 웃으며 생활하는 시간이지 않을까 합니다. 나름 잘 보낸거 같습니다. 

 

최종 프로젝트 내용

 

1. 금주에 작업한 내용

저는 금주에 배포와 버그 잡는것을 담당하였습니다. 코드가 업데이트 될때마다 재배포를 진행하였고, 이에 발생하는 오류를 해결하는 역할이 주요 업무 였습니다. 시간이 남는 경우에는 발견된 버그들을 수정하였습니다. 

 

2. 앞으로 해야할 일(주말 및 다음주)

현재 배포 과정에서 tts 사운드가 출력되지 않는 현상이 발생하였습니다. 월요일날 가서 코드를 합친 후 해결하려고 합니다. 그 후에는 계속 금주 하였던거 처럼 배포 및 버그 해결을 할 거 같습니다. 

 

금주의 이모티콘(금주의 나의 상태를 제일 잘 표현하는 이모티콘)

+ Recent posts