가끔 보자, 하늘.

LangGraph와 MCP를 사용한 서비스 구축 본문

개발 이야기/개발 및 서비스

LangGraph와 MCP를 사용한 서비스 구축

가온아 2025. 8. 13. 09:00
Spring Boot API와 LangGraph를 활용한 로컬 AI 에이전트 구축 실전 가이드

자체 서비스에 MCP를 도입해 보세요.

당신의 서비스에 MCP 서버와 클라이언트를 만들고 LangGraph를 이용해 서비스를 확장해 보세요.

Spring Boot API와 LangGraph를 활용한 로컬 AI 에이전트 구축 실전 가이드

서론: 프로젝트 개요 및 기술 스택 소개

최근 대규모 언어 모델(LLM)의 발전은 단순한 챗봇을 넘어, 외부 도구와 상호작용하며 복잡한 문제를 해결하는 'AI 에이전트'라는 새로운 패러다임을 열었다. 본 문서는 이러한 AI 에이전트를 직접 구축하고자 하는 개발자를 위한 실전 가이드이다. 클라우드 기반의 값비싼 API에 의존하는 대신, 로컬 환경에서 모든 것을 제어할 수 있는 프라이빗 AI 시스템 구축에 초점을 맞춘다.

목표 및 핵심 컨셉

이 글의 최종 목표는 사용자의 자연어 질문(예: "15와 3을 곱하면 얼마야?")을 이해하고, 외부 API를 호출하여 계산을 수행한 뒤, 그 결과를 사용자에게 자연스럽게 제공하는 지능형 AI 에이전트를 구축하는 것이다. 이를 위해 다음과 같은 핵심 컨셉을 적용한다.

  • 분리된 아키텍처 (Separated Architecture): 안정성이 중요한 비즈니스 로직(사칙연산)은 검증된 백엔드 프레임워크인 Java Spring Boot로 구현한다. 반면, 유연한 추론과 도구 사용 결정은 Python 기반의 AI 에이전트 프레임워크인 LangGraph가 담당하도록 역할을 명확히 분리한다. 이는 시스템의 유지보수성과 확장성을 극대화하는 현대적인 소프트웨어 설계 원칙이다.
  • 로컬 LLM 활용 (Private AI): Ollama와 Google의 경량 모델인 Gemma를 사용하여 외부 클라우드 서비스 의존 없이 개인 PC 환경에서 모든 AI 추론을 수행한다. 이를 통해 데이터 프라이버시를 확보하고, API 비용 부담 없이 무제한으로 실험할 수 있는 환경을 구축한다.
  • MCP (Model-Context-Protocol) 정의: 에이전트와 API 서버 간의 명확하고 정형화된 소통을 위해 자체적인 JSON 규약인 'MCP'를 정의한다. 이는 LLM이 생성하는 모호한 자연어를 기계가 이해할 수 있는 명확한 명령으로 변환하는 핵심적인 과정이며, 안정적인 도구 사용의 기반이 된다.

기술 스택 요약

본 프로젝트에서 사용할 주요 기술 스택은 다음과 같다.

  • 백엔드 API 서버: Java 17, Spring Boot 3.x, Gradle
  • AI 에이전트 프레임워크: Python 3.10+, LangGraph
  • 로컬 LLM 환경: Ollama, Gemma 2B
  • 통신 프로토콜: 자체 정의 MCP (JSON 기반)
  • 개발 도구: VSCode 또는 IntelliJ, Postman 또는 HTTP Client

글의 전체적인 흐름은 (1) Spring Boot API 서버 구축 → (2) Ollama 및 LangGraph 에이전트 개발 → (3) 두 시스템 연동 및 최종 테스트 → (4) LangGraph 심층 분석 순으로 진행될 것이다.

1부: 백엔드 구축 - Spring Boot 사칙연산 API 서버 개발

AI 에이전트가 사용할 '도구(Tool)'는 신뢰할 수 있고 안정적이어야 한다. 이번 장에서는 검증된 엔터프라이즈 프레임워크인 Java Spring Boot를 사용하여 견고한 사칙연산 API 서버를 구축하는 과정을 상세히 다룬다.

프로젝트 환경 설정

