forked from kimhyungsik/ax_hub_mcp_tool
feat: Refactor monolith to microservices architecture
This commit is contained in:
34
axhub-common/build.gradle
Normal file
34
axhub-common/build.gradle
Normal file
@@ -0,0 +1,34 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
api 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
|
||||
api 'io.github.resilience4j:resilience4j-core:2.2.0'
|
||||
api 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
|
||||
api 'io.github.resilience4j:resilience4j-ratelimiter'
|
||||
api 'io.github.resilience4j:resilience4j-retry:2.2.0'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop:3.3.0'
|
||||
|
||||
api 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
|
||||
api 'com.h2database:h2'
|
||||
api 'p6spy:p6spy:3.9.1'
|
||||
api 'io.lettuce:lettuce-core:6.6.0.RELEASE'
|
||||
|
||||
api "org.mapstruct:mapstruct:1.5.5.Final"
|
||||
|
||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
|
||||
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||
api 'com.networknt:json-schema-validator:1.4.0'
|
||||
api 'org.springframework.kafka:spring-kafka:3.2.0'
|
||||
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
}
|
||||
29
axhub-gateway/build.gradle
Normal file
29
axhub-gateway/build.gradle
Normal file
@@ -0,0 +1,29 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':axhub-common')
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
// MyBatis & DB
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
implementation 'p6spy:p6spy:3.9.1'
|
||||
|
||||
// MapStruct
|
||||
implementation "org.mapstruct:mapstruct:1.5.5.Final"
|
||||
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
|
||||
annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final"
|
||||
|
||||
implementation 'com.networknt:json-schema-validator:1.4.0'
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.shinhanlife.axhub.biz.mcp.gateway;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableCaching
|
||||
public class AxHubGatewayApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AxHubGatewayApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package io.shinhanlife.axhub.biz.mcp.gateway.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.axhub.biz.mcp.adapter.connector.LegacyEimsConnector;
|
||||
import io.shinhanlife.axhub.biz.mcp.adapter.connector.ThirdPartySecurityConnector;
|
||||
import io.shinhanlife.axhub.biz.mcp.adapter.dto.JsonRpcRequest;
|
||||
import io.shinhanlife.axhub.biz.mcp.adapter.dto.JsonRpcResponse;
|
||||
import io.shinhanlife.axhub.biz.mcp.adapter.dto.Params;
|
||||
import io.shinhanlife.axhub.biz.mcp.adapter.util.PiiMaskingUtils;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService;
|
||||
@@ -15,7 +12,7 @@ import io.shinhanlife.axhub.common.mcp.security.SecurityProperties;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -32,85 +29,19 @@ public class McpRouterController {
|
||||
private final RedisRegistryService redisRegistryService;
|
||||
private final ExecuteService executeService;
|
||||
private final SecurityProperties securityProperties;
|
||||
private final LegacyEimsConnector legacyEimsConnector;
|
||||
private final ThirdPartySecurityConnector thirdPartySecurityConnector;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
// 생성자 주입
|
||||
public McpRouterController(RedisRegistryService redisRegistryService,
|
||||
ExecuteService executeService,
|
||||
SecurityProperties securityProperties,
|
||||
LegacyEimsConnector legacyEimsConnector,
|
||||
ThirdPartySecurityConnector thirdPartySecurityConnector,
|
||||
ObjectMapper objectMapper) {
|
||||
this.redisRegistryService = redisRegistryService;
|
||||
this.executeService = executeService;
|
||||
this.securityProperties = securityProperties;
|
||||
this.legacyEimsConnector = legacyEimsConnector;
|
||||
this.thirdPartySecurityConnector = thirdPartySecurityConnector;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Operation(summary = "Adapter 툴 실행 라우팅 (로컬 모듈화)", description = "AI Agent의 요청을 받아 직접 레거시 시스템을 호출합니다.")
|
||||
@PostMapping("/execute-tool")
|
||||
public ResponseEntity<Map<String, Object>> routeToPreBuildTool(@RequestBody Map<String, Object> agentPayload) {
|
||||
String trackId = UUID.randomUUID().toString();
|
||||
log.info("[{}] AI Agent로부터 툴 실행 요청 수신 완료", trackId);
|
||||
log.info("[{}] AI Agent가 보낸 원본 payload: {}", trackId, agentPayload);
|
||||
|
||||
try {
|
||||
// 1. 요청 파싱 (Map -> JsonRpcRequest)
|
||||
agentPayload.put("id", trackId);
|
||||
JsonRpcRequest request = objectMapper.convertValue(agentPayload, JsonRpcRequest.class);
|
||||
Params params = request.getParams();
|
||||
|
||||
if (params == null) {
|
||||
throw new IllegalArgumentException("Invalid params: params 객체가 비어있습니다.");
|
||||
}
|
||||
|
||||
String routingType = params.getRoutingType();
|
||||
String interfaceId = params.getInterfaceId();
|
||||
|
||||
// 2. 라우터 실행 (분기 처리) - HTTP 통신 없이 직접 호출!
|
||||
String executionResult;
|
||||
if (interfaceId != null && (interfaceId.startsWith("DRM_") || interfaceId.startsWith("BM_"))) {
|
||||
executionResult = thirdPartySecurityConnector.executeSecurityModule(interfaceId, params.getData());
|
||||
} else {
|
||||
executionResult = legacyEimsConnector.executeByTool(routingType, interfaceId, params.getData(), params.getSpec());
|
||||
}
|
||||
|
||||
// 3. PII 마스킹
|
||||
String maskedResult = PiiMaskingUtils.mask(executionResult);
|
||||
|
||||
// 4. 응답 조립 (JsonRpcResponse -> Map)
|
||||
JsonRpcResponse response = new JsonRpcResponse();
|
||||
response.setId(trackId);
|
||||
response.setResult(maskedResult);
|
||||
|
||||
Map<String, Object> rpcResponse = objectMapper.convertValue(response, Map.class);
|
||||
log.info("[{}] 로컬 Adapter 처리 성공", trackId);
|
||||
|
||||
return ResponseEntity.ok(rpcResponse);
|
||||
|
||||
} catch (ResourceAccessException e) {
|
||||
log.error("[{}] Adapter 시스템 응답 시간 초과 (Timeout) 발생! 사유: {}", trackId, e.getMessage());
|
||||
return ResponseEntity.status(504).body(Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"error", Map.of("code", -32603, "message", "인터페이스 서버 연결 시간 초과 (Timeout)"),
|
||||
"id", trackId
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[{}] 시스템 예외 에러 발생: {}", trackId, e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"error", Map.of("code", -32000, "message", "내부 게이트웨이 서버 오류: " + e.getMessage()),
|
||||
"id", trackId
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 1. [신규] 표준 MCP 파이프라인 호출 (가장 중요!)
|
||||
@Operation(summary = "MCP 파이프라인 호출", description = "MCP 표준 파이프라인(ExecuteService)을 통해 레거시 툴을 호출합니다.")
|
||||
@PostMapping("/tools/call")
|
||||
public ResponseEntity<Object> callTool(
|
||||
@@ -120,7 +51,6 @@ public class McpRouterController {
|
||||
@RequestBody Map<String, Object> payload,
|
||||
@RequestAttribute(value = "tenantId", required = false) String tenantId) {
|
||||
|
||||
// 메타데이터 주입
|
||||
java.util.Map<String, Object> meta = new java.util.HashMap<>();
|
||||
meta.put("traceId", traceId != null ? traceId : java.util.UUID.randomUUID().toString());
|
||||
meta.put("agentId", agentId != null ? agentId : "UNKNOWN");
|
||||
@@ -128,21 +58,9 @@ public class McpRouterController {
|
||||
payload.put("meta", meta);
|
||||
|
||||
log.info("[MCP 표준] ExecuteService 파이프라인을 통한 툴 호출 시작 (Tenant: {}, Trace: {})", tenantId, meta.get("traceId"));
|
||||
if (payload != null) {
|
||||
try {
|
||||
log.info(" [Gateway] AI Agent 요청 파라미터: {}", objectMapper.writeValueAsString(payload));
|
||||
} catch (Exception e) {
|
||||
log.info(" [Gateway] AI Agent 요청 파라미터: {}", payload);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Object result = executeService.execute(payload, tenantId);
|
||||
try {
|
||||
log.info("\n [MCP Gateway -> AI Agent] 최종 응답 반환: {}", objectMapper.writeValueAsString(result));
|
||||
} catch (Exception e) {
|
||||
log.info("\n [MCP Gateway -> AI Agent] 최종 응답 반환: {}", result);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (SecurityException se) {
|
||||
log.warn(" [보안 차단] 권한 오류: {}", se.getMessage());
|
||||
@@ -153,7 +71,6 @@ public class McpRouterController {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 기존 표준 규격 메서드들 (유지 및 동적 Registry 반영)
|
||||
@GetMapping("/tools/list")
|
||||
public ResponseEntity<JsonRpcResponse> listTools() {
|
||||
List<ToolMetadata> activeTools = redisRegistryService.getAllTools();
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.shinhanlife.axhub.biz.mcp.gateway.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스
|
||||
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ToolMetadata {
|
||||
|
||||
// 1. Tool 기본 정보
|
||||
private String toolName; // 툴 고유 명칭 (예: CustomerSearchTool)
|
||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
||||
|
||||
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
|
||||
private Map<String, Object> parametersSchema;
|
||||
|
||||
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
||||
private Map<String, String> actionPrompts;
|
||||
|
||||
// 2-1. 도메인 부서 그룹명 (권한 관리에 사용)
|
||||
private String domainGroup;
|
||||
|
||||
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
|
||||
private String endpoint;
|
||||
|
||||
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
|
||||
private String podUrl;
|
||||
|
||||
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
|
||||
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
|
||||
|
||||
// 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용)
|
||||
private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992)
|
||||
|
||||
// 5. 인프라 상태 정보 (DIRECT 연동 시 사용)
|
||||
private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프
|
||||
|
||||
// 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반)
|
||||
private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%)
|
||||
private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수
|
||||
private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수
|
||||
}
|
||||
@@ -1,60 +1,57 @@
|
||||
package io.shinhanlife.axhub.biz.mcp.gateway.service;
|
||||
|
||||
import io.shinhanlife.axhub.biz.mcp.adapter.dto.Params;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.router.RequestValidator;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.router.ToolRouter;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.router.ResponseAggregator;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.client.ToolClient;
|
||||
import java.util.Map;
|
||||
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExecuteService {
|
||||
|
||||
// 파이프라인의 핵심 컴포넌트들
|
||||
private final RequestValidator validator;
|
||||
private final ToolPlanner planner;
|
||||
private final ToolRouter router;
|
||||
private final ToolClient client;
|
||||
private final ResponseAggregator aggregator;
|
||||
private final KillSwitchService killSwitchService;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public Object execute(Map<String, Object> payload, String tenantId) {
|
||||
log.info(" [ExecuteService] 전체 실행 흐름 제어 시작");
|
||||
|
||||
// [비상 차단 1단계] Agent 단위 차단 검사
|
||||
killSwitchService.checkAgent(tenantId);
|
||||
|
||||
// [비상 차단 2단계] Tool 단위 차단 검사
|
||||
Map<String, Object> params = payload.containsKey("params") ? (Map<String, Object>) payload.get("params") : null;
|
||||
String toolName = params != null ? (String) params.get("name") : (String) payload.get("toolName");
|
||||
killSwitchService.checkTool(toolName);
|
||||
|
||||
// 1. 검증 (Validator)
|
||||
validator.validate(payload);
|
||||
|
||||
// 2. 실행 계획 생성 및 권한 검증 (Planner)
|
||||
var planObj = planner.createPlan(payload, tenantId);
|
||||
ToolMetadata plan = (ToolMetadata) planObj;
|
||||
|
||||
// [비상 차단 3단계] Route/Legacy 단위 차단 검사
|
||||
if (plan.getIntegrationType() != null) {
|
||||
killSwitchService.checkRoute(plan.getIntegrationType());
|
||||
}
|
||||
|
||||
// 3. 라우팅 (Router) - Redis의 Tool Registry 참조
|
||||
var targetPod = router.route(plan);
|
||||
// Dynamic Routing: Find the target Pod from the Redis registry
|
||||
String targetUrl = "http://localhost:8082"; // Fallback
|
||||
if (plan.getPodUrl() != null && !plan.getPodUrl().isEmpty()) {
|
||||
targetUrl = plan.getPodUrl();
|
||||
}
|
||||
|
||||
// 4. 실제 Tool 호출 (Client) - Retry/Timeout 포함 (동적 제어 적용)
|
||||
var rawResponse = client.call(targetPod, payload, plan);
|
||||
log.info(" [ExecuteService] 라우팅 목적지: {}", targetUrl);
|
||||
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
|
||||
|
||||
// 5. 응답 정규화 및 병합 (Aggregator)
|
||||
return aggregator.normalize(rawResponse);
|
||||
// Forward the request to the target tool pod
|
||||
try {
|
||||
ResponseEntity<Object> response = restTemplate.postForEntity(executeApiUrl, payload, Object.class);
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
log.error(" [ExecuteService] Tool Pod 호출 실패: {}", e.getMessage());
|
||||
throw new RuntimeException("Tool Pod 호출 실패: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user