자바 개발자의 AI 입문기 (5편) LangGraph 상태 기반 워크플로우
시리즈: 자바 개발자의 AI 입문기
5 / 5편
- 11편 - Python 환경세팅과 첫 API 호출
- 22편 - LangChain 기초
- 33편 - RAG 기초
- 44편 - RAG 실전
- 55편 - LangGraph지금 읽는 중
목차
LangGraph는 AI 호출을 노드(작업), 엣지(연결), 상태(데이터)로 짜는 상태 기반 워크플로우 프레임워크입니다. 다단계 처리와 조건부 분기, 도구를 쓰는 에이전트에 맞고, 자바 개발자에게는 Spring Batch의 Job/Step 구조와 비슷하게 읽힙니다. 노드가 늘어나면 디버깅이 어려워지고, 단순한 선형 흐름이라면 체이닝이 더 적합합니다.
이전 편 요약
4편에서는 RAG 파이프라인을 완성하고, 문서 로딩과 청킹, 인덱싱을 거쳐 Knowledge Base 검색 챗봇까지 만들었습니다. 이번 편에서는 LangGraph를 배웁니다. 시리즈의 마지막 편입니다.
LangGraph가 뭔가요?
한 줄로 말하면 AI 호출을 상태 머신처럼 관리하는 프레임워크입니다.
4편에서 만든 RAG 챗봇은 단순했어요:
질문 → 검색 → 답변근데 실제 서비스는 더 복잡해요:
질문 분석
→ 카테고리 분류 (인사? 기술? 일반?)
→ 해당 DB에서 검색
→ 답변 생성
→ 답변 검증 (이상하면 다시 생성)
→ 최종 응답이런 복잡한 흐름을 깔끔하게 관리하려면 LangGraph가 필요합니다.
자바 개발자를 위한 비유
| LangGraph | Java 비유 |
|---|---|
| Graph | Spring Batch Job |
| Node | Step, Tasklet |
| Edge | Flow, Transition |
| State | ExecutionContext |
| Conditional Edge | FlowExecutionStatus 분기 |
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단계: 상태 정의
from typing import TypedDict, Literal
class ChatState(TypedDict):
question: str
category: Literal["hr", "tech", "general"]
context: str
answer: str2단계: 노드 함수 정의
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 자동 응답 시스템 같은 것도 해 볼 만합니다. 더 깊게 가려면 다음 순서를 생각해 볼 수 있습니다.
- RAG 심화: Hybrid Search, Re-ranking, Query Expansion
- Agent 심화: Tool Use, Function Calling, 멀티 에이전트
- 프로덕션화: 캐싱, 로깅, 모니터링, 비용 최적화
- 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
