feat: Separate Gateway and Tool server architectures, implement Tool Auto-Registration, and add mock response mapping

This commit is contained in:
jade
2026-07-04 00:22:44 +09:00
parent 93b2d6e205
commit a3e4dc7e38
9 changed files with 191 additions and 8 deletions

View File

@@ -77,7 +77,6 @@ dependencies {
} }
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
options.compilerArgs = [ options.compilerArgs << '-parameters'
'-Amapstruct.defaultComponentModel=spring' options.compilerArgs << '-Amapstruct.defaultComponentModel=spring'
]
} }

View File

@@ -0,0 +1,22 @@
package io.shinhanlife;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import java.util.Collections;
@SpringBootApplication
@ComponentScan(basePackages = {
"io.shinhanlife.axhub.biz.mcp.gateway",
"io.shinhanlife.axhub.biz.mcp.adapter",
"io.shinhanlife.axhub.common.mcp"
})
public class AxHubGatewayApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(AxHubGatewayApplication.class);
// Gateway는 8081 포트에서 실행
app.setDefaultProperties(Collections.singletonMap("server.port", "8081"));
app.run(args);
}
}

View File

@@ -0,0 +1,31 @@
package io.shinhanlife;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.Collections;
@SpringBootApplication
@EnableScheduling
@ComponentScan(basePackages = {
"io.shinhanlife.axhub.biz.mcp.tool",
"io.shinhanlife.axhub.biz.mcp.adapter",
"io.shinhanlife.axhub.common.mcp"
})
public class AxHubToolApplication {
public static void main(String[] args) {
// 기본 포트는 8082로 설정하되, 파라미터(--server.port=808x)가 있으면 해당 포트로 설정
String port = "8082";
for (String arg : args) {
if (arg.startsWith("--server.port=")) {
port = arg.substring("--server.port=".length());
}
}
System.setProperty("server.port", port);
SpringApplication app = new SpringApplication(AxHubToolApplication.class);
app.run(args);
}
}

View File

@@ -153,7 +153,11 @@ public class McpRouterController {
@PostMapping("/registry/heartbeat") @PostMapping("/registry/heartbeat")
public ResponseEntity<String> heartbeat(@RequestBody String toolName) { public ResponseEntity<String> heartbeat(@RequestBody String toolName) {
redisRegistryService.refreshHeartbeat(toolName); boolean success = redisRegistryService.refreshHeartbeat(toolName);
return ResponseEntity.ok("Heartbeat updated"); if (success) {
return ResponseEntity.ok("Heartbeat updated");
} else {
return ResponseEntity.status(404).body("Tool not found");
}
} }
} }

View File

