← 프로젝트 목록으로
완료2026.06

OP.GG MCP 디스코드 봇

OP.GG MCP와 OpenAI API를 연결해 게임 질문, 챔피언 추천과 코칭을 디스코드에서 제공합니다. 지인들과 사용하며 응답 시간을 8~10초에서 약 4초로 줄였습니다.

OpenAI APIMCPDiscordGoJavaJSON-RPCJDA

담당 역할 · 개인 프로젝트 기획·개발, 도구 연동, 실제 사용 및 개선 검증

전적을 보는 것에서, 플레이 방법을 묻는 것으로

리그 오브 레전드를 플레이하면서 승률이나 상성 순위만으로는 당장 어떻게 행동해야 할지 알기 어렵다고 느꼈습니다. 상대 조합을 보고 어떤 챔피언을 고를지, 라인전에서 언제 어떤 스킬로 대응할지 묻고 싶었습니다.

그래서 평소 친구들과 사용하는 디스코드 안에서 질문하고 답을 받을 수 있는 AI 롤 코치 옥토봇(Octobot)을 만들었습니다. OP.GG MCP의 게임 정보와 OpenAI API를 연결해 통계를 플레이 가이드로 설명하는 흐름을 구성했습니다.

여기서 MCP(Model Context Protocol)는 AI 애플리케이션이 외부 도구의 목록과 사용 방법을 확인하고, 정해진 방식으로 도구 실행을 요청하게 하는 통신 규약입니다. OpenAI API로 질문과 답변을 처리하고, OP.GG MCP로 실제 게임 데이터를 조회하는 역할을 나눴습니다.

제가 맡은 범위는 기능·사용 흐름 정의, AI 코딩 도구를 활용한 구현, MCP 클라이언트와 디스코드 연동, 실제 사용 및 개선 결과 확인입니다.

Java와 JDA로 첫 번째 사용 흐름 만들기

처음에는 익숙한 Java 17과 JDA(Java Discord API)로 구현했습니다. JDA는 자바 코드에서 디스코드 메시지를 받고 응답을 보내는 데 사용하는 라이브러리입니다. 디스코드에서 봇을 호출하면 질문을 받아 OpenAI API와 통신하고, 필요한 게임 정보를 조회한 뒤 분석 결과를 디스코드의 제목·본문·항목을 묶은 카드형 메시지인 임베드로 전달했습니다.

디스코드 질문
  → JDA 메시지 수신
  → OpenAI의 질문 해석과 도구 호출 요청
  → OP.GG MCP 도구로 게임 정보 조회
  → 조회 결과를 활용한 답변 생성
  → 디스코드 응답

단순한 아이템 추천을 넘어 상대 조합에 따른 챔피언 추천 순위, 1대1 매치업의 레벨별 콤보와 스킬 설명을 답변에 담도록 프롬프트와 출력 형식을 조정했습니다.

아래는 초기 개발 기록에 남긴 메시지 리스너의 축약 예시입니다. 질문을 받아 AI 서비스로 넘기는 진입점을 보여주며, 임베드 구성과 내부 도구 호출 구현은 생략되어 있습니다.

public class MessageListener extends ListenerAdapter {
    @Override
    public void onMessageReceived(MessageReceivedEvent event) {
        if (event.getAuthor().isBot()) return;
        String message = event.getMessage().getContentRaw();

        if (message.startsWith("@옥토봇")) {
            String response = aiService.askGpt(message);
            event.getChannel().sendMessage(response).queue();
        }
    }
}

친구들과 사용하며 확인한 답변 형태

친구들과 사용하는 롤 디스코드 방에서 상대 탑·정글·미드 조합을 입력하고 미드 챔피언을 추천받는 방식으로 사용했습니다. 별도 전적 검색 창을 열지 않고 대화 안에서 추천 이유와 관련 통계를 확인할 수 있었습니다.

아래는 당시 실제 응답 화면입니다. 상대 조합의 특징을 설명하고 추천 챔피언과 OP.GG 통계를 함께 보여줍니다. 화면의 수치는 당시 조회 결과이며, 현재 게임 통계나 추천을 의미하지 않습니다.

상대 레넥톤·니달리·트위스티드 페이트 조합에 대해 리산드라를 추천하고 이유와 OP.GG 통계를 설명하는 옥토봇의 디스코드 응답
친구들과 사용한 디스코드에서의 챔피언 추천 응답

