feat: Refactor monolith to microservices architecture

This commit is contained in:
jade
2026-07-05 17:27:44 +09:00
parent 367a47516d
commit 10f3f0513d
200 changed files with 541 additions and 1789 deletions

34
axhub-common/build.gradle Normal file
View 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'
}

View 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'
}

View File

@@ -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);
}
}

View File

@@ -1,12 +1,9 @@
package io.shinhanlife.axhub.biz.mcp.gateway.controller; package io.shinhanlife.axhub.biz.mcp.gateway.controller;
import com.fasterxml.jackson.databind.ObjectMapper; 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.JsonRpcRequest;
import io.shinhanlife.axhub.biz.mcp.adapter.dto.JsonRpcResponse; 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.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.dto.ToolMetadata;
import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService; import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService;
import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService; 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 lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; 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.Map;
import java.util.UUID; import java.util.UUID;
@@ -32,85 +29,19 @@ public class McpRouterController {
private final RedisRegistryService redisRegistryService; private final RedisRegistryService redisRegistryService;
private final ExecuteService executeService; private final ExecuteService executeService;
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final LegacyEimsConnector legacyEimsConnector;
private final ThirdPartySecurityConnector thirdPartySecurityConnector;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final RestTemplate restTemplate = new RestTemplate();
// 생성자 주입
public McpRouterController(RedisRegistryService redisRegistryService, public McpRouterController(RedisRegistryService redisRegistryService,
ExecuteService executeService, ExecuteService executeService,
SecurityProperties securityProperties, SecurityProperties securityProperties,
LegacyEimsConnector legacyEimsConnector,
ThirdPartySecurityConnector thirdPartySecurityConnector,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
this.redisRegistryService = redisRegistryService; this.redisRegistryService = redisRegistryService;
this.executeService = executeService; this.executeService = executeService;
this.securityProperties = securityProperties; this.securityProperties = securityProperties;
this.legacyEimsConnector = legacyEimsConnector;
this.thirdPartySecurityConnector = thirdPartySecurityConnector;
this.objectMapper = objectMapper; 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)을 통해 레거시 툴을 호출합니다.") @Operation(summary = "MCP 파이프라인 호출", description = "MCP 표준 파이프라인(ExecuteService)을 통해 레거시 툴을 호출합니다.")
@PostMapping("/tools/call") @PostMapping("/tools/call")
public ResponseEntity<Object> callTool( public ResponseEntity<Object> callTool(
@@ -120,7 +51,6 @@ public class McpRouterController {
@RequestBody Map<String, Object> payload, @RequestBody Map<String, Object> payload,
@RequestAttribute(value = "tenantId", required = false) String tenantId) { @RequestAttribute(value = "tenantId", required = false) String tenantId) {
// 메타데이터 주입
java.util.Map<String, Object> meta = new java.util.HashMap<>(); java.util.Map<String, Object> meta = new java.util.HashMap<>();
meta.put("traceId", traceId != null ? traceId : java.util.UUID.randomUUID().toString()); meta.put("traceId", traceId != null ? traceId : java.util.UUID.randomUUID().toString());
meta.put("agentId", agentId != null ? agentId : "UNKNOWN"); meta.put("agentId", agentId != null ? agentId : "UNKNOWN");
@@ -128,21 +58,9 @@ public class McpRouterController {
payload.put("meta", meta); payload.put("meta", meta);
log.info("[MCP 표준] ExecuteService 파이프라인을 통한 툴 호출 시작 (Tenant: {}, Trace: {})", tenantId, meta.get("traceId")); 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 { try {
Object result = executeService.execute(payload, tenantId); 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); return ResponseEntity.ok(result);
} catch (SecurityException se) { } catch (SecurityException se) {
log.warn(" [보안 차단] 권한 오류: {}", se.getMessage()); log.warn(" [보안 차단] 권한 오류: {}", se.getMessage());
@@ -153,7 +71,6 @@ public class McpRouterController {
} }
} }
// 2. 기존 표준 규격 메서드들 (유지 동적 Registry 반영)
@GetMapping("/tools/list") @GetMapping("/tools/list")
public ResponseEntity<JsonRpcResponse> listTools() { public ResponseEntity<JsonRpcResponse> listTools() {
List<ToolMetadata> activeTools = redisRegistryService.getAllTools(); List<ToolMetadata> activeTools = redisRegistryService.getAllTools();

View File

@@ -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초당 허용 최대 요청 수
}

View File

@@ -1,60 +1,57 @@
package io.shinhanlife.axhub.biz.mcp.gateway.service; package io.shinhanlife.axhub.biz.mcp.gateway.service;
import io.shinhanlife.axhub.biz.mcp.adapter.dto.Params; import io.shinhanlife.axhub.biz.mcp.adapter.dto.Params;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; 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 java.util.Map;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; import java.util.List;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ExecuteService { public class ExecuteService {
// 파이프라인의 핵심 컴포넌트들
private final RequestValidator validator;
private final ToolPlanner planner; private final ToolPlanner planner;
private final ToolRouter router;
private final ToolClient client;
private final ResponseAggregator aggregator;
private final KillSwitchService killSwitchService; private final KillSwitchService killSwitchService;
private final RestTemplate restTemplate = new RestTemplate();
public Object execute(Map<String, Object> payload, String tenantId) { public Object execute(Map<String, Object> payload, String tenantId) {
log.info(" [ExecuteService] 전체 실행 흐름 제어 시작"); log.info(" [ExecuteService] 전체 실행 흐름 제어 시작");
// [비상 차단 1단계] Agent 단위 차단 검사
killSwitchService.checkAgent(tenantId); killSwitchService.checkAgent(tenantId);
// [비상 차단 2단계] Tool 단위 차단 검사
Map<String, Object> params = payload.containsKey("params") ? (Map<String, Object>) payload.get("params") : null; 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"); String toolName = params != null ? (String) params.get("name") : (String) payload.get("toolName");
killSwitchService.checkTool(toolName); killSwitchService.checkTool(toolName);
// 1. 검증 (Validator)
validator.validate(payload);
// 2. 실행 계획 생성 권한 검증 (Planner)
var planObj = planner.createPlan(payload, tenantId); var planObj = planner.createPlan(payload, tenantId);
ToolMetadata plan = (ToolMetadata) planObj; ToolMetadata plan = (ToolMetadata) planObj;
// [비상 차단 3단계] Route/Legacy 단위 차단 검사
if (plan.getIntegrationType() != null) { if (plan.getIntegrationType() != null) {
killSwitchService.checkRoute(plan.getIntegrationType()); killSwitchService.checkRoute(plan.getIntegrationType());
} }
// 3. 라우팅 (Router) - Redis의 Tool Registry 참조 // Dynamic Routing: Find the target Pod from the Redis registry
var targetPod = router.route(plan); String targetUrl = "http://localhost:8082"; // Fallback
if (plan.getPodUrl() != null && !plan.getPodUrl().isEmpty()) {
targetUrl = plan.getPodUrl();
}
// 4. 실제 Tool 호출 (Client) - Retry/Timeout 포함 (동적 제어 적용) log.info(" [ExecuteService] 라우팅 목적지: {}", targetUrl);
var rawResponse = client.call(targetPod, payload, plan); String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
// 5. 응답 정규화 병합 (Aggregator) // Forward the request to the target tool pod
return aggregator.normalize(rawResponse); 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