@@ -34,13 +34,15 @@ public class RedisRegistryService {
/** /**
* 하트비트 갱신 (TTL 초기화) * 하트비트 갱신 (TTL 초기화)
*/ */
public void refreshHeartbeat(String toolName) { public boolean refreshHeartbeat(String toolName) {
String key = KEY_PREFIX + toolName; String key = KEY_PREFIX + toolName;
Boolean exists = redisTemplate.expire(key, DEFAULT_TTL); Boolean exists = redisTemplate.expire(key, DEFAULT_TTL);
if (Boolean.TRUE.equals(exists)) { if (Boolean.TRUE.equals(exists)) {
log.debug(" [RedisRegistry] 하트비트 갱신: {}", toolName); log.debug(" [RedisRegistry] 하트비트 갱신: {}", toolName);
return true;
} else { } else {
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", toolName); log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", toolName);
return false;
} }
} }
public List<String> getAvailablePods(String toolName) { public List<String> getAvailablePods(String toolName) {

View File

@@ -212,6 +212,7 @@ public class MockToolAutoRegistrar {
HttpHeaders headers = createHeaders(); HttpHeaders headers = createHeaders();
headers.setContentType(MediaType.TEXT_PLAIN); headers.setContentType(MediaType.TEXT_PLAIN);
boolean needsReRegistration = false;
for (ToolConfig tool : TOOLS) { for (ToolConfig tool : TOOLS) {
try { try {
String heartbeatUrl = gatewayUrl + "/registry/heartbeat"; String heartbeatUrl = gatewayUrl + "/registry/heartbeat";
@@ -224,8 +225,14 @@ public class MockToolAutoRegistrar {
log.info(" [MockTool] {} Heartbeat 서버 전송 완료", tool.getName()); log.info(" [MockTool] {} Heartbeat 서버 전송 완료", tool.getName());
} catch (Exception e) { } catch (Exception e) {
log.error(" [MockTool] {} Heartbeat 전송 실패: {}", tool.getName(), e.getMessage()); log.error(" [MockTool] {} Heartbeat 전송 실패: {}", tool.getName(), e.getMessage());
needsReRegistration = true;
} }
} }
if (needsReRegistration) {
log.info(" [MockTool] 하트비트 실패로 인해 전체 툴 재등록을 시도합니다.");
registerOnStartup();
}
} }
@EventListener(ContextClosedEvent.class) @EventListener(ContextClosedEvent.class)

View File

@@ -1,7 +1,26 @@
# Local 환경 전용 설정 (H2 메모리 DB 등) # Local ?˜ê²½ ?„ìš© ?¤ì • (H2 메모ë¦?DB ??
spring.datasource.url=jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; spring.datasource.url=jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=sa spring.datasource.username=sa
spring.datasource.password=password spring.datasource.password=password
spring.h2.console.enabled=true spring.h2.console.enabled=true
# EIMS µ¿Àû ¶ó¿ìÆÃ Á¢¼Ó Á¤º¸
eims.http.url=http://localhost:8081/api/gateway
eims.tcp.host=127.0.0.1
eims.tcp.port=8090
eims.tcp.timeout=5000
eims.jsp.form.url=http://localhost:8081/mock/jsp-form
eims.jsp.json.url=http://localhost:8081/mock/jsp-json
eims.mci.url=http://localhost:8081/mock/esb/api
# API º¸¾È Ű ¼³Á¤
mcp.security.api-keys.SHINHAN_MCP_SECRET_KEY_2026=mcp-client-1
mcp.security.api-keys.SHINHAN_MCP_TEST_KEY_9999=mcp-client-2
mcp.security.tenant-domains.mcp-client-1=CUSTOMER,COMMON
mcp.security.tenant-domains.mcp-client-2=ALL
# Gateway/Tool URLs
mcp.adapter.url=http://localhost:8081/rpc/v1/execute
mcp.gateway.url=http://localhost:8081/mcp/api/v1
mcp.tool.host=localhost

View File

@@ -1,5 +1,5 @@
spring.application.name=axhub-admin spring.application.name=axhub-admin
server.port=8080 server.port=8081
# 기본적으로 local 환경을 실행 # 기본적으로 local 환경을 실행
spring.profiles.active=local spring.profiles.active=local

View File

@@ -0,0 +1,99 @@
{
"BILL_001": {
"status": "SUCCESS",
"message": "청구 심사 상태 조회가 완료되었습니다.",
"data": {
"billingStatus": "PROCESSING",
"expectedCompletionDate": "2026-07-05"
}
},
"BILL_002": {
"status": "SUCCESS",
"message": "청구 처리가 완료되었습니다.",
"data": {
"processId": "PRC-998811",
"result": "APPROVED"
}
},
"BOND_001": {
"status": "SUCCESS",
"message": "디지털 증권 발행 가능 한도 조회가 완료되었습니다.",
"data": {
"availableLimit": 500000000,
"currency": "KRW"
}
},
"BOND_002": {
"status": "SUCCESS",
"message": "디지털 증권 발행이 성공적으로 완료되었습니다.",
"data": {
"bondId": "BND-2026-07-04-1234",
"issuedAmount": 10000000
}
},
"HR_VAC_01": {
"status": "SUCCESS",
"message": "연차 휴가 등록이 완료되었습니다.",
"data": {
"vacationId": "VAC-8877",
"status": "APPROVED"
}
},
"HR_VAC_02": {
"status": "SUCCESS",
"message": "잔여 연차 일수 조회가 완료되었습니다.",
"data": {
"totalDays": 15,
"usedDays": 3,
"remainingDays": 12
}
},
"COM_SMS_01": {
"status": "SUCCESS",
"message": "SMS 발송이 성공적으로 완료되었습니다.",
"data": {
"messageId": "SMS-11223344",
"sentTime": "2026-07-04 10:00:00"
}
},
"COM_EML_01": {
"status": "SUCCESS",
"message": "이메일 발송이 성공적으로 완료되었습니다.",
"data": {
"emailId": "EML-998877",
"sentTime": "2026-07-04 10:00:00"
}
},
"CNTR_001": {
"status": "SUCCESS",
"message": "계약 상태 조회가 완료되었습니다.",
"data": {
"contractStatus": "ACTIVE",
"startDate": "2024-01-01"
}
},
"CNTR_002": {
"status": "SUCCESS",
"message": "계약 상세 내역 조회가 완료되었습니다.",
"data": {
"productName": "신한 라이프 스마트 연금보험",
"monthlyPremium": 500000
}
},
"CRM_001": {
"status": "SUCCESS",
"message": "고객 등급 조회가 완료되었습니다.",
"data": {
"customerGrade": "VIP",
"loyaltyPoints": 15000
}
},
"CRM_002": {
"status": "SUCCESS",
"message": "고객 상세 정보 조회가 완료되었습니다.",
"data": {
"address": "서울특별시 중구 세종대로 9",
"phoneNumber": "010-XXXX-XXXX"
}
}
}