가장 먼저 Spring Initializr를 통해 프로젝트의 뼈대를 생성한다. 설정은 다음과 같이 지정한다.

  • Project: Gradle - Groovy
  • Language: Java
  • Spring Boot: 3.x.x
  • Project Metadata:
    • Group: com.example
    • Artifact: mcp-server
    • Packaging: Jar
    • Java: 17
  • Dependencies: Spring Web, Lombok

생성된 프로젝트를 다운로드하여 VSCode나 IntelliJ와 같은 IDE에서 연다. Lombok은 DTO나 엔티티 클래스에서 반복적인 Getter, Setter, 생성자 코드를 어노테이션으로 대체하여 코드를 간결하게 만들어준다.

MCP(Model-Context-Protocol) 설계

에이전트와 서버 간의 원활한 통신을 위해 명확한 규약, 즉 프로토콜을 정의하는 것은 매우 중요하다. 우리는 이를 MCP(Model-Context-Protocol)라 명명하고, JSON 형식을 사용한다.

요청(Request) 형식

에이전트가 서버에 계산을 요청할 때 사용할 형식이다. 어떤 연산을 원하는지(operation)와 계산에 필요한 숫자들(operands)을 명시한다.

{
  "operation": "add",
  "operands": [10, 5]
}

응답(Response) 형식

서버가 에이전트에게 결과를 반환할 때 사용할 형식이다. 성공 여부(status)를 명시하고, 성공 시 결과(result)를, 실패 시 에러 메시지(message)를 포함한다.

// 성공 시
{
  "status": "success",
  "result": 15
}

// 실패 시
{
  "status": "error",
  "message": "Division by zero is not allowed."
}

이처럼 정형화된 프로토콜은 LLM의 예측 불가능성을 제어하고 시스템의 안정성을 높이는 핵심 요소이다.

API 컨트롤러 및 서비스 구현

이제 MCP 규약을 기반으로 실제 코드를 작성한다. 먼저 요청과 응답 데이터를 담을 DTO(Data Transfer Object) 클래스를 생성한다.

// CalculationRequest.java
import java.util.List;
import lombok.Data;

@Data
public class CalculationRequest {
    private String operation;
    private List<Double> operands;
}

// CalculationResponse.java
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class CalculationResponse {
    private String status;
    private Double result;
    private String message;
}

다음으로, HTTP 요청을 받아 처리하는 CalculatorController를 작성한다. @PostMapping을 통해 /calculate 엔드포인트를 생성하고, @RequestBody로 JSON 요청을 CalculationRequest 객체에 매핑한다.

// CalculatorController.java
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class CalculatorController {

    private final CalculationService calculationService;

    public CalculatorController(CalculationService calculationService) {
        this.calculationService = calculationService;
    }

    @PostMapping("/calculate")
    public CalculationResponse calculate(@RequestBody CalculationRequest request) {
        return calculationService.performCalculation(request);
    }
}

실제 계산 로직은 CalculationService 클래스로 분리하여 컨트롤러의 책임을 덜어준다. 이 서비스 클래스는 요청된 operation에 따라 적절한 계산을 수행하고 예외 상황을 처리한다.

// CalculationService.java
import org.springframework.stereotype.Service;

@Service
public class CalculationService {
    public CalculationResponse performCalculation(CalculationRequest request) {
        if (request.getOperands() == null || request.getOperands().size() != 2) {
            return CalculationResponse.builder().status("error").message("Exactly two operands are required.").build();
        }

        double a = request.getOperands().get(0);
        double b = request.getOperands().get(1);

        try {
            double result = switch (request.getOperation()) {
                case "add" -> a + b;
                case "subtract" -> a - b;
                case "multiply" -> a * b;
                case "divide" -> {
                    if (b == 0) {
                        throw new IllegalArgumentException("Division by zero is not allowed.");
                    }
                    yield a / b;
                }
                default -> throw new IllegalArgumentException("Invalid operation: " + request.getOperation());
            };
            return CalculationResponse.builder().status("success").result(result).build();
        } catch (IllegalArgumentException e) {
            return CalculationResponse.builder().status("error").message(e.getMessage()).build();
        }
    }
}

단독 서버 테스트

AI 에이전트와 연동하기 전에, API 서버가 독립적으로 완벽하게 동작하는지 검증하는 것은 필수이다. Postman이나 VSCode의 REST Client, 또는 터미널의 curl 명령어를 사용하여 테스트할 수 있다.