동작하는 봇에서 발견한 두 가지 문제

실제로 사용하고 다른 사용자에게도 전달하려다 보니, 기능 구현만으로 해결되지 않는 문제가 보였습니다.

  • 실행 환경 의존성: 초기 Java 버전을 사용자 PC에서 실행하려면 자바 프로그램을 돌리는 실행 환경인 JRE를 설치하고, 환경을 설정한 뒤 프로그램을 묶은 JAR 파일을 실행해야 했습니다. 비개발자가 받아 사용하기에는 준비 과정이 길었습니다.
  • 도구 호출의 대기 시간: 여러 정보를 조회할 때 도구를 하나씩 호출했습니다. 답변이 나오기까지 약 8~10초가 걸리는 경우가 있어 짧은 밴픽 시간에 사용하기 불편했습니다.

개선 목표는 별도 런타임 설치 부담을 줄이고, 여러 외부 조회의 대기 시간이 누적되는 구조를 바꾸는 것이었습니다.

Go 전환과 MCP 클라이언트 재구현

단일 실행 파일로 배포 준비 단순화

AI 코딩 도구를 활용해 Go 기반으로 재구성했습니다. go build로 단일 실행 파일을 만들 수 있어, 기존 Java 버전에서 필요했던 JRE 설치 과정을 줄일 수 있었습니다. Windows용 octobot.exe로 실행하는 형태를 구성했습니다.

개발 당시 기록에서는 메모리 사용량이 약 500MB에서 약 30MB 수준으로 줄어든 것을 관찰했습니다. 언어 전환과 도구 호출 구조 변경을 함께 진행한 결과입니다.

HTTP 405 오류를 통해 연결 방식 확인

Go 버전에서는 처음에 서버가 하나의 연결을 통해 계속 이벤트를 보내는 SSE(Server-Sent Events) 방식으로 OP.GG MCP에 연결하려 했습니다. 이때 405 Method Not Allowed 오류가 발생했습니다. 당시 연결 대상의 네트워크 통신을 분석해 HTTP POST로 JSON-RPC 2.0 요청을 보내는 방식을 확인했습니다. JSON-RPC는 실행할 메서드 이름과 입력값, 요청 식별값을 JSON 형식에 담아 외부 기능을 호출하는 규약입니다. 이 연결에서는 서버의 연속 이벤트를 기다리는 대신 요청을 보내고 대응하는 응답을 읽어야 했습니다.

SSE 스트림을 처리하던 코드를 제거하고 Go로 요청·응답 클라이언트를 직접 구성했습니다. 요청별 ID와 메서드, 파라미터를 전달하고 HTTP 응답을 파싱하는 방식입니다.

다음은 개발 기록에 남긴 요청 메시지 구조입니다.

type jsonRPCRequest struct {
    JSONRPC string `json:"jsonrpc"`
    Method  string `json:"method"`
    ID      *int64 `json:"id,omitempty"`
    Params  any    `json:"params,omitempty"`
}

여러 요청이 동시에 생겨도 같은 번호를 발급하지 않도록, 증가 연산을 나눌 수 없는 한 동작으로 처리하는 atomic.AddInt64로 ID를 발급하고 JSONRPC를 "2.0"으로 설정했습니다. 연결을 수정한 뒤 당시 제공되던 분석 도구 21개의 목록을 가져와 프로그램에서 재사용할 수 있도록 저장했습니다. 매번 목록을 다시 요청하지 않고 보관한 값을 쓰는 것을 캐싱이라고 합니다. 이는 도구 목록의 캐싱이며, 게임 통계 결과 전체를 캐싱했다는 의미는 아닙니다.

순차 조회를 병렬 도구 호출로 변경

전체 응답에서 중요한 병목은 모델이 요청한 도구들을 실행하는 루프였습니다. 예를 들어 챔피언 종합 분석과 라인 매치업 정보가 함께 필요하면, 기존 방식에서는 첫 번째 조회가 끝난 뒤 두 번째 조회를 시작했습니다.

