Skip to content

자바 개발자의 AI 입문기 (5편) LangGraph 상태 기반 워크플로우

시리즈: 자바 개발자의 AI 입문기

5 / 5편
  1. 11편 - Python 환경세팅과 첫 API 호출
  2. 22편 - LangChain 기초
  3. 33편 - RAG 기초
  4. 44편 - RAG 실전
  5. 55편 - LangGraph지금 읽는 중
다른 시리즈 전체 보기 →
목차

LangGraph는 AI 호출을 노드(작업), 엣지(연결), 상태(데이터)로 짜는 상태 기반 워크플로우 프레임워크입니다. 다단계 처리와 조건부 분기, 도구를 쓰는 에이전트에 맞고, 자바 개발자에게는 Spring Batch의 Job/Step 구조와 비슷하게 읽힙니다. 노드가 늘어나면 디버깅이 어려워지고, 단순한 선형 흐름이라면 체이닝이 더 적합합니다.





이전 편 요약

4편에서는 RAG 파이프라인을 완성하고, 문서 로딩과 청킹, 인덱싱을 거쳐 Knowledge Base 검색 챗봇까지 만들었습니다. 이번 편에서는 LangGraph를 배웁니다. 시리즈의 마지막 편입니다.





LangGraph가 뭔가요?

한 줄로 말하면 AI 호출을 상태 머신처럼 관리하는 프레임워크입니다.

4편에서 만든 RAG 챗봇은 단순했어요:

질문 → 검색 → 답변

근데 실제 서비스는 더 복잡해요:

질문 분석 
  → 카테고리 분류 (인사? 기술? 일반?)
  → 해당 DB에서 검색
  → 답변 생성
  → 답변 검증 (이상하면 다시 생성)
  → 최종 응답

이런 복잡한 흐름을 깔끔하게 관리하려면 LangGraph가 필요합니다.



자바 개발자를 위한 비유

LangGraphJava 비유
GraphSpring Batch Job
NodeStep, Tasklet
EdgeFlow, Transition
StateExecutionContext
Conditional EdgeFlowExecutionStatus 분기

Spring Batch를 써 보셨다면 익숙한 구조입니다. Job이 여러 Step으로 구성되고, Step 결과에 따라 다음 Step이 결정됩니다.





LangGraph 설치

pip install langgraph




핵심 개념: 노드, 엣지, 상태

노드 (Node)

“하나의 작업 단위”입니다.

def analyze_question(state):
    """질문을 분석하는 노드"""
    question = state["question"]
    # 분석 로직...
    return {"category": "hr", "question": question}

엣지 (Edge)

“노드 간 연결”입니다.

# A 노드 실행 후 B 노드로 이동
graph.add_edge("A", "B")

# 조건부 이동
graph.add_conditional_edges("A", route_function, {
    "hr": "hr_search",
    "tech": "tech_search"
})

상태 (State)

“노드 간 공유 데이터”입니다.

from typing import TypedDict

class State(TypedDict):
    question: str
    category: str
    search_results: list
    answer: str




실습: 카테고리별 검색 챗봇

간단한 예제로 시작해봅시다.

요구사항:

  1. 질문을 분석해서 카테고리 분류 (인사 / 기술 / 기타)
  2. 카테고리에 맞는 검색 수행
  3. 검색 결과 기반 답변 생성

1단계: 상태 정의

from typing import TypedDict, Literal

class ChatState(TypedDict):
    question: str
    category: Literal["hr", "tech", "general"]
    context: str
    answer: str

2단계: 노드 함수 정의

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

load_dotenv()

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)