# 덧셈 테스트
curl -X POST http://localhost:8080/api/calculate \
-H "Content-Type: application/json" \
-d '{
  "operation": "add",
  "operands": [20, 22]
}'

# 0으로 나누기 예외 테스트
curl -X POST http://localhost:8080/api/calculate \
-H "Content-Type: application/json" \
-d '{
  "operation": "divide",
  "operands": [10, 0]
}'

예상된 성공 및 실패 응답이 정확히 반환되는 것을 확인했다면, 이제 AI 에이전트를 구축할 준비가 된 것이다.

2부: AI 에이전트 구축 - Ollama, Gemma 그리고 LangGraph

이 장에서는 프로젝트의 핵심인 AI 에이전트를 구축한다. 사용자의 자연어 입력을 해석하고, 1부에서 만든 API를 '도구'로 사용하여 문제를 해결하는 지능적인 로직을 LangGraph를 통해 구현하는 과정을 상세히 다룬다.

로컬 LLM 환경 준비

먼저 로컬에서 LLM을 구동할 환경을 설정한다. Ollama 공식 홈페이지에서 자신의 운영체제에 맞는 버전을 다운로드하여 설치한다. 설치가 완료되면 터미널에서 다음 명령어를 실행하여 경량 고성능 모델인 Gemma를 다운로드한다.

ollama pull gemma:2b

ollama run gemma:2b 명령어로 모델이 정상적으로 실행되는지 확인한다. 이제 로컬 환경에서 자유롭게 사용할 수 있는 강력한 언어 모델을 갖추게 되었다.

LangGraph 프로젝트 설정

Python 프로젝트를 위한 가상환경을 생성하고, 필요한 라이브러리를 설치한다.

python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install langgraph langchain-community langchain-core requests ollama

LangGraph의 핵심: 상태 기반 그래프 설계

LangGraph는 '상태(State)'를 중심으로 작동하는 그래프 기반 프레임워크이다. 에이전트의 작업 흐름을 노드(Node)와 엣지(Edge)의 연결로 명시적으로 정의한다.

1. 상태(State) 정의

그래프의 각 노드가 공유하고 업데이트할 데이터 구조인 AgentState를 정의한다. Python의 TypedDict를 사용하여 각 필드의 타입을 명시한다.

from typing import List, TypedDict, Annotated
from langchain_core.messages import BaseMessage
import operator

class AgentState(TypedDict):
    messages: Annotated[List[BaseMessage], operator.add]

messages 필드는 사용자의 질문, LLM의 응답, 도구 실행 결과 등 모든 대화 기록을 순차적으로 저장하는 역할을 한다.

2. 도구(Tool) 및 LLM 정의

에이전트가 사용할 '계산기' 도구를 정의한다. 이 함수는 LLM이 호출할 수 있는 형태로 만들어지며, 내부적으로는 1부에서 만든 Spring Boot API와 통신한다. 이 부분이 바로 **MCP 클라이언트**의 실제 구현체이다.

import requests
import json
from langchain_core.tools import tool

API_URL = "http://localhost:8080/api/calculate"

@tool
def calculator(operation: str, operands: List[float]) -> str:
    """
    Performs arithmetic calculation. 
    'operation' can be 'add', 'subtract', 'multiply', 'divide'.
    'operands' must be a list of exactly two numbers.
    """
    try:
        mcp_request = {"operation": operation, "operands": operands}
        response = requests.post(API_URL, json=mcp_request)
        response.raise_for_status() # HTTP 에러 발생 시 예외 처리
        
        mcp_response = response.json()
        if mcp_response.get("status") == "success":
            return json.dumps({"result": mcp_response.get("result")})
        else:
            return json.dumps({"error": mcp_response.get("message")})
    except requests.exceptions.RequestException as e:
        return json.dumps({"error": f"API connection error: {e}"})

tools = [calculator]

# 로컬 Ollama Gemma 모델을 LangChain과 연동
from langchain_community.chat_models import ChatOllama

llm = ChatOllama(model="gemma:2b", temperature=0)
# LLM이 도구를 사용할 수 있도록 바인딩
llm_with_tools = llm.bind_tools(tools)
주목할 점: calculator 함수의 docstring은 LLM에게 이 도구의 사용법을 알려주는 중요한 역할을 한다. LLM은 이 설명을 보고 언제, 어떻게 이 도구를 호출할지 결정한다.

