#고민리스트 #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` 의존하지 않고 작성할 수 있어서 코드 관리도 쉬워보였다.