def classify_question(state: ChatState) -> ChatState:
    """질문을 카테고리로 분류"""
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", """질문을 분류하세요.
        - hr: 휴가, 급여, 복지, 인사 관련
        - tech: 기술, 개발, 도구 관련
        - general: 그 외
        
        반드시 hr, tech, general 중 하나만 출력하세요."""),
        ("user", "{question}")
    ])
    
    chain = prompt | llm | StrOutputParser()
    category = chain.invoke({"question": state["question"]}).strip().lower()
    
    # 유효한 카테고리인지 확인
    if category not in ["hr", "tech", "general"]:
        category = "general"
    
    return {"category": category}


def search_hr(state: ChatState) -> ChatState:
    """인사 관련 검색 (시뮬레이션)"""
    # 실제로는 Vector DB 검색
    hr_docs = {
        "휴가": "연차 15일, 3년차부터 20일",
        "급여": "매월 25일 지급",
        "복지": "점심 식대 지원, 자기계발비 연 100만원"
    }
    
    question = state["question"]
    context = next((v for k, v in hr_docs.items() if k in question), 
                   "관련 인사 정책을 찾을 수 없습니다.")
    
    return {"context": context}


def search_tech(state: ChatState) -> ChatState:
    """기술 관련 검색 (시뮬레이션)"""
    tech_docs = {
        "배포": "GitHub Actions + ArgoCD 사용",
        "모니터링": "Datadog 대시보드 참고",
        "코드리뷰": "PR 올리면 2명 이상 승인 필요"
    }
    
    question = state["question"]
    context = next((v for k, v in tech_docs.items() if k in question),
                   "관련 기술 문서를 찾을 수 없습니다.")
    
    return {"context": context}


def search_general(state: ChatState) -> ChatState:
    """일반 검색"""
    return {"context": "일반 문의는 총무팀에 연락해주세요."}


def generate_answer(state: ChatState) -> ChatState:
    """최종 답변 생성"""
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", """참고 정보를 바탕으로 친절하게 답변하세요.
        
        참고 정보:
        {context}"""),
        ("user", "{question}")
    ])
    
    chain = prompt | llm | StrOutputParser()
    answer = chain.invoke({
        "context": state["context"],
        "question": state["question"]
    })
    
    return {"answer": answer}

3단계: 그래프 구성

from langgraph.graph import StateGraph, END

def route_by_category(state: ChatState) -> str:
    """카테고리에 따라 다음 노드 결정"""
    category = state.get("category", "general")
    return category


# 그래프 생성
workflow = StateGraph(ChatState)

# 노드 추가
workflow.add_node("classify", classify_question)
workflow.add_node("hr", search_hr)
workflow.add_node("tech", search_tech)
workflow.add_node("general", search_general)
workflow.add_node("answer", generate_answer)

# 시작점 설정
workflow.set_entry_point("classify")

# 조건부 엣지: 분류 결과에 따라 검색 노드 선택
workflow.add_conditional_edges(
    "classify",
    route_by_category,
    {
        "hr": "hr",
        "tech": "tech",
        "general": "general"
    }
)

# 검색 노드들 → 답변 노드로
workflow.add_edge("hr", "answer")
workflow.add_edge("tech", "answer")
workflow.add_edge("general", "answer")

# 답변 노드 → 종료
workflow.add_edge("answer", END)

# 컴파일
app = workflow.compile()

4단계: 실행

# 테스트
questions = [
    "연차 며칠 받아요?",
    "배포는 어떻게 하나요?",
    "회사 주소가 어디예요?"
]

for q in questions:
    result = app.invoke({"question": q})
    print(f"Q: {q}")
    print(f"카테고리: {result['category']}")
    print(f"A: {result['answer']}\n")

결과:

Q: 연차 며칠 받아요?
카테고리: hr
A: 연차 휴가는 입사 1년 후 15일이 부여되고, 3년차부터는 20일로 늘어납니다.

Q: 배포는 어떻게 하나요?
카테고리: tech
A: 배포는 GitHub Actions와 ArgoCD를 사용합니다. 자세한 내용은 기술 문서를 참고해주세요.

Q: 회사 주소가 어디예요?
카테고리: general
A: 일반 문의는 총무팀에 연락해주세요.

질문 종류에 따라 다른 경로를 타고, 적절한 답변이 생성됐어요.





그래프 시각화

LangGraph는 그래프를 시각화할 수 있어요.

from IPython.display import Image, display

# Mermaid 다이어그램 생성
print(app.get_graph().draw_mermaid())

출력:

graph TD
    __start__ --> classify
    classify --> hr
    classify --> tech
    classify --> general
    hr --> answer
    tech --> answer
    general --> answer
    answer --> __end__

Spring Batch의 Job 흐름도와 비슷하죠?





실전 활용: 재시도 로직 추가

답변이 이상하면 다시 생성하는 로직을 추가해봅시다.

def validate_answer(state: ChatState) -> str:
    """답변이 유효한지 검증"""
    answer = state.get("answer", "")
    
    # 답변이 너무 짧거나 "모르겠다"로 끝나면 재시도
    if len(answer) < 20 or "모르겠" in answer:
        return "retry"
    return "done"


# 재시도 노드
def regenerate_answer(state: ChatState) -> ChatState:
    """더 구체적인 프롬프트로 재생성"""
    # ... 재생성 로직
    return {"answer": "재생성된 답변..."}


# 조건부 엣지 추가
workflow.add_conditional_edges(
    "answer",
    validate_answer,
    {
        "retry": "regenerate",
        "done": END
    }
)

이런 식으로 복잡한 워크플로우를 깔끔하게 표현할 수 있어요.





LangGraph vs LangChain 체이닝

상황추천
단순한 순차 처리LangChain 체이닝 (| 연산자)
조건부 분기 필요LangGraph
반복/재시도 로직LangGraph
여러 에이전트 협업LangGraph
상태 관리 필요LangGraph

4편에서 만든 RAG 챗봇은 체이닝으로 충분했어요. 근데 “카테고리별 분기”, “답변 검증”, “재시도” 같은 게 필요하면 LangGraph가 적합합니다.





시리즈 마무리

5편에 걸쳐 AI 개발의 기초를 다뤘습니다.

편주제배운 것
1편환경 세팅Python, Cursor, OpenAI API
2편LangChain프롬프트 템플릿, 체이닝
3편RAG 기초Embedding, Vector DB
4편RAG 실전문서 로딩, 청킹, 챗봇
5편LangGraph상태 기반 워크플로우

여기까지 따라오셨다면 사내 문서 검색 챗봇, 코드 리뷰 자동화 도구, FAQ 자동 응답 시스템 같은 것도 해 볼 만합니다. 더 깊게 가려면 다음 순서를 생각해 볼 수 있습니다.

  1. RAG 심화: Hybrid Search, Re-ranking, Query Expansion
  2. Agent 심화: Tool Use, Function Calling, 멀티 에이전트
  3. 프로덕션화: 캐싱, 로깅, 모니터링, 비용 최적화
  4. Fine-tuning: 도메인 특화 모델 학습 (Level 5)

참고 자료로는 LangChain 공식 튜토리얼, “Building LLM Apps”(온라인 강의), 한국어 AI 커뮤니티(디스코드, 슬랙 등)가 있습니다.

처음엔 “이거 자바로 못하나?” 싶었는데 Python으로 해 보니 생각보다 쉬웠고, Cursor IDE 덕분에 문법을 몰라도 바이브코딩으로 어떻게든 됐습니다. 백엔드 경험이 있으면 시스템 설계 감각, API 구조 이해, 프로덕션 배포 경험이 그대로 도움이 됩니다. 이 시리즈가 AI 입문의 시작점이 됐으면 좋겠습니다.

글을 따라 하신 뒤에는 classify 노드가 hr, tech, general 중 하나만 반환하는지, 미분류 카테고리가 general로 처리되는지, app.get_graph().draw_mermaid() 출력이 의도한 구조인지만 확인해 보시면 됩니다.





참고:

LangGraph 공식 문서: https://langchain-ai.github.io/langgraph/
LangGraph 튜토리얼: https://python.langchain.com/docs/langgraph
Agent 개념: https://python.langchain.com/docs/concepts/agents




읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github