3. 노드(Node)와 엣지(Edge) 정의

이제 그래프를 구성할 핵심 요소인 노드와 엣지를 정의한다.

  • agent 노드 (결정자): 현재 상태(대화 기록)를 받아 LLM을 호출하고, 다음 행동(도구 호출 또는 최종 답변)을 결정한다.
  • tool_executor 노드 (실행자): agent가 도구 호출을 결정하면, 실제로 도구를 실행하고 그 결과를 상태에 추가한다.
  • 조건부 엣지 (Conditional Edge): agent 노드의 결과에 따라 tool_executor로 갈지, 아니면 그래프를 종료할지 경로를 결정한다.
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolExecutor

tool_executor = ToolExecutor(tools)

# Agent 노드: LLM을 호출하여 다음 행동 결정
def agent_node(state):
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

# Tool Executor 노드: 도구를 실행
def tool_node(state):
    tool_calls = state["messages"][-1].tool_calls
    tool_results = tool_executor.batch([
        {"type": "tool_call", "id": call["id"], "args": call["args"]} for call in tool_calls
    ])
    # 결과를 ToolMessage 형태로 변환하여 상태에 추가
    from langchain_core.messages import ToolMessage
    tool_messages = [
        ToolMessage(content=json.dumps(res), tool_call_id=call["id"])
        for res, call in zip(tool_results, tool_calls)
    ]
    return {"messages": tool_messages}

# 조건부 엣지: 다음 경로 결정
def should_continue(state):
    if state["messages"][-1].tool_calls:
        return "tool" # 도구 호출이 있으면 tool_node로
    else:
        return END # 없으면 종료

이 순환 구조(Agent → Tool → Agent)가 바로 복잡한 문제를 해결하는 LangGraph 에이전트의 핵심적인 작동 방식이다.

그래프 컴파일 및 실행

정의된 요소들을 StateGraph에 추가하고 컴파일하여 실행 가능한 에이전트를 만든다.

# 그래프 워크플로우 정의
workflow = StateGraph(AgentState)

workflow.add_node("agent", agent_node)
workflow.add_node("tool", tool_node)

workflow.set_entry_point("agent")

workflow.add_conditional_edges(
    "agent",
    should_continue,
    {
        "tool": "tool",
        "end": END,
    },
)
workflow.add_edge("tool", "agent")

# 그래프 컴파일
app = workflow.compile()

# 실행
from langchain_core.messages import HumanMessage

inputs = {"messages": [HumanMessage(content="34와 56을 더하면 결과가 어떻게 되나요?")]}
for event in app.stream(inputs, stream_mode="values"):
    print("--- 상태 업데이트 ---")
    event["messages"][-1].pretty_print()

위 코드를 실행하면, 에이전트가 사용자의 질문을 받고(HumanMessage), 계산기 도구를 호출해야겠다고 판단하며(AIMessage with tool_calls), 실제로 도구를 실행하여 API 서버로부터 결과를 받아(ToolMessage), 최종적으로 사용자에게 답변하는 전체 과정을 단계별로 확인할 수 있다.

3부: 시스템 통합 및 최종 테스트

이제 독립적으로 개발된 두 시스템, 즉 Spring Boot API 서버와 LangGraph 에이전트를 함께 실행하여 전체 워크플로우가 의도대로 작동하는지 검증한다. 이 단계는 시스템의 잠재적인 문제를 발견하고 해결하는 중요한 과정이다.

실행 전 체크리스트

