864 lines
17 KiB
Markdown
864 lines
17 KiB
Markdown
# Dewey MCP 서버 API 명세서
|
|
|
|
## 개요
|
|
|
|
Dewey MCP (Model Context Protocol) 서버는 JSON-RPC 2.0 프로토콜을 기반으로 AI 장기기억 시스템의 기능을 제공합니다.
|
|
|
|
- **프로토콜 버전**: 2024-11-05
|
|
- **서버 이름**: Dewey Memory Server
|
|
- **서버 버전**: 0.0.1-SNAPSHOT
|
|
- **Base URL**: `http://localhost:8080/mcp`
|
|
- **아키텍처**: MVC (Model-View-Controller) 패턴
|
|
|
|
---
|
|
|
|
## 아키텍처
|
|
|
|
### MVC 구조
|
|
|
|
```
|
|
Controller (McpController)
|
|
↓
|
|
API Service (McpService)
|
|
↓
|
|
Domain Service (MemoryService)
|
|
↓
|
|
Repository (MemoryRepository)
|
|
↓
|
|
Database (PostgreSQL / Redis)
|
|
```
|
|
|
|
### 계층별 역할
|
|
|
|
- **Controller**: HTTP 요청 처리 및 라우팅
|
|
- **API Service**: Controller와 Domain Service 사이의 어댑터
|
|
- **Domain Service**: 비즈니스 로직 처리
|
|
- **Repository**: 데이터 접근 계층
|
|
- **Model**: 도메인 엔티티 및 DTO
|
|
|
|
---
|
|
|
|
## 엔드포인트
|
|
|
|
### POST /mcp
|
|
|
|
모든 MCP 요청은 이 단일 엔드포인트로 처리됩니다. 요청 본문은 JSON-RPC 2.0 형식을 따릅니다.
|
|
|
|
**요청 헤더:**
|
|
```
|
|
Content-Type: application/json
|
|
```
|
|
|
|
---
|
|
|
|
## MCP 메서드
|
|
|
|
### 1. initialize
|
|
|
|
MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다.
|
|
|
|
#### 요청
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "1",
|
|
"method": "initialize",
|
|
"params": {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {},
|
|
"clientInfo": {
|
|
"name": "client-name",
|
|
"version": "1.0.0"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 응답
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "1",
|
|
"result": {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {
|
|
"tools": {},
|
|
"resources": {},
|
|
"prompts": {}
|
|
},
|
|
"serverInfo": {
|
|
"name": "Dewey Memory Server",
|
|
"version": "0.0.1-SNAPSHOT"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**응답 DTO**: `McpInitializeResponse`
|
|
|
|
---
|
|
|
|
### 2. tools/list
|
|
|
|
사용 가능한 모든 도구 목록을 반환합니다.
|
|
|
|
#### 요청
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "2",
|
|
"method": "tools/list"
|
|
}
|
|
```
|
|
|
|
#### 응답
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "2",
|
|
"result": {
|
|
"tools": [
|
|
{
|
|
"name": "store_memory",
|
|
"description": "메모리를 Redis에 임시 저장합니다",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory_text": {
|
|
"type": "string",
|
|
"description": "저장할 메모리 텍스트"
|
|
},
|
|
"user_id": {
|
|
"type": "string",
|
|
"description": "사용자 ID"
|
|
},
|
|
"importance": {
|
|
"type": "integer",
|
|
"description": "중요도 (1-5)",
|
|
"minimum": 1,
|
|
"maximum": 5
|
|
}
|
|
},
|
|
"required": ["memory_text", "user_id"]
|
|
}
|
|
},
|
|
{
|
|
"name": "retrieve_memory",
|
|
"description": "사용자의 메모리를 조회합니다",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"user_id": {
|
|
"type": "string",
|
|
"description": "사용자 ID"
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "조회할 메모리 개수",
|
|
"default": 10
|
|
}
|
|
},
|
|
"required": ["user_id"]
|
|
}
|
|
},
|
|
{
|
|
"name": "search_memory",
|
|
"description": "벡터 유사도 기반으로 메모리를 검색합니다",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "검색 쿼리"
|
|
},
|
|
"user_id": {
|
|
"type": "string",
|
|
"description": "사용자 ID (선택)"
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "검색 결과 개수",
|
|
"default": 5
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**응답 DTO**: `McpToolResponse.ToolListResponse`
|
|
|
|
---
|
|
|
|
### 3. tools/call
|
|
|
|
특정 도구를 실행합니다. 도구 실행 결과에 따라 응답 내용이 달라집니다.
|
|
|
|
#### 요청 형식
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "3",
|
|
"method": "tools/call",
|
|
"params": {
|
|
"arguments": {
|
|
"name": "도구명",
|
|
"arguments": {
|
|
"도구별_파라미터": "값"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 응답 형식
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "3",
|
|
"result": {
|
|
"isError": false,
|
|
"content": "실행 결과 메시지",
|
|
"parts": [],
|
|
"metadata": {
|
|
"tool": "도구명",
|
|
"timestamp": 1701234567890
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**응답 DTO**: `McpToolCallResponse`
|
|
|
|
#### 도구별 사용 예시
|
|
|
|
##### store_memory
|
|
|
|
메모리를 Redis에 임시 저장합니다.
|
|
|
|
**요청:**
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "4",
|
|
"method": "tools/call",
|
|
"params": {
|
|
"arguments": {
|
|
"name": "store_memory",
|
|
"arguments": {
|
|
"memory_text": "오늘 사용자가 피자를 주문했습니다",
|
|
"user_id": "user123",
|
|
"importance": 4
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**성공 응답:**
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "4",
|
|
"result": {
|
|
"isError": false,
|
|
"content": "Memory stored successfully",
|
|
"parts": [],
|
|
"metadata": {
|
|
"tool": "store_memory",
|
|
"timestamp": 1701234567890
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**비즈니스 로직**: `MemoryService.storeTemporaryMemory()` 호출
|
|
|
|
---
|
|
|
|
##### retrieve_memory
|
|
|
|
사용자의 영구 메모리를 PostgreSQL에서 조회합니다.
|
|
|
|
**요청:**
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "5",
|
|
"method": "tools/call",
|
|
"params": {
|
|
"arguments": {
|
|
"name": "retrieve_memory",
|
|
"arguments": {
|
|
"user_id": "user123",
|
|
"limit": 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**성공 응답:**
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "5",
|
|
"result": {
|
|
"isError": false,
|
|
"content": "Retrieved 10 memories",
|
|
"parts": [],
|
|
"metadata": {
|
|
"tool": "retrieve_memory",
|
|
"timestamp": 1701234567890
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**비즈니스 로직**: `MemoryService.getPermanentMemories()` 호출
|
|
|
|
---
|
|
|
|
##### search_memory
|
|
|
|
벡터 유사도 기반으로 메모리를 검색합니다.
|
|
|
|
**요청:**
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "6",
|
|
"method": "tools/call",
|
|
"params": {
|
|
"arguments": {
|
|
"name": "search_memory",
|
|
"arguments": {
|
|
"query": "커피",
|
|
"user_id": "user123",
|
|
"limit": 5
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**성공 응답:**
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "6",
|
|
"result": {
|
|
"isError": false,
|
|
"content": "Found 3 matching memories",
|
|
"parts": [],
|
|
"metadata": {
|
|
"tool": "search_memory",
|
|
"timestamp": 1701234567890
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**비즈니스 로직**: `MemoryService.searchMemoriesByVector()` 호출
|
|
|
|
**에러 응답 예시:**
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "6",
|
|
"result": {
|
|
"isError": true,
|
|
"content": "Error: Unknown tool: invalid_tool",
|
|
"parts": [],
|
|
"metadata": {
|
|
"tool": "invalid_tool",
|
|
"timestamp": 1701234567890
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4. resources/list
|
|
|
|
사용 가능한 모든 리소스 목록을 반환합니다.
|
|
|
|
#### 요청
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "7",
|
|
"method": "resources/list"
|
|
}
|
|
```
|
|
|
|
#### 응답
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "7",
|
|
"result": {
|
|
"resources": [
|
|
{
|
|
"uri": "memory://recent",
|
|
"name": "Recent Memories",
|
|
"description": "최근 저장된 메모리 리소스",
|
|
"mimeType": "application/json"
|
|
},
|
|
{
|
|
"uri": "memory://long-term",
|
|
"name": "Long-term Memories",
|
|
"description": "장기 저장된 메모리 리소스",
|
|
"mimeType": "application/json"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**응답 DTO**: `McpResourceResponse.ResourceListResponse`
|
|
|
|
---
|
|
|
|
### 5. resources/read
|
|
|
|
특정 리소스의 내용을 읽습니다.
|
|
|
|
#### 요청
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "8",
|
|
"method": "resources/read",
|
|
"params": {
|
|
"uri": "memory://recent"
|
|
}
|
|
}
|
|
```
|
|
|
|
또는
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "8",
|
|
"method": "resources/read",
|
|
"params": {
|
|
"arguments": {
|
|
"uri": "memory://recent"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 응답
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "8",
|
|
"result": {
|
|
"uri": "memory://recent",
|
|
"mimeType": "application/json",
|
|
"text": "{\"message\": \"Resource content for memory://recent\"}",
|
|
"metadata": {
|
|
"uri": "memory://recent",
|
|
"timestamp": 1701234567890
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**응답 DTO**: `McpResourceResponse.ResourceReadResponse`
|
|
|
|
---
|
|
|
|
### 6. prompts/list
|
|
|
|
사용 가능한 모든 프롬프트 목록을 반환합니다.
|
|
|
|
#### 요청
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "9",
|
|
"method": "prompts/list"
|
|
}
|
|
```
|
|
|
|
#### 응답
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "9",
|
|
"result": {
|
|
"prompts": [
|
|
{
|
|
"name": "memory_summary",
|
|
"description": "메모리 목록을 요약하는 프롬프트",
|
|
"arguments": [
|
|
{
|
|
"name": "memories",
|
|
"description": "요약할 메모리 텍스트 목록",
|
|
"required": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "memory_search",
|
|
"description": "메모리 검색을 위한 프롬프트",
|
|
"arguments": [
|
|
{
|
|
"name": "query",
|
|
"description": "검색할 키워드",
|
|
"required": true
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**응답 DTO**: `McpPromptResponse.PromptListResponse`
|
|
|
|
---
|
|
|
|
### 7. prompts/get
|
|
|
|
특정 프롬프트를 가져옵니다.
|
|
|
|
#### 요청
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "10",
|
|
"method": "prompts/get",
|
|
"params": {
|
|
"arguments": {
|
|
"name": "memory_summary",
|
|
"arguments": {
|
|
"memories": ["메모리1", "메모리2", "메모리3"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 응답
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "10",
|
|
"result": {
|
|
"name": "memory_summary",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": "Prompt: memory_summary"
|
|
}
|
|
],
|
|
"arguments": {
|
|
"memories": ["메모리1", "메모리2", "메모리3"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**응답 DTO**: `McpPromptResponse.PromptGetResponse`
|
|
|
|
---
|
|
|
|
## 에러 응답
|
|
|
|
에러가 발생한 경우 다음과 같은 형식으로 응답됩니다:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": "1",
|
|
"error": {
|
|
"code": -32603,
|
|
"message": "Internal error",
|
|
"data": null
|
|
}
|
|
}
|
|
```
|
|
|
|
### 에러 코드
|
|
|
|
| 코드 | 의미 | 설명 |
|
|
|------|------|------|
|
|
| `-32600` | Invalid Request | 요청 형식이 잘못됨 |
|
|
| `-32601` | Method not found | 존재하지 않는 메서드 호출 |
|
|
| `-32602` | Invalid params | 파라미터가 잘못됨 |
|
|
| `-32603` | Internal error | 서버 내부 오류 |
|
|
| `-32700` | Parse error | JSON 파싱 오류 |
|
|
|
|
**에러 처리**: `GlobalExceptionHandler`에서 전역 예외 처리
|
|
|
|
---
|
|
|
|
## 사용 예시
|
|
|
|
### cURL 예시
|
|
|
|
#### initialize
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": "1",
|
|
"method": "initialize",
|
|
"params": {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {},
|
|
"clientInfo": {
|
|
"name": "test-client",
|
|
"version": "1.0.0"
|
|
}
|
|
}
|
|
}'
|
|
```
|
|
|
|
#### tools/list
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": "2",
|
|
"method": "tools/list"
|
|
}'
|
|
```
|
|
|
|
#### tools/call - store_memory
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": "3",
|
|
"method": "tools/call",
|
|
"params": {
|
|
"arguments": {
|
|
"name": "store_memory",
|
|
"arguments": {
|
|
"memory_text": "사용자는 파이썬을 좋아합니다",
|
|
"user_id": "user123",
|
|
"importance": 4
|
|
}
|
|
}
|
|
}
|
|
}'
|
|
```
|
|
|
|
#### tools/call - retrieve_memory
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": "4",
|
|
"method": "tools/call",
|
|
"params": {
|
|
"arguments": {
|
|
"name": "retrieve_memory",
|
|
"arguments": {
|
|
"user_id": "user123",
|
|
"limit": 10
|
|
}
|
|
}
|
|
}
|
|
}'
|
|
```
|
|
|
|
#### tools/call - search_memory
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": "5",
|
|
"method": "tools/call",
|
|
"params": {
|
|
"arguments": {
|
|
"name": "search_memory",
|
|
"arguments": {
|
|
"query": "커피",
|
|
"user_id": "user123",
|
|
"limit": 5
|
|
}
|
|
}
|
|
}
|
|
}'
|
|
```
|
|
|
|
---
|
|
|
|
## 데이터 모델
|
|
|
|
### Memory (도메인 엔티티)
|
|
|
|
PostgreSQL에 저장되는 영구 메모리 엔티티입니다.
|
|
|
|
```java
|
|
@Entity
|
|
@Table(name = "memories")
|
|
public class Memory {
|
|
private Long id;
|
|
private String userId;
|
|
private String memoryText;
|
|
private Integer importance; // 1~5
|
|
private String tags; // JSON 형식
|
|
private LocalDateTime createdAt;
|
|
private LocalDateTime updatedAt;
|
|
}
|
|
```
|
|
|
|
### TemporaryMemory (도메인 모델)
|
|
|
|
Redis에 저장되는 임시 메모리 모델입니다.
|
|
|
|
```java
|
|
public class TemporaryMemory {
|
|
private String id; // Redis key
|
|
private String userId;
|
|
private String memoryText;
|
|
private Integer importance;
|
|
private LocalDateTime createdAt;
|
|
private LocalDateTime expiresAt; // TTL 기반
|
|
}
|
|
```
|
|
|
|
### MemoryResponse (응답 DTO)
|
|
|
|
```java
|
|
public class MemoryResponse {
|
|
private Long id;
|
|
private String userId;
|
|
private String memoryText;
|
|
private Integer importance;
|
|
private String tags;
|
|
private LocalDateTime createdAt;
|
|
private LocalDateTime updatedAt;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 참고사항
|
|
|
|
### 1. 요청/응답 형식
|
|
|
|
- 모든 요청은 `Content-Type: application/json` 헤더가 필요합니다.
|
|
- `id` 필드는 요청과 응답을 매칭하기 위해 사용됩니다.
|
|
- 모든 응답은 `jsonrpc: "2.0"` 필드를 포함합니다.
|
|
|
|
### 2. 아키텍처
|
|
|
|
- **MVC 패턴**을 따르며, 각 계층이 명확히 분리되어 있습니다.
|
|
- **Controller**는 요청 처리만 담당합니다.
|
|
- **API Service**는 Controller와 Domain Service 사이의 어댑터 역할을 합니다.
|
|
- **Domain Service**는 비즈니스 로직을 처리합니다.
|
|
- **Repository**는 데이터 접근만 담당합니다.
|
|
|
|
### 3. 데이터 저장
|
|
|
|
- **임시 메모리**: Redis에 저장 (TTL 3일)
|
|
- **영구 메모리**: PostgreSQL에 저장
|
|
- **벡터 검색**: PostgreSQL의 pgvector 확장 사용 (향후 구현)
|
|
|
|
### 4. CORS 설정
|
|
|
|
- CORS는 모든 origin에서 허용되도록 설정되어 있습니다 (`/mcp/**`).
|
|
- 프로덕션 환경에서는 특정 origin만 허용하도록 변경하는 것을 권장합니다.
|
|
|
|
### 5. 예외 처리
|
|
|
|
- 전역 예외 처리는 `GlobalExceptionHandler`에서 수행됩니다.
|
|
- 유효성 검증 오류는 `MethodArgumentNotValidException`으로 처리됩니다.
|
|
- 모든 예외는 JSON-RPC 2.0 에러 형식으로 변환됩니다.
|
|
|
|
### 6. 구현 상태
|
|
|
|
- ✅ 기본 MCP 프로토콜 구현 완료
|
|
- ✅ 도메인 모델 및 Repository 생성 완료
|
|
- ✅ MVC 구조 재설계 완료
|
|
- ⏳ Redis 연동 (TODO)
|
|
- ⏳ 벡터 검색 구현 (TODO)
|
|
- ⏳ 배치 처리 구현 (TODO)
|
|
|
|
---
|
|
|
|
## 버전 정보
|
|
|
|
- **문서 버전**: 2.0.0
|
|
- **최종 업데이트**: 2025-12-10
|
|
- **서버 버전**: 0.0.1-SNAPSHOT
|
|
- **아키텍처**: MVC (Model-View-Controller)
|
|
|
|
---
|
|
|
|
## 관련 파일 구조
|
|
|
|
```
|
|
src/main/java/com/pandol365/dewey/
|
|
├── api/
|
|
│ ├── controller/
|
|
│ │ └── McpController.java # Controller 계층
|
|
│ ├── dto/
|
|
│ │ ├── request/ # 요청 DTO
|
|
│ │ │ ├── McpJsonRpcRequest.java
|
|
│ │ │ ├── MemoryStoreRequest.java
|
|
│ │ │ └── MemorySearchRequest.java
|
|
│ │ └── response/ # 응답 DTO (View)
|
|
│ │ ├── McpJsonRpcResponse.java
|
|
│ │ ├── McpInitializeResponse.java
|
|
│ │ ├── McpToolResponse.java
|
|
│ │ ├── McpResourceResponse.java
|
|
│ │ ├── McpPromptResponse.java
|
|
│ │ ├── McpToolCallResponse.java
|
|
│ │ └── MemoryResponse.java
|
|
│ └── service/ # API Service 계층
|
|
│ ├── McpService.java
|
|
│ └── impl/
|
|
│ └── McpServiceImpl.java
|
|
├── domain/
|
|
│ └── memory/
|
|
│ ├── model/ # 도메인 모델 (Model)
|
|
│ │ ├── Memory.java
|
|
│ │ └── TemporaryMemory.java
|
|
│ ├── repository/ # Repository 계층
|
|
│ │ └── MemoryRepository.java
|
|
│ └── service/ # Domain Service 계층
|
|
│ ├── MemoryService.java
|
|
│ └── impl/
|
|
│ └── MemoryServiceImpl.java
|
|
└── exception/
|
|
└── GlobalExceptionHandler.java # 예외 처리
|
|
```
|