Loading the catalog…
Loading the catalog…
🌱 들어가며 요즘 Claude, ChatGPT, Cursor 같은 AI 도구를 쓰다 보면 LLM , MCP 같은 단어를 정말 자주 보게 된다. 막연하게 "AI 모델이구나", "뭔가 연결하는 거구나" 정도로만 알고 있었는데, 백엔드 개발자로서 이걸 내 서비스에 어떻게 붙일 수 있는지 제대로 알고 싶어서 직접 공부하고 만들어 봤다. 이번 글에서는 LLM이 무엇이고 어떻게 동작하는지 LLM의 한계와, 그걸 보완하는 MCP가 무엇인지 Spring AI 로 간단한 MCP 서버를 만들어서 AI 클라이언트에 연결하는 과정 까지 정리해 본다. 🧠 LLM이란? LLM(Large Language Model, 대규모 언어 모델) 은 방대한 양의 텍스트를 학습해서 사람처럼 글을 이해하고 생성하는 모델 이다. ChatGPT의 GPT, Anthropic의 Claude, Google의 Gemini 등이 모두 LLM이다. 동작 원리: "다음 단어 맞히기" LLM의 원리는 생각보다 단순하다. 주어진 문장 다음에 올 가장 그럴듯한 단어를 예측 하는 것을 반복한다. 입력: "스프링에서 의존성을 주입하는 방법은" ↓ 다음 단어 예측 "생성자" → "주입," → "필드" → "주입," → ... 인터넷의 수많은 문서, 코드, 책을 학습하면서 "이런 문맥에서는 이런 말이 나온다"는 패턴을 익혔기 때문에, 단어 예측만 반복하는데도 질문에 답하고 코드를 짜는 것처럼 보이는 것이다. 알아두면 좋은 용어 용어 설명 토큰(Token) LLM이 텍스트를 처리하는 단위. 단어보다 작은 조각이며, 한글은 보통 한 글자가 1~2 토큰 정도다. API 비용도 토큰 단위로 계산 된다. 컨텍스트 윈도우 LLM이 한 번에 볼 수 있는 입력 + 출력의 최대 토큰 수. 대화가 길어지면 앞부분을 잊는 이유다. 프롬프트(Prompt) LLM에게 주는 입력. 역할을 지정하는 시스템 프롬프트 와 실제 질문인 유저 프롬프트 로 나뉜다. 환각(Hallucination) 모르는 내용을 그럴듯하게 지어내는 현상. "다음에 올 그럴듯한 말"을 만드는 구조상 생기는 문제다. 학습 기준일(Cutoff) 모델이 학습한 데이터의 마지막 시점. 그 이후의 정보는 모른다. LLM의 한계 LLM은 똑똑하지만, 기본적으로 학습된 지식 안에서만 답할 수 있다. ❌ 내 서비스 DB 에 있는 데이터를 모른다 ❌ 오늘 날씨, 최신 뉴스 같은 실시간 정보를 모른다 ❌ 직접 API를 호출하거나 파일을 수정 하는 등 행동을 할 수 없다 그래서 등장한 개념이 Tool Calling(Function Calling) 이다. LLM에게 "이런 함수들이 있어"라고 알려주면, LLM이 필요할 때 "이 함수를 이 파라미터로 호출해줘" 라고 요청하고, 애플리케이션이 실제로 실행한 결과를 다시 LLM에게 돌려준다. 사용자: "최근 7일 결제 장애 있었어?" ↓ LLM: (내가 모르는 정보네 → search-incidents(service="결제", days=7) 호출 요청) ↓ 애플리케이션: 실제 DB 조회 → 결과 반환 ↓ LLM: "네, 3일 전에 PG사 타임아웃으로 인한 장애가 1건 있었습니다." 그리고 이 Tool Calling을 표준화 한 것이 바로 MCP다. 💡 MCP란? MCP(Model Context Protocol) 는 Anthropic이 2024년 11월에 공개한 LLM과 외부 도구/데이터를 연결하기 위한 표준 프로토콜 이다. 흔히 "AI를 위한 USB-C" 라고 비유한다. MCP 이전: AI 앱마다 도구 연동 방식이 제각각 → 도구 N개 × AI 앱 M개 = N×M개의 연동 코드 MCP 이후: 도구는 MCP 서버로 한 번만 만들고, MCP를 지원하는 모든 AI 앱에서 사용 → N+M ┌────────────────────┐ ┌─────────────────────┐ │ MCP Host │ │ MCP Server (내가 │ │ (Claude, Cursor, │ JSON- │ 만들 서버) │ │ IDE 등) │ RPC │ - Tools │ │ └ MCP Client ────┼─────────▶│ - Resources │──▶ DB / API │ │◀─────────┼ - Prompts │ └────────────────────┘ └─────────────────────┘ MCP 서버가 제공하는 3가지 구분 설명 예시 Tools LLM이 호출 할 수 있는 함수 주문 조회, 티켓 생성 Resources LLM이 읽을 수 있는 데이터 설정 파일, 문서 Prompts 재사용 가능한 프롬프트 템플릿 "코드 리뷰해줘" 템플릿 이 중 가장 많이 쓰이는 건 Tools 다. 이번 글도 Tool 위주로 진행한다. 🤔 Function Calling이랑 뭐가 다른데? LLM이 함수를 호출한다는 개념 자체는 같다. 차이는 어디에 붙어있느냐 다. Function Calling : 내 애플리케이션 코드 안에서 특정 LLM API에 함수를 등록 → 내 앱에서만 사용 가능 MCP : 함수를 독립된 서버 로 분리하고 표준 프로토콜로 노출 → Claude, Cursor, 다른 에이전트 등 어디서든 재사용 즉, MCP는 Function Calling을 표준화하고 재사용 가능하게 만든 것 이라고 이해하면 된다. 백엔드 개발자 입장에서 보면 "AI가 호출하는 API 서버" 를 만드는 것과 비슷하다. 🔑 Spring AI로 MCP 서버 만들기 Java 진영에서는 Spring AI 가 MCP 서버/클라이언트를 모두 지원한다. 특히 Spring AI 2.0부터는 @McpTool 어노테이션이 Spring AI 본체로 들어오면서, 컨트롤러 만들듯이 MCP 도구를 만들 수 있게 됐다. 이 글은 Spring AI 2.0.x + Spring Boot 4.x 기준이다. 예제 시나리오 토이 프로젝트용으로 서비스 장애 이력 을 저장하는 간단한 서버를 만들고, 이 조회 기능을 MCP로 노출한다. AI에게 "지난주에 결제 관련 장애 있었어?" 라고 물어보면 AI가 직접 조회해서 답하게 만드는 것이 목표다. 🧲 환경 구성 1. build.gradle dependencies { implementation platform("org.springframework.ai:spring-ai-bom:2.0.1") // MCP 서버 (Streamable HTTP, Spring MVC 기반) implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' } 전송 방식에 따라 스타터가 나뉜다. 전송 방식 스타터 용도 STDIO spring-ai-starter-mcp-server 로컬 프로세스로 실행 HTTP (WebMVC) spring-ai-starter-mcp-server-webmvc 서버로 띄워서 원격으로 사용 HTTP (WebFlux) spring-ai-starter-mcp-server-webflux 리액티브 환경 익숙한 Spring MVC 방식으로 서버를 띄우고 싶어서 WebMVC + Streamable HTTP 를 선택했다. 2. application.yml spring: ai: mcp: server: name: incident-mcp-server version: 1.0.0 type: SYNC protocol: STREAMABLE # Streamable HTTP annotation-scanner: enabled: true # @McpTool 자동 스캔 이렇게만 설정하면 POST /mcp 엔드포인트가 자동으로 열린다. 🛠️ Tool 구현 Entity & Repository @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Incident { @Id @GeneratedValue private Long id; private String title; private String service; // 결제, 회원, 주문 ... @Enumerated(EnumType.STRING) private Severity severity; private String cause; private LocalDateTime occurredAt; } public enum Severity { CRITICAL, MAJOR, MINOR } public interface IncidentRepository extends JpaRepository<Incident, Long> { List<Incident> findByServiceAndOccurredAtAfterOrderByOccurredAtDesc( String service, LocalDateTime from); List<Incident> findTop10ByOrderByOccurredAtDesc(); } MCP Tool import org.springframework.ai.mcp.annotation.McpTool; import org.springframework.ai.mcp.annotation.McpToolParam; @Component @RequiredArgsConstructor public class IncidentTools { private final IncidentRepository incidentRepository; @McpTool( name = "search-incidents", description = "특정 서비스의 최근 장애 이력을 조회한다. 서비스명과 조회할 기간(일)을 받는다.", annotations = @McpTool.McpAnnotations(readOnlyHint = true) ) public List<IncidentDto> searchIncidents( @McpToolParam(description = "서비스명 (예: 결제, 회원, 주문)", required = true) String service, @McpToolParam(description = "오늘로부터 며칠 전까지 조회할지", required = true) int days) { LocalDateTime from = LocalDateTime.now().minusDays(days); return incidentRepository .findByServiceAndOccurredAtAfterOrderByOccurredAtDesc(service, from) .stream() .map(IncidentDto::from) .toList(); } @McpTool( name = "recent-incidents", description = "전체 서비스의 최근 장애 10건을 조회한다.", annotations = @McpTool.McpAnnotations(readOnlyHint = true) ) public List<IncidentDto> recentIncidents() { return incidentRepository.findTop10ByOrderByOccurredAtDesc() .stream() .map(IncidentDto::from) .toList(); } } public record IncidentDto( Long id, String title, String service, String severity, String cause, LocalDateTime occurredAt) { public static IncidentDto from(Incident i) { return new IncidentDto(i.getId(), i.getTitle(), i.getService(), i.getSeverity().name(), i.getCause(), i.getOccurredAt()); } } @RestController 에 @GetMapping 붙이던 것과 거의 비슷하다. 차이점은 딱 하나, 💡 description 이 곧 API 문서이자 프롬프트다. 앞에서 본 것처럼 LLM은 결국 텍스트를 보고 판단 한다. 그래서 description 을 읽고 "이 도구를 언제, 어떤 파라미터로 호출할지" 를 결정한다. 설명이 모호하면 AI가 엉뚱한 도구를 호출하거나 아예 호출하지 않는다. Swagger 문서 쓰듯이 구체적으로 작성하는 것이 핵심이다. 또 반환값은 Spring AI가 JSON으로 직렬화해서 LLM에게 전달한다. 반환 데이터도 결국 토큰 이기 때문에, 엔티티를 그대로 반환하기보다는 필요한 필드만 담은 DTO 를 반환해야 컨텍스트 낭비와 불필요한 정보 노출을 막을 수 있다. 🔌 AI 클라이언트에 연결하기 1. MCP Inspector로 먼저 테스트 바로 AI에 붙이기 전에, 공식 디버깅 도구인 MCP Inspector 로 도구 목록과 호출 결과를 확인할 수 있다. Postman 같은 역할이다. npx @modelcontextprotocol/inspector 브라우저가 열리면 Transport를 Streamable HTTP , URL을 http://localhost:8080/mcp 로 입력하고 연결한다. Tools 탭에서 search-incidents , recent-incidents 두 개가 보이면 성공이다. 파라미터를 직접 넣고 호출해서 응답 JSON도 확인할 수 있다. 2. Claude Code에 연결 claude mcp add --transport http incident http://localhost:8080/mcp 연결 후 이렇게 물어보면, > 최근 7일 동안 결제 서비스에 장애 있었어? 원인도 정리해줘 AI가 스스로 search-incidents 도구를 service="결제", days=7 로 호출하고, 조회 결과를 바탕으로 장애 건수와 원인을 요약해서 답변해 준다. 내가 한 일은 도구를 만들고 설명을 잘 써둔 것 뿐인데, 어떤 도구를 언제 쓸지는 LLM이 알아서 판단한다는 점이 신기했다. Cursor, Claude Desktop 등 MCP를 지원하는 다른 클라이언트에서도 같은 URL로 연결할 수 있다. 서버는 하나, 클라이언트는 무엇이든 — 이게 MCP의 핵심 장점이다. ⚠️ 주의할 점 항목 내용 인증 HTTP 기반 MCP 서버는 기본적으로 인증 없는 엔드포인트 가 열린다. localhost 밖으로 노출하려면 Spring Security 등으로 반드시 인증을 붙여야 한다. 권한 최소화 조회용 도구는 readOnlyHint = true 로 명시하고, 삭제·수정 같은 도구는 신중하게 만든다. LLM이 잘못 판단해서 호출할 수도 있기 때문이다. Description 품질 AI가 도구를 잘 못 쓴다면 코드보다 description 부터 의심하자. 응답 크기 조회 결과가 너무 크면 컨텍스트 윈도우를 낭비한다. 페이징이나 건수 제한을 두자. 민감 정보 반환한 데이터는 외부 LLM으로 전달된다. 개인정보·보안 정보는 DTO에서 제외한다. 🌸 마치며 💭 느낀 점 사실 AI 쪽은 제대로 공부해 본 적이 없어서, 처음에는 LLM, 토큰, MCP 같은 용어부터 낯설었다. "AI를 붙인다"고 하면 모델을 직접 학습시키거나 복잡한 파이썬 코드를 짜야 하는 줄 알았는데, 막상 해보니 내가 평소에 하던 일과 크게 다르지 않았다. @McpTool 은 @GetMapping 처럼 느껴졌고 description 은 Swagger 설명을 쓰는 느낌이었고 결국 "사람 대신 AI가 호출하는 API" 를 만든 것이었다 가장 신기했던 건, 나는 메서드를 만들고 설명만 적어뒀을 뿐인데 어떤 도구를 언제, 어떤 값으로 호출할지는 AI가 알아서 판단 한다는 점이었다. 반대로 말하면 설명을 대충 쓰면 AI도 대충 판단한다는 뜻이라, 코드만큼 설명을 잘 쓰는 것 이 중요하다는 걸 느꼈다. 또 AI가 내 DB 데이터를 직접 조회할 수 있다는 건 편하지만, 그만큼 인증이나 권한, 민감 정보 노출 은 더 신경 써야겠다는 생각이 들었다. 아직 깊이 있게 아는 건 아니지만, 백엔드 개발자도 AI를 "쓰는 사람"에서 한 발 나아가 "AI가 쓸 수 있는 기능을 만드는 사람" 이 될 수 있겠다는 걸 알게 된 계기였다. 📝 정리 처음엔 LLM이나 MCP가 거창한 기술처럼 느껴졌는데, 정리해 보니 LLM 은 "다음 단어를 예측하는 모델"이고, 학습한 것 밖의 정보는 모른다 그래서 Tool Calling 으로 외부 기능을 호출할 수 있게 했고 MCP 는 그 도구 연결 방식을 표준화한 프로토콜 이다 라는 흐름으로 이해할 수 있었다. 특히 Spring AI를 쓰면 기존 Service/Repository 코드는 그대로 두고 @McpTool 만 붙이면 되기 때문에, 이미 만들어 둔 Spring 프로젝트에도 부담 없이 적용해 볼 수 있을 것 같다. 다음 글에서는 반대로 Spring AI MCP Client 를 이용해서, 내 애플리케이션 안에서 외부 MCP 서버(GitHub, Slack 등)를 호출하는 에이전트를 만들어 볼 예정이다. 📚 참고 Model Context Protocol 공식 문서 Spring AI Reference - MCP Server Boot Starter Spring AI Reference - MCP Server Annotations Spring AI Reference - Upgrade Notes
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
[Spring AI] LLM 기초부터 MCP 서버 만들기까지. 🌱 들어가며 요즘 Claude, ChatGPT, Cursor 같은 AI 도구를 쓰다 보면 LLM , MCP 같은 단어를 정말 자주 보게 된다. 막연하게 "AI 모델이구나", "뭔가 연결하는 거구나" 정도로만 알고 있었는데, 백엔드 개발자로서 이걸 내 서비스에 어떻게 붙일 수 있는지 제대로 알고 싶어서 직접 공부하고 만들어 봤다. 이번 글에서는 LLM이 무엇이고 어떻게 동작하는지 LLM의 한계와, 그걸 보완하는 MCP가 무엇인지 Spring AI 로 간단한 MCP 서버를 만들어서 AI 클라이언트에 연결하는 과정 까지 정리해 본다. 🧠 LLM이란? LLM(Large Language Model, 대규모 언어 모델) 은 방대한 양의 텍스트를…
Open source