엔드투엔드 테스트를 시작하기 전에, 다음 사항들을 반드시 확인해야 한다.

  1. Spring Boot API 서버 실행: IDE나 터미널을 통해 1부에서 개발한 Spring Boot 애플리케이션을 실행한다. 콘솔 로그에 "Tomcat started on port(s): 8080"과 같은 메시지가 출력되는지 확인한다.
  2. Ollama 서비스 실행: Ollama가 백그라운드에서 정상적으로 실행 중인지 확인한다. 터미널에서 ollama list 명령어를 실행했을 때, gemma:2b 모델이 목록에 나타나야 한다.
  3. 포트 및 URL 확인: LangGraph의 Python 코드(calculator 도구)에 정의된 API_URL(http://localhost:8080/api/calculate)이 실행 중인 Spring Boot 서버의 주소와 일치하는지 다시 한번 확인한다.

엔드투엔드 시나리오 시연

모든 준비가 완료되었다면, 2부에서 작성한 LangGraph Python 스크립트를 실행하고 다양한 질문을 입력하여 시스템의 반응을 테스트한다.

디버깅 및 로그 분석 팁

문제 발생 시 원인을 효과적으로 찾기 위해, Spring Boot 서버의 콘솔 로그와 LangGraph 스크립트의 출력 로그를 나란히 놓고 비교 분석하는 것이 매우 유용하다. 데이터의 흐름을 시각적으로 추적할 수 있기 때문이다.

  1. (Python) LangGraph 출력: agent 노드가 생성한 tool_calls 내용을 확인한다. MCP JSON 요청 형식이 올바른가?
  2. (Java) Spring Boot 로그: 들어온 HTTP POST 요청과 그 내용을 확인한다. Python에서 보낸 요청이 정확히 수신되었는가?
  3. (Java) Spring Boot 로그: 서비스 로직 처리 후 반환하는 응답(성공 또는 에러)을 확인한다.
  4. (Python) LangGraph 출력: tool_node가 받은 API 응답(ToolMessage) 내용을 확인한다. Java 서버의 응답이 정확히 전달되었는가?

이 과정을 통해 통신 오류, 데이터 형식 불일치, 로직 에러 등 다양한 문제의 원인을 특정할 수 있다.

성공 사례

가장 기본적인 시나리오이다. 사용자의 질문이 명확한 사칙연산을 요구할 때, 에이전트는 정확히 도구를 호출하고 계산 결과를 반환해야 한다.

  • 입력: "100에서 25를 빼면 얼마인가요?"
  • 예상 흐름:
    1. LLM이 'subtract' 연산과 피연산자 [100, 25]를 식별한다.
    2. agent 노드가 calculator(operation='subtract', operands=[100, 25]) 호출을 생성한다.
    3. tool_node가 Spring Boot API에 MCP 요청을 보낸다.
    4. API 서버가 {"status": "success", "result": 75}를 반환한다.
    5. LLM이 결과를 바탕으로 "100에서 25를 빼면 75입니다."와 같은 최종 답변을 생성한다.

경계 사례 (Edge Case)

시스템의 견고성을 확인하기 위해 의도적으로 예외적인 상황을 테스트한다.

  • 입력: "10을 0으로 나눠봐."
  • 예상 흐름:
    1. LLM이 'divide' 연산과 [10, 0]을 식별하고 도구 호출을 생성한다.
    2. tool_node가 API 서버에 요청을 보낸다.
    3. Spring Boot의 CalculationService에서 0으로 나누는 예외를 감지하고, {"status": "error", "message": "Division by zero is not allowed."}를 반환한다.
    4. 이 에러 메시지가 ToolMessage로 에이전트에게 전달된다.
    5. LLM이 에러 메시지를 보고 "0으로 나눌 수 없습니다."와 같은 적절한 답변을 생성한다.
  • 입력: "사과 5개랑 바나나 3개를 더해줘."
  • 예상 흐름:
    1. LLM이 이 질문이 숫자 계산이 아닌 개념적 질문임을 이해한다.
    2. calculator 도구의 설명(docstring)에 부합하지 않다고 판단하여 도구를 호출하지 않는다.
    3. "사과와 바나나는 종류가 달라 직접 더할 수 없지만, 총 과일은 8개입니다."와 같이 자체적으로 답변을 생성한다.

이러한 테스트를 통해 우리는 단순히 코드를 실행하는 것을 넘어, 시스템의 각 구성 요소가 유기적으로 상호작용하며 다양한 상황에 지능적으로 대처하는지 종합적으로 검증할 수 있다.

4부: 심층 분석 - LangGraph, 다른 프레임워크와의 비교

지금까지 LangGraph를 사용하여 에이전트를 구축했다. 그렇다면 왜 수많은 프레임워크 중 LangGraph를 선택했을까? 이 장에서는 LangGraph의 본질적인 특징을 파헤치고, 기존 LangChain(LCEL)이나 다른 프레임워크와 비교하여 어떤 차별점과 장단점을 갖는지 심도 있게 분석한다.

LangGraph의 본질: 왜 '그래프'인가?

LangGraph의 가장 큰 특징은 이름에서 알 수 있듯 '그래프', 특히 **'상태를 가진 순환 그래프(Stateful, Cyclic Graph)'**를 만들 수 있다는 점이다.

  • 순환(Cycles)의 중요성: 기존 LangChain의 핵심인 LCEL(LangChain Expression Language)은 주로 단방향 파이프라인(DAG, Directed Acyclic Graph)을 구성하는 데 강점이 있다. 데이터가 입력부터 출력까지 한 방향으로 흐르는 구조이다. 반면, LangGraph는 '상태(State)'를 중심으로 노드 간의 순환을 명시적으로 허용한다. 이는 에이전트가 특정 작업을 수행한 후, 그 결과를 바탕으로 다시 생각하고 다음 행동을 결정하는 과정을 자연스럽게 모델링할 수 있게 해준다.
  • 에이전틱 워크플로우(Agentic Workflow): 이러한 순환 구조는 ReAct(Reason-Act)와 같은 에이전트적 사고 과정을 완벽하게 지원한다. [생각(Reason) → 행동(Act) → 관찰(Observe)]의 사이클을 반복하며 문제를 해결하는 에이전트의 작동 방식을 코드로 명확하게 구현할 수 있다. 우리의 프로젝트에서 `agent` 노드가 '생각'을, `tool_node`가 '행동'을, 그리고 API 응답이 '관찰'에 해당하며, 이 과정이 최종 답변에 도달할 때까지 반복된다.

프레임워크 비교 분석

LangGraph의 특징을 더 명확히 이해하기 위해 다른 프레임워크와 비교해 본다.

LangGraph

  • 장점:
    • 복잡한 제어 흐름: 여러 도구를 조합하고, 실패 시 재시도하며, 중간에 사람의 피드백을 받는 등 복잡하고 비선형적인 에이전트 로직 구현에 최적화되어 있다.
    • 명시적인 상태 관리: 모든 중간 과정과 결과가 `AgentState`에 기록되므로, 에이전트가 어떤 판단을 내렸는지 추적하고 디버깅하기가 매우 용이하다.
    • 자체 수정(Self-correction): 도구 사용이 실패했을 때, 그 실패 정보를 바탕으로 다른 방법이나 다른 도구를 시도하는 '자체 수정 루프'를 구현하는 데 강력하다.
  • 단점:
    • 초기 설정의 복잡성: 단순한 순차적 작업을 처리하기에는 상태, 노드, 엣지 등을 정의해야 하므로 LCEL에 비해 초기 설정 코드(boilerplate)가 많다.
    • 학습 곡선: 그래프 이론과 상태 관리 개념에 익숙하지 않다면 LCEL보다 학습 곡선이 상대적으로 가파를 수 있다.

LangChain (LCEL)

  • 장점:
    • 간결함과 생산성: RAG(검색 증강 생성) 파이프라인처럼 순차적인 작업 흐름을 매우 간결한 코드로 구성할 수 있다. (e.g., prompt | model | parser)
    • 방대한 생태계: 수많은 사전 구축된 컴포넌트(LLM, Retriever, Parser 등)를 레고 블록처럼 쉽게 조립하여 빠르게 프로토타입을 만들 수 있다.
  • 단점:
    • 순환 구조의 한계: 복잡한 조건부 분기나 순환 구조를 구현하려면 다소 부자연스러운 구조가 되거나 추가적인 로직이 필요하다.
    • 암시적 상태 관리: 에이전트의 중간 생각이나 상태를 명시적으로 추적하고 관리하기가 LangGraph보다 복잡하다.

기타 프레임워크 (예: Microsoft AutoGen)

  • 차이점 및 패러다임: AutoGen과 같은 프레임워크는 '다중 에이전트 간의 대화'에 초점을 맞춘다. 즉, 단일 에이전트의 내부 상태를 정교하게 제어하기보다는, 각기 다른 전문성을 가진 여러 에이전트(예: 코딩 에이전트, 테스트 에이전트, 비평가 에이전트)가 서로 대화하고 협력하여 문제를 해결하는 시나리오에 더 적합하다. 이는 LangGraph의 '단일 에이전트 제어'와는 다른 패러다임이다.

어떤 상황에 어떤 프레임워크를 선택해야 하는가?

선택 가이드라인

  • LangGraph 추천 시나리오:
    • 여러 도구를 순서에 맞게, 또는 조건에 따라 동적으로 사용해야 할 때.
    • 도구 실행 결과에 따라 다음 행동이 완전히 달라지는 복잡한 의사결정 과정이 필요할 때.
    • 에이전트의 모든 행동 단계를 명확히 로깅하고 디버깅해야 하는 프로덕션 수준의 시스템을 구축할 때.
    • 사용자 피드백을 받아 작업을 수정하는 'Human-in-the-loop' 워크플로우가 필요할 때.
  • LangChain (LCEL) 추천 시나리오:
    • 사용자 질문을 받아 문서를 검색하고, 그 내용을 바탕으로 답변을 생성하는 RAG 파이프라인을 빠르게 구축할 때.
    • 데이터를 특정 형식으로 변환하거나 요약하는 등, 입력에서 출력까지의 흐름이 명확한 순차적 작업을 처리할 때.
    • 빠른 프로토타이핑을 통해 아이디어를 검증하고 싶을 때.

결론적으로, '최고의' 프레임워크는 없다. 해결하고자 하는 문제의 복잡성과 요구되는 제어 수준에 따라 가장 적합한 '도구'를 선택하는 것이 중요하다. 본 프로젝트처럼 LLM이 스스로 판단하여 외부 도구를 호출하고 그 결과를 바탕으로 다시 생각하는 '에이전틱'한 동작을 구현하고자 한다면, LangGraph는 매우 강력하고 적절한 선택지이다.

결론: 프로젝트 정리 및 확장 가능성

본 문서는 Java Spring Boot로 안정적인 API 서버를 구축하고, 로컬 LLM(Ollama + Gemma)과 LangGraph를 활용하여 지능형 AI 에이전트를 개발하는 전 과정을 상세히 안내했다. 우리는 이 과정을 통해 단순히 기술을 나열하는 것을 넘어, 각 기술이 어떤 철학을 가지고 있으며 어떻게 유기적으로 결합하여 강력한 시스템을 만드는지 살펴보았다.

주요 학습 포인트

  • 관심사 분리(Separation of Concerns)의 가치: 안정적인 비즈니스 로직(Java)과 유연한 AI 추론 로직(Python)을 분리함으로써, 각 부분을 독립적으로 개발, 테스트, 확장할 수 있는 견고한 아키텍처의 이점을 확인했다.
  • 명시적 제어 흐름의 힘: LangGraph를 통해 AI 에이전트의 예측 불가능한 동작을 '상태를 가진 그래프'라는 명확하고 제어 가능한 구조로 설계할 수 있었다. 이는 복잡한 AI 시스템의 디버깅과 유지보수를 용이하게 만드는 핵심 요소이다.
  • 로컬 우선(Local-First) AI: Ollama를 통해 외부 서비스 의존성과 비용 문제에서 벗어나, 데이터 프라이버시를 지키며 자유롭게 실험하고 개발할 수 있는 환경의 중요성을 체감했다.

향후 확장 방향 제시

이 프로젝트는 시작점일 뿐이다. 여기서 더 나아가 다음과 같은 방향으로 시스템을 확장할 수 있다.

  1. 도구(Tool) 확장: 사칙연산 외에 실시간 날씨 조회, 웹 검색, 구글 캘린더 연동 등 새로운 기능을 Spring Boot API에 추가하고, LangGraph 에이전트가 이 도구들을 상황에 맞게 사용할 수 있도록 확장할 수 있다.
  2. LLM 성능 개선: 프롬프트 엔지니어링을 통해 LLM이 사용자의 미묘한 의도를 더 정확하게 파악하고, 여러 도구 중 가장 적절한 것을 선택하도록 유도하는 기법을 적용할 수 있다.
  3. 사용자 인터페이스(UI) 구축: Gradio나 Streamlit과 같은 Python 라이브러리를 사용하여, 터미널이 아닌 웹 브라우저에서 사용자가 쉽게 에이전트와 상호작용할 수 있는 UI를 추가할 수 있다.
  4. 클라우드 배포: 완성된 Spring Boot API 서버와 LangGraph 에이전트를 Docker 컨테이너로 패키징하여 AWS, GCP, Azure 등 클라우드 환경에 배포함으로써 실제 서비스로 운영하는 방안을 고려할 수 있다.

이 가이드가 여러분만의 독창적이고 강력한 AI 에이전트를 구축하는 데 훌륭한 발판이 되기를 바란다.

반응형