#고민리스트 #AI #MCP #SpringAI #POC - 정리한 날짜 : #2025-05-29 ### 상황 `Jira`는 내가 가진 업무 파악하기 어렵다. 심지어 `Jira`에서 제공하는 대시보드는 가독성이 너무 떨어지고 조작이 불편하다. 그래서 아래 정보 기반으로 `Cluade MCP`를 활용해 업무 요약을 하고 있다. - 업무 우선 순위 추적 - `Issue` 추가 및 변경 추적 - `Issue` 내 추가된 `comment` 추적 조금 더 편리하게 사용할 수 있을까 고민했다. ### 문제 `Claude MCP`를 사용하려면 다음과 같은 문제점이 있다. - 매일 수동 입력해서 불편하다. - 입력값이 항상 같은데, 매번 입력해야 한다. ### 원인 매일 수동 입력해야 하는 이유는 `Claude` 클라이언트만 `MCP` 툴 트리거 할 수 있기 때문이다. ![[스크린샷 2025-05-29 오전 9.01.08.png]] ### 해결 `MCP 툴 트리거를 자유자제로 활용할 수 있는 MCP 클라이언트를 구현하면 되지 않을까?` 생각했고 곧바로 실행에 옮겼다. - 매일 동일한 질문을 스케줄러 돌려서 메일을 전송한다. #### POC 동작 `Spring AI`를 활용해 `MCP` 툴을 만들어 동작 POC 진행했다. 동작 확인을 위해 클라이언트를 생성했다. ![[압축 화면 기록.mov]] #### AI 모델에게 Tool 인식하게 하기 `Spring AI`를 활용해 MCP 툴 구현 방법은 어렵지 않았다. `@Tool` 애너테이션과 `@ToolParam`을 활용해 AI 에게 어떤 메서드인지를 힌트를 주면 된다. 다음은 예시다. ```kotlin @Tool(description = "Jira 프로젝트에서 일감 조회") fun getJiraIssuesInProject( @ToolParam(description = "프로젝트 명") projectName: String, ): String { //... return jiraClient.getJiraIssuesInProject(projectName) } ``` 이렇게 설정하고 프롬프트를 다음처럼 입력하면 해당 툴 실행 후 응답을 분석해 결과를 제공한다. ``` 프로젝트 명이 ABC인 프로젝트에 어떤 이슈 있는지 설명해줘 ``` #### 민감함 데이터 주입하지 않기 - ❌ 하지 말아야 하는 행동 Jira API 호출하는 만큼 `Jira API key`가 주입되게 된다. 만약 AI 에게 입력값으로 넘겨준다면 외부로 노출될 가능성이 높다. ``` 나 API Key가 OOO인데, 프로젝트 명이 ABC인 프로젝트에 어떤 이슈 있는지 설명해줘 ``` 또한 `@ToolParam`으로 이어지게 만드는 것도 문제이니 꼭 조심하자. ```kotlin @Tool(description = "프로젝트 조회") fun getJiraProjects( @ToolParam(description = "Jira Api Key") apiKey: String, ): String { //... } ``` 코드 이해할 때, 다음처럼 툴 인스턴스를 넘긴다. ```kotlin @PostMapping( "/jira", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.TEXT_EVENT_STREAM_VALUE] ) fun jira( @RequestHeader("Jira-Email") email: String, @RequestHeader("Jira-Api-Key") apiKey: String, @RequestBody message: ChatMessage ) = ChatClient.create(vertexAiGeminiChatModel) .prompt(message.text) .tools(jiraService()) .stream() .content() ``` 해당 툴 서비스를 인스턴스로 넘기게 되는데 두 가지 방식으로 직접 주입할 수 있겠다는 생각이 들었다. - 빈으로 등록 후 주입된 값 먼저 넣기 - 장점 : 변수로 등록해두면 쉽게 사용 가능하다. 인스턴스 재활용할 수 있다. - 단점 : 주입된 API 키만 사용하기 때문에 주입되지 않으면 사용하기 어려운 구조다. - 데코레이터 패턴을 활용해 값 주입하기 - 장점 : 요청마다 주입해서 사용할 수 있따. - 단점 : 요청마다 인스턴스를 생성해야 한다. #### 민감함 데이터 주입하지 않기 - ✅ 민감한 데이터는 직접 주입 API 키를 요청마다 입력받아 사용하고 싶던 니즈가 있었기 때문에 후자로 진행했다. ```kotlin @PostMapping( "/jira", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.TEXT_EVENT_STREAM_VALUE] ) fun jira( @RequestHeader("Jira-Email") email: String, @RequestHeader("Jira-Api-Key") apiKey: String, @RequestBody message: ChatMessage ) = ChatClient.create(vertexAiGeminiChatModel) .prompt(message.text) .tools(jiraDecoratorService.create(apiKey, email)) .stream() .content() class JiraToolService( private val apiKey: String, private val email: String, ) { @Tool(description = "Jira 프로젝트 조회") fun getJiraProjects(/*인자*/): String { //... } } ``` 또한 `@Tool` 관리 객체는 스프링 의존성을 없애기 위해 외부에서 빈 주입받도록 구현하기도 했다. ```kotlin @Service class JiraDecoratorService( private val jiraClient: JiraClient, ) { fun create(apiKey: String, email: String): JiraToolService { return JiraToolService(apiKey, email, jiraClient) } } ``` #### Jira API 호출하기 Jira API 호출은 Http Interface 활용하면 쉽게 구현 가능하다. ```kotlin @HttpExchange interface JiraClient { @GetExchange("/rest/api/3/project/search") fun getJiraProjects(@RequestHeader("Authorization") authorization: String): String @GetExchange("/rest/api/3/issue/{issueId}") fun getJiraIssueDetails( @RequestHeader("Authorization") authorization: String, @PathVariable issueId: String ): String @PostExchange( "/rest/api/3/search/jql", contentType = MediaType.APPLICATION_JSON_VALUE, accept = [MediaType.APPLICATION_JSON_VALUE] ) fun getJiraIssuesInProject( @RequestHeader("Authorization") authorization: String, @RequestBody searchIssueSummaryJqlRequest: SearchIssueSummaryJqlRequest, ): String } ``` ### 결과 - `POC` 진행하면서 `사내 FAQ, 문의내용 등을 조합해 챗봇을 만들 수 있지 않을까?` 생각들었다. 바로 진행해봐야겠다. - `Spring AI`를 처음 사용해봤는데 추상화가 잘되서 사용성이 좋았다. 또한 `Spring AI` 활용하는 `Tool` 관리 방식은 `Spring` 의존하지 않고 작성할 수 있어서 코드 관리도 쉬워보였다.