Go 버전에서는 모델이 한 번에 요청한 여러 도구 호출을 각각 고루틴으로 실행했습니다. 고루틴은 여러 작업을 함께 진행할 수 있게 하는 Go의 실행 단위입니다. sync.WaitGroup은 시작한 작업들이 모두 끝날 때까지 기다리는 역할을 합니다. 각 조회를 먼저 시작한 뒤 전부 완료되면 결과를 모으도록 변경했습니다. 서로 독립적인 조회의 네트워크 대기 시간이 차례로 더해지는 것을 줄이기 위한 변경입니다.

변경 전: 도구 A 조회 → 도구 B 조회 → 결과 취합 → 답변 생성

변경 후: 도구 A 조회 ─┐
                      ├→ 결과 취합 → 답변 생성
         도구 B 조회 ─┘

다음은 개발 당시 기록한 병렬 호출 부분의 발췌입니다.

toolCallsCount := len(respMessage.ToolCalls)
type toolResult struct {
    index int
    msg   openai.ChatCompletionMessage
}
resultsChan := make(chan toolResult, toolCallsCount)
var wg sync.WaitGroup

for idx, toolCall := range respMessage.ToolCalls {
    wg.Add(1)
    go func(i int, tc openai.ToolCall) {
        defer wg.Done()

        resultData := callAndTranslateTool(
            tc.Function.Name, tc.Function.Arguments,
        )
        resultsChan <- toolResult{
            index: i,
            msg: openai.ChatCompletionMessage{
                Role:       openai.ChatMessageRoleTool,
                Content:    resultData,
                ToolCallID: tc.ID,
            },
        }
    }(idx, toolCall)
}

wg.Wait()
close(resultsChan)

여러 고루틴이 돌려주는 결과를 모으기 위해 채널을 사용했습니다. 채널은 실행 중인 작업 사이에서 값을 전달하는 통로입니다. 결과를 받을 때까지 임시 보관할 수 있는 칸 수(버퍼)를 도구 호출 수에 맞췄고, 요청마다 붙은 ToolCallID로 어떤 결과가 어느 도구 호출에 해당하는지 연결했습니다. 위 코드는 조회 실행과 대기 부분을 보여주는 발췌이며, 채널에서 결과를 읽어 후속 모델 요청에 반영하는 과정은 포함하지 않습니다.

병렬화는 같은 단계에서 실행 가능한 외부 조회에 적용했습니다. 앞선 조회 결과가 필요한 후속 호출이나 모델의 답변 생성 시간까지 제거하는 방식은 아닙니다.

경량 모델 전환과 출력 형식 간소화

도구 호출을 병렬화한 뒤에도 답변 시간의 상당 부분은 모델이 답변을 생성하는 시간이었습니다. 그래서 더 가벼운 모델로 변경하고, 프롬프트와 출력 형식을 간소화해 모델이 만들어야 하는 내용을 줄였습니다. 측정해 보니 응답 시간 개선 효과가 가장 컸던 부분이 이 변경이었습니다.

변경 후 확인한 결과와 남은 과제

항목 초기 Java 버전 Go 전환·호출 구조·모델 개선 후
로컬 실행 준비 JRE 설치 및 JAR 실행 단일 실행 파일 형태
복수 도구 조회 순차 호출 고루틴 기반 병렬 호출
전체 답변 시간 약 8~10초로 기록 약 4초로 기록
메모리 사용량 약 500MB로 관찰 약 30MB 수준으로 관찰

수치는 당시 개발·사용 과정에서 관찰하고 기록한 결과입니다. 동일 조건의 반복 측정 횟수, 모델·입력별 분포, 실행 환경을 함께 남긴 정식 벤치마크는 아니므로 모든 질문에서 같은 성능을 보장하는 값으로 사용하지 않습니다. 전체 응답 시간에는 외부 API와 모델 응답 시간도 영향을 줍니다.

실제로 사용해 보면서 기능의 유무뿐 아니라 실행 준비 과정과 답변을 기다리는 시간도 사용성의 일부라는 점을 확인했습니다. 익숙한 Java로 사용 흐름을 먼저 만들고, 확인된 실행 환경 의존성과 외부 호출 병목을 개선하는 순서로 진행했습니다.

추가로 보완·확인할 항목은 각 호출을 기다리는 최대 시간(타임아웃), 사용자가 요청을 취소하거나 시간이 초과됐을 때 관련 작업도 멈추는 처리(컨텍스트 취소), 일부 도구만 실패했을 때의 응답 처리, 같은 조건에서의 성능 재측정입니다.