refactor: remove unused eims senders
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.aop;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.aop
|
||||
* @className EimsMonitoringAspect
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class EimsMonitoringAspect {
|
||||
|
||||
// 포인트컷: io.shinhanlife.dap.lib.adapter.sender 패키지 내의 EimsSender를 구현한 모든 클래스의 메서드를 타겟으로 지정합니다.
|
||||
@Around("execution(* io.shinhanlife.dap.lib.adapter.sender.*EimsSender.*(..))")
|
||||
public Object monitorEimsCommunication(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
|
||||
// 1. 호출되는 클래스와 메서드 이름 추출
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
Object[] args = joinPoint.getArgs(); // 파라미터
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 2. [요청 로깅] EIMS망으로 요청이 나가기 직전
|
||||
log.info(" [EIMS 요청] {} - {}() | Params: {}", className, methodName, Arrays.toString(args));
|
||||
|
||||
try {
|
||||
// 실제 레거시 통신 로직 실행 (이 코드가 없으면 통신이 진행되지 않습니다)
|
||||
Object result = joinPoint.proceed();
|
||||
|
||||
// 3. [응답 로깅] 정상적으로 통신이 완료된 후
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.info("◀ [EIMS 응답] {} - {}() | 소요시간: {}ms | Result: {}", className, methodName, executionTime, result);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 4. [에러 로깅] 레거시 통신 중 장애(타임아웃 등) 발생 시
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.error(" [EIMS 에러] {} - {}() | 소요시간: {}ms | Error: {}", className, methodName, executionTime, e.getMessage());
|
||||
|
||||
// 에러를 삼키지 않고 다시 던져서 기존 예외 처리 로직(서킷 브레이커 등)이 작동하게 합니다.
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.exception;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.exception
|
||||
* @className MciCommunicationException
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class MciCommunicationException extends RuntimeException {
|
||||
|
||||
public MciCommunicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MciCommunicationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import io.shinhanlife.dap.lib.adapter.support.TicketManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className EaiEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("eaiEimsSender")
|
||||
@RequiredArgsConstructor
|
||||
public class EaiEimsSender implements EimsSender {
|
||||
|
||||
private final ObjectMapper jsonMapper;
|
||||
private final XmlMapper xmlMapper;
|
||||
|
||||
// 실전 코드: 스프링이 제공하는 카프카 템플릿 주입
|
||||
private final KafkaTemplate<String, String> kafkaTemplate;
|
||||
|
||||
// 비동기 티켓 매니저
|
||||
private final TicketManager ticketManager;
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String jsonPayload) throws Exception {
|
||||
StopWatch stopWatch = new StopWatch(); stopWatch.start();
|
||||
|
||||
try {
|
||||
JsonNode jsonNode = jsonMapper.readTree(jsonPayload);
|
||||
String xmlData = xmlMapper.writeValueAsString(jsonNode);
|
||||
String esbStandardXml = wrapWithEaiHeader(interfaceId, xmlData);
|
||||
|
||||
log.info(" [EAI 어댑터] Kafka 토픽(eai-topic)으로 전송 시도...");
|
||||
|
||||
try {
|
||||
// 실전 코드 적용: Kafka로 메시지 발행
|
||||
kafkaTemplate.send("eai-topic", esbStandardXml);
|
||||
log.info(" [EAI 어댑터] Kafka 전송 완료!");
|
||||
} catch (Exception e) {
|
||||
// 로컬 환경에는 카프카가 없으므로 에러가 날 수 있습니다. 테스트를 위해 로깅만 하고 넘깁니다.
|
||||
log.warn(" 로컬 환경이거나 Ka<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
|
||||
"<EaiMessage>\n" +
|
||||
" <Header>\n" +
|
||||
" <ChannelId>MCP_GATEWAY_ASYNC</ChannelId>\n" +
|
||||
" <InterfaceId>EAI_BATCH_JOB</InterfaceId>\n" +
|
||||
" <Timestamp>1782705901850</Timestamp>\n" +
|
||||
" <TransferType>ASYNC</TransferType>\n" +
|
||||
" </Header>\n" +
|
||||
" <Body>\n" +
|
||||
" <ObjectNode>\n" +
|
||||
" <batchId>BATCH_20260629_001</batchId>\n" +
|
||||
" <targetSystem>GLOBAL_MINIMUM_TAX_SYS</targetSystem>\n" +
|
||||
" <recordCount>50000</recordCount>\n" +
|
||||
" </ObjectNode>\n" +
|
||||
" </Body>\n" +
|
||||
"</EaiMessage>fka 서버에 연결할 수 없습니다. (메시지 출력으로 대체합니다) \n전송하려던 메시지: {}", esbStandardXml);
|
||||
}
|
||||
|
||||
// 비동기 폴링을 위한 티켓 발급
|
||||
String ticketId = ticketManager.issueTicket(interfaceId, jsonPayload);
|
||||
|
||||
return String.format(
|
||||
"{\"status\":\"PROCESSING\", \"interfaceId\":\"%s\", \"ticketId\":\"%s\", \"message\":\"비동기 작업이 접수되었습니다. 상태 조회 API를 통해 결과를 확인하세요.\"}",
|
||||
interfaceId, ticketId);
|
||||
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
log.info(" [SLA 모니터링 - EAI] 소요시간: {} ms", stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
private String wrapWithEaiHeader(String interfaceId, String xmlData) {
|
||||
// 비동기 EAI는 추적을 위해 TransferType이나 Batch ID 같은 속성이 추가로 들어가는 경우가 많습니다.
|
||||
return String.format(
|
||||
"<EaiMessage>" +
|
||||
"<Header>" +
|
||||
"<ChannelId>MCP_GATEWAY_ASYNC</ChannelId>" +
|
||||
"<InterfaceId>%s</InterfaceId>" +
|
||||
"<Timestamp>%d</Timestamp>" +
|
||||
"<TransferType>ASYNC</TransferType>" +
|
||||
"</Header>" +
|
||||
"<Body>%s</Body>" +
|
||||
"</EaiMessage>",
|
||||
interfaceId, System.currentTimeMillis(), xmlData
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className EimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public interface EimsSender {
|
||||
// 프로토콜에 상관없이 이 메서드 하나로 통일합니다.
|
||||
String send(String interfaceId, String payload) throws Exception;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import io.shinhanlife.dap.lib.config.GlowCommunicationProperties;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className HttpEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HttpEimsSender implements EimsSender {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final String eimsUrl;
|
||||
private final GlowCommunicationProperties glowProps;
|
||||
|
||||
public HttpEimsSender(GlowCommunicationProperties glowProps) {
|
||||
this.glowProps = glowProps;
|
||||
// yml의 대내 MCI host, port, uri를 조합하여 EIMS 호출 주소 생성
|
||||
this.eimsUrl = glowProps.getMci().getHost() + ":" + glowProps.getMci().getPort() + glowProps.getMci().getUri();
|
||||
|
||||
/*
|
||||
*********************************************** 중요 **************************************************
|
||||
this.restClient = RestClient.create();
|
||||
보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다.
|
||||
*********************************************** 중요 **************************************************
|
||||
*/
|
||||
|
||||
// HTTP/2 통신 시 Stream Cancelled(RST_STREAM) 에러 방지를 위해 HTTP/1.1 전용 Factory 사용
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(3000);
|
||||
factory.setReadTimeout(5000);
|
||||
|
||||
this.restClient = RestClient.builder().requestFactory(factory).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) {
|
||||
log.info(" [HTTP 모드] EIMS API 호출 중... URL: {}", eimsUrl);
|
||||
|
||||
// EIMS가 요구하는 JSON 포맷으로 래핑해서 전송 (EIMS 규격에 따라 수정 가능)
|
||||
Map<String, String> requestBody = Map.of(
|
||||
"interfaceId", interfaceId,
|
||||
"data", payload
|
||||
);
|
||||
|
||||
// 4번 항목 적용: MDC에 저장된 traceId를 추출하여 HTTP Header(X-Trace-Id)로 전파
|
||||
String traceId = MDC.get("traceId");
|
||||
if (traceId == null) traceId = "SYSTEM-GENERATED-" + UUID.randomUUID().toString();
|
||||
|
||||
return restClient.post()
|
||||
.uri(eimsUrl)
|
||||
.header("X-Trace-Id", traceId)
|
||||
.header("X-Shinhan-Global-ID", traceId)
|
||||
.body(requestBody)
|
||||
.retrieve()
|
||||
.body(String.class); // 응답 결과를 String으로 받음
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className JspFormEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JspFormEimsSender implements EimsSender {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final String jspUrl;
|
||||
|
||||
public JspFormEimsSender(@Value("${eims.jsp.form.url}") String jspUrl) {
|
||||
this.jspUrl = jspUrl;
|
||||
this.restClient = RestClient.builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) {
|
||||
log.info(" [JSP Form 모드] 레거시 폼 데이터 전송 중... URL: {}", jspUrl);
|
||||
|
||||
// 1. Form Data 조립 (HTML <form> 태그 전송과 동일한 효과)
|
||||
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("interfaceId", interfaceId);
|
||||
formData.add("data", payload);
|
||||
|
||||
// 2. HTTP 전송 (Content-Type: application/x-www-form-urlencoded)
|
||||
String rawResponse = restClient.post()
|
||||
.uri(jspUrl)
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(formData)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
// 3. JSP 특유의 앞뒤 공백 및 엔터 제거
|
||||
return rawResponse != null ? rawResponse.trim() : "";
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className JspJsonEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JspJsonEimsSender implements EimsSender {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final String jspUrl;
|
||||
|
||||
// 여기서 eims.jsp.json.url 딱 하나만 깔끔하게 받아옵니다!
|
||||
public JspJsonEimsSender(@Value("${eims.jsp.json.url}") String jspUrl) {
|
||||
this.jspUrl = jspUrl;
|
||||
this.restClient = RestClient.builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) {
|
||||
log.info(" [JSP JSON 모드] JSON 페이로드 전송 중... URL: {}", jspUrl);
|
||||
|
||||
// 1. JSON 객체로 조립 (스프링이 알아서 JSON String으로 변환해 줌)
|
||||
Map<String, String> jsonBody = Map.of(
|
||||
"interfaceId", interfaceId,
|
||||
"data", payload
|
||||
);
|
||||
|
||||
// 2. HTTP 전송 (Content-Type: application/json)
|
||||
String rawResponse = restClient.post()
|
||||
.uri(jspUrl)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(jsonBody)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
// 3. JSP 응답 정제
|
||||
return rawResponse != null ? rawResponse.trim() : "";
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className MciEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("mciEimsSender")
|
||||
public class MciEimsSender implements EimsSender {
|
||||
|
||||
private final ObjectMapper jsonMapper;
|
||||
private final XmlMapper xmlMapper;
|
||||
private final RestClient restClient; // Spring Boot 3.2+ 최신 HTTP 클라이언트
|
||||
private final String mciUrl;
|
||||
|
||||
public MciEimsSender(ObjectMapper jsonMapper, XmlMapper xmlMapper, @Value("${eims.mci.url}") String mciUrl) {
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.xmlMapper = xmlMapper;
|
||||
this.mciUrl = mciUrl;
|
||||
this.restClient = RestClient.create(); // 클라이언트 초기화
|
||||
|
||||
/*
|
||||
*********************************************** 중요 **************************************************
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(3000);
|
||||
factory.setReadTimeout(5000);
|
||||
this.restClient = RestClient.builder().requestFactory(factory).build();
|
||||
보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다.
|
||||
*********************************************** 중요 **************************************************
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String jsonPayload) throws Exception {
|
||||
StopWatch stopWatch = new StopWatch(); stopWatch.start();
|
||||
|
||||
try {
|
||||
JsonNode jsonNode = jsonMapper.readTree(jsonPayload);
|
||||
String xmlData = xmlMapper.writer().withRootName("Body").writeValueAsString(jsonNode);
|
||||
String esbStandardXml = wrapWithEsbHeader(interfaceId, xmlData);
|
||||
|
||||
log.info(" [ESB 어댑터] 전송 준비 완료 - RestClient 호출 시작");
|
||||
|
||||
String responseXml = restClient.post()
|
||||
.uri(mciUrl)
|
||||
.contentType(MediaType.APPLICATION_XML)
|
||||
.body(esbStandardXml)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
log.info(" [ESB 어댑터] 응답 수신 완료: {}", responseXml);
|
||||
|
||||
JsonNode responseNode = xmlMapper.readTree(responseXml);
|
||||
return jsonMapper.writeValueAsString(responseNode);
|
||||
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
log.info(" [SLA 모니터링 - MCI] 소요시간: {} ms", stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
private String wrapWithEsbHeader(String interfaceId, String xmlData) {
|
||||
return String.format("<EsbMessage><Header><InterfaceId>%s</InterfaceId></Header><Body>%s</Body></EsbMessage>", interfaceId, xmlData);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className MciStringEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("mciStringEimsSender")
|
||||
public class MciStringEimsSender implements EimsSender {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final String mciUrl;
|
||||
|
||||
public MciStringEimsSender(@Value("${eims.mcistring.url}") String mciUrl) {
|
||||
this.mciUrl = mciUrl;
|
||||
this.restClient = RestClient.create();
|
||||
/*
|
||||
*********************************************** 중요 **************************************************
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(3000);
|
||||
factory.setReadTimeout(5000);
|
||||
this.restClient = RestClient.builder().requestFactory(factory).build();
|
||||
보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다.
|
||||
*********************************************** 중요 **************************************************
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) throws Exception {
|
||||
StopWatch stopWatch = new StopWatch(); stopWatch.start();
|
||||
|
||||
try {
|
||||
log.info(" [ESB 어댑터(String)] 전송 준비 완료 - RestClient 호출 시작 (Interface: {})", interfaceId);
|
||||
|
||||
String response = restClient.post()
|
||||
.uri(mciUrl)
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(payload != null ? payload : "")
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
log.info(" [ESB 어댑터(String)] 응답 수신 완료: {}", response);
|
||||
return response != null ? response : "";
|
||||
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
log.info(" [SLA 모니터링 - MCI(String)] 소요시간: {} ms", stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.integration.mci.config.ShinhanIntegrationProperties;
|
||||
import io.shinhanlife.dap.lib.integration.mci.dto.MciRequestWrapper;
|
||||
import io.shinhanlife.dap.lib.integration.mci.dto.ShinhanCommonHeaderDto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className ShinhanMciSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@EnableConfigurationProperties(ShinhanIntegrationProperties.class)
|
||||
public class ShinhanMciSender {
|
||||
|
||||
private final ObjectMapper jsonMapper;
|
||||
private final RestClient restClient;
|
||||
private final ShinhanIntegrationProperties properties;
|
||||
private final AtomicInteger sequenceGenerator = new AtomicInteger(1);
|
||||
|
||||
public ShinhanMciSender(ObjectMapper jsonMapper, ShinhanIntegrationProperties properties) {
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.properties = properties;
|
||||
this.restClient = RestClient.create();
|
||||
}
|
||||
|
||||
public <T> String send(String targetUrl, MciRequestWrapper<T> requestWrapper) throws Exception {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
try {
|
||||
ShinhanCommonHeaderDto header = requestWrapper.getTgrmCmnnhddValu();
|
||||
if (header == null) {
|
||||
header = new ShinhanCommonHeaderDto();
|
||||
requestWrapper.setTgrmCmnnhddValu(header);
|
||||
}
|
||||
|
||||
// 필수 헤더 자동 세팅 로직
|
||||
header.setEnvrTypeCd(properties.getEnvrTypeCd());
|
||||
header.setReqRspnScCd("S"); // S: 요청
|
||||
header.setAppliDutjCd("DAP"); // 어플리케이션업무코드
|
||||
|
||||
if (header.getGlbId() == null || header.getGlbId().isEmpty()) {
|
||||
header.setGlbId(generateGlbId());
|
||||
}
|
||||
|
||||
int currentSeq = sequenceGenerator.getAndIncrement();
|
||||
header.setPgrsSriaNo(String.format("%03d", currentSeq)); // 3자리
|
||||
|
||||
if (header.getReqTgrmTnsmDtptDt() == null) {
|
||||
header.setReqTgrmTnsmDtptDt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
|
||||
}
|
||||
|
||||
String jsonPayload = jsonMapper.writeValueAsString(requestWrapper);
|
||||
|
||||
log.info(" [신한 통합 MCI 어댑터] 전송 준비 완료 - URL: {}", targetUrl);
|
||||
log.debug(" [신한 통합 MCI 어댑터] 요청 Payload: {}", jsonPayload);
|
||||
|
||||
String responseJson = restClient.post()
|
||||
.uri(targetUrl)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(jsonPayload)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
log.info(" [신한 통합 MCI 어댑터] 응답 수신 완료");
|
||||
log.debug(" [신한 통합 MCI 어댑터] 응답 Payload: {}", responseJson);
|
||||
|
||||
return responseJson;
|
||||
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
log.info(" [SLA 모니터링 - MCI 연계] 소요시간: {} ms", stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateGlbId() {
|
||||
// 전사공통키 (37 Byte)
|
||||
// 전문생성상세일시(17) + 전문생성시스템명(9) + 어플리케이션업무코드(3) + 전문세션번호(8)
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); // 17 byte
|
||||
String systemName = String.format("%-9s", "AXHUB"); // 9 byte, left-aligned padded with spaces
|
||||
String appCode = "DAP"; // 3 byte
|
||||
String sessionNo = UUID.randomUUID().toString().substring(0, 8).toUpperCase(); // 8 byte
|
||||
return timestamp + systemName + appCode + sessionNo;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className TcpEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TcpEimsSender implements EimsSender {
|
||||
|
||||
@Value("${eims.tcp.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${eims.tcp.port}")
|
||||
private int port;
|
||||
|
||||
@Value("${eims.tcp.timeout}")
|
||||
private int timeout;
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) throws Exception {
|
||||
log.info(" [TCP 소켓 모드] EIMS 접속 중... {}:{}", host, port);
|
||||
|
||||
// TCP 소켓 자원을 사용 후 안전하게 닫아주는 try-with-resources 구문
|
||||
try (Socket socket = new Socket()) {
|
||||
// 1. 타임아웃 및 연결 설정
|
||||
socket.connect(new InetSocketAddress(host, port), timeout);
|
||||
socket.setSoTimeout(timeout); // 읽기 타임아웃
|
||||
|
||||
OutputStream os = socket.getOutputStream();
|
||||
InputStream is = socket.getInputStream();
|
||||
|
||||
// 2. 데이터 송신 (EUC-KR 인코딩 필수)
|
||||
// 보통 금융권 TCP 통신은 맨 앞에 전체 길이나 인터페이스 ID를 헤더로 붙입니다.
|
||||
String sendData = interfaceId + payload;
|
||||
os.write(sendData.getBytes("EUC-KR"));
|
||||
os.flush();
|
||||
|
||||
// 3. 데이터 수신
|
||||
byte[] buffer = new byte[4096];
|
||||
int readByte = is.read(buffer);
|
||||
|
||||
if (readByte == -1) {
|
||||
throw new RuntimeException("EIMS 서버가 응답 없이 연결을 종료했습니다.");
|
||||
}
|
||||
|
||||
// 받은 바이트를 다시 한글(EUC-KR) 문자열로 복원
|
||||
return new String(buffer, 0, readByte, "EUC-KR");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className DynamicPayloadBuilder
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Service
|
||||
public class DynamicPayloadBuilder {
|
||||
|
||||
public String buildFixedLengthString(List<Map<String, Object>> specList, Map<String, Object> data) throws Exception {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (Map<String, Object> spec : specList) {
|
||||
String name = (String) spec.get("name");
|
||||
// EIMS가 고정장을 요구할 경우를 대비한 len 파라미터 체크 (기본값 0 방어)
|
||||
int len = spec.get("len") != null ? (Integer) spec.get("len") : 0;
|
||||
String type = (String) spec.get("type");
|
||||
String rawValue = String.valueOf(data.getOrDefault(name, ""));
|
||||
|
||||
// len 값이 없으면 변환 없이 바로 이어붙임 (JSON 통신용)
|
||||
if (len == 0) {
|
||||
sb.append(rawValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// len 값이 있으면 고정장 통신 규칙 적용
|
||||
byte[] rawBytes = rawValue.getBytes("EUC-KR");
|
||||
if (rawBytes.length > len) {
|
||||
throw new IllegalArgumentException(name + " 길이가 " + len + " 바이트를 초과합니다.");
|
||||
}
|
||||
|
||||
int padLength = len - rawBytes.length;
|
||||
StringBuilder paddedValue = new StringBuilder(rawValue);
|
||||
|
||||
if ("NUMBER".equalsIgnoreCase(type)) {
|
||||
for (int i = 0; i < padLength; i++) paddedValue.insert(0, "0");
|
||||
} else {
|
||||
for (int i = 0; i < padLength; i++) paddedValue.append(" ");
|
||||
}
|
||||
sb.append(paddedValue.toString());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className DynamicSchemaValidator
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Service
|
||||
public class DynamicSchemaValidator {
|
||||
|
||||
public boolean validate(List<Map<String, Object>> specList, Map<String, Object> data, StringBuilder errorLog) {
|
||||
for (Map<String, Object> spec : specList) {
|
||||
String name = (String) spec.get("name");
|
||||
String type = (String) spec.get("type");
|
||||
boolean isRequired = spec.get("required") != null && (Boolean) spec.get("required");
|
||||
Object value = data.get(name);
|
||||
|
||||
// 1. 필수값 체크
|
||||
if (isRequired && (value == null || String.valueOf(value).trim().isEmpty())) {
|
||||
errorLog.append(String.format("[%s] 필드는 필수 입력 항목입니다. ", name));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 타입 체크 (값이 있을 때만)
|
||||
if (value != null && !String.valueOf(value).trim().isEmpty()) {
|
||||
if ("NUMBER".equalsIgnoreCase(type) && !String.valueOf(value).matches("-?\\d+(\\.\\d+)?")) {
|
||||
errorLog.append(String.format("[%s] 필드는 숫자여야 합니다. ", name));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.adapter.exception.MciCommunicationException;
|
||||
import io.shinhanlife.dap.lib.adapter.sender.EimsSender;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className MciTemplate
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MciTemplate {
|
||||
|
||||
private final EimsSender httpEimsSender;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* MCI 인터페이스를 호출하고 결과를 지정된 타입으로 반환합니다.
|
||||
*
|
||||
* @param interfaceId 호출할 MCI 인터페이스 ID
|
||||
* @param requestDto 요청 데이터 객체
|
||||
* @param responseType 응답을 매핑할 클래스 타입
|
||||
* @param <T> 요청 객체 타입
|
||||
* @param <R> 응답 객체 타입
|
||||
* @return 매핑된 응답 객체
|
||||
* @throws MciCommunicationException 통신 또는 파싱 실패 시 예외 발생
|
||||
*/
|
||||
public <T, R> R call(String interfaceId, T requestDto, Class<R> responseType) {
|
||||
log.info(" [MciTemplate] 시작 - Interface ID: {}", interfaceId);
|
||||
|
||||
try {
|
||||
String payload = objectMapper.writeValueAsString(requestDto);
|
||||
log.debug(" [MciTemplate] 전송 페이로드: {}", payload);
|
||||
|
||||
String responseJson = httpEimsSender.send(interfaceId, payload);
|
||||
log.debug(" [MciTemplate] 수신 응답 JSON: {}", responseJson);
|
||||
|
||||
R response = objectMapper.readValue(responseJson, responseType);
|
||||
log.info(" [MciTemplate] 완료 - Interface ID: {}", interfaceId);
|
||||
|
||||
return response;
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error(" [MciTemplate] JSON 변환 중 오류 발생", e);
|
||||
throw new MciCommunicationException("MCI 통신 중 JSON 파싱 오류", e);
|
||||
} catch (Exception e) {
|
||||
log.error(" [MciTemplate] 통신 중 오류 발생", e);
|
||||
throw new MciCommunicationException("MCI 통신 실패", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className ResultStandardizer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
public class ResultStandardizer {
|
||||
|
||||
// 오라클 조회 결과(Map)의 키 값을 카멜 케이스로 변환 (스키마 매핑)
|
||||
public Map<String, Object> standardize(Map<String, Object> rawData) {
|
||||
Map<String, Object> standardMap = new HashMap<>();
|
||||
|
||||
rawData.forEach((key, value) -> {
|
||||
String camelKey = convertToCamelCase(key);
|
||||
standardMap.put(camelKey, value);
|
||||
});
|
||||
|
||||
return standardMap;
|
||||
}
|
||||
|
||||
private String convertToCamelCase(String snakeCase) {
|
||||
if (snakeCase == null || snakeCase.isEmpty()) return snakeCase;
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean nextIsUpper = false;
|
||||
|
||||
for (char c : snakeCase.toLowerCase().toCharArray()) {
|
||||
if (c == '_') {
|
||||
nextIsUpper = true;
|
||||
} else {
|
||||
result.append(nextIsUpper ? Character.toUpperCase(c) : c);
|
||||
nextIsUpper = false;
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className TicketManager
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TicketManager {
|
||||
|
||||
// 로컬 시뮬레이션을 위한 인메모리 저장소 (운영 환경에서는 Redis 등을 사용)
|
||||
private final Map<String, Ticket> ticketStore = new ConcurrentHashMap<>();
|
||||
|
||||
// 가짜 EAI 작업(10초 대기)을 실행할 백그라운드 쓰레드 풀
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
|
||||
|
||||
/**
|
||||
* 새로운 비동기 작업을 접수하고 티켓을 발급합니다.
|
||||
*/
|
||||
public String issueTicket(String interfaceId, String payload) {
|
||||
String ticketId = "TICKET-" + UUID.randomUUID().toString();
|
||||
|
||||
Ticket ticket = new Ticket();
|
||||
ticket.setTicketId(ticketId);
|
||||
ticket.setStatus("PROCESSING");
|
||||
ticket.setMessage("EAI 시스템에서 데이터 처리 중입니다...");
|
||||
|
||||
ticketStore.put(ticketId, ticket);
|
||||
|
||||
log.info(" [TicketManager] 비동기 작업 티켓 발급 완료: {}", ticketId);
|
||||
|
||||
// 10초 뒤에 자동으로 작업을 완료 상태로 변경하는 백그라운드 시뮬레이터 실행
|
||||
simulateEaiProcessing(ticketId, interfaceId);
|
||||
|
||||
return ticketId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 티켓의 현재 상태를 조회합니다.
|
||||
*/
|
||||
public Ticket getTicketStatus(String ticketId) {
|
||||
return ticketStore.getOrDefault(ticketId, new Ticket("NOT_FOUND", "해당 티켓을 찾을 수 없습니다."));
|
||||
}
|
||||
|
||||
/**
|
||||
* 10초 뒤에 상태를 COMPLETED로 변경하여 진짜 EAI 배치가 끝난 것처럼 흉내냅니다.
|
||||
*/
|
||||
private void simulateEaiProcessing(String ticketId, String interfaceId) {
|
||||
scheduler.schedule(() -> {
|
||||
Ticket ticket = ticketStore.get(ticketId);
|
||||
if (ticket != null) {
|
||||
ticket.setStatus("COMPLETED");
|
||||
ticket.setMessage("EAI 배치가 정상적으로 완료되었습니다.");
|
||||
// 가짜 최종 결과 데이터 주입
|
||||
ticket.setResultData("{\"resultCode\":\"0000\", \"interfaceId\":\"" + interfaceId + "\", \"processedRecords\":50000}");
|
||||
ticketStore.put(ticketId, ticket);
|
||||
log.info(" [TicketManager] EAI 비동기 작업 시뮬레이션 완료! (Ticket: {})", ticketId);
|
||||
}
|
||||
}, 10, TimeUnit.SECONDS); // 10초 지연
|
||||
}
|
||||
|
||||
// 티켓 상태를 담을 내부 DTO 클래스
|
||||
@lombok.Data
|
||||
public static class Ticket {
|
||||
private String ticketId;
|
||||
private String status; // PROCESSING, COMPLETED, FAILED, NOT_FOUND
|
||||
private String message;
|
||||
private String resultData; // 최종 완료 시 담길 데이터
|
||||
|
||||
public Ticket() {}
|
||||
public Ticket(String status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.test;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.test
|
||||
* @className MockEimsHttpServer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class MockEimsHttpServer {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MockEimsHttpServer(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostMapping("/gateway")
|
||||
public Map<String, Object> mockEimsReceiver(
|
||||
@RequestHeader(value = "X-Trace-Id", required = false) String traceId,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
|
||||
String interfaceId = (String) request.get("interfaceId");
|
||||
String data = (String) request.get("data");
|
||||
|
||||
log.info(" [가짜 EIMS 서버] HTTP 요청 수신 완료!");
|
||||
log.info(" 수신된 Trace-ID (거래고유번호): {}", traceId != null ? traceId : "없음");
|
||||
log.info(" 인터페이스ID: {}, 데이터: [{}]", interfaceId, data);
|
||||
|
||||
// No-Code Mock 응답 생성 (JSON 설정 파일 기반)
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource("mock-responses.json");
|
||||
Map<String, Object> mockDataMap = objectMapper.readValue(resource.getInputStream(), Map.class);
|
||||
|
||||
if (mockDataMap.containsKey(interfaceId)) {
|
||||
return (Map<String, Object>) mockDataMap.get(interfaceId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("JSON 파싱 에러 또는 mock 파일 읽기 실패 (기본 응답 반환)", e);
|
||||
}
|
||||
|
||||
// 기본 응답
|
||||
return Map.of(
|
||||
"status", "404",
|
||||
"message", "MOCK 데이터가 정의되지 않았습니다.",
|
||||
"receivedLength", data.length()
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("/mock/esb/api")
|
||||
public String mockEsbReceiver(@RequestBody String xmlPayload) {
|
||||
log.info(" [가짜 ESB 서버] MCI/ESB 요청 수신 완료!");
|
||||
log.info(" 수신된 XML 전문: {}", xmlPayload);
|
||||
|
||||
// MciEimsSender가 기대하는 JSON 변환용 XML 포맷 응답
|
||||
return "<Response><status>SUCCESS</status><message>MOCK_MCI_EIMS_RECEIVE_SUCCESS</message><data><info>정상 처리되었습니다.</info></data></Response>";
|
||||
}
|
||||
|
||||
@PostMapping("/mock/esb/string")
|
||||
public String mockEsbStringReceiver(@RequestBody(required = false) String payload) {
|
||||
log.info(" [가짜 ESB 서버] MCI String 요청 수신 완료!");
|
||||
log.info(" 수신된 String 전문: {}", payload);
|
||||
|
||||
// MciSampleStringResponse 에 맞게 고정 길이 응답 생성
|
||||
// name (10), age (3), joinDate (8), statusCode (2)
|
||||
// targetList (30) -> MciSampleTargetDto (itemCode 5, itemValue 5) x 3
|
||||
return "홍길동 03020260901OKA0001B0001A0002B0002A0003B0003";
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.test;
|
||||
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
// 대신 "local" 환경(application-local.properties)에서만 가짜 소켓 서버가 켜지도록 보장합니다.
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.test
|
||||
* @className MockEimsTcpServer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Profile("local")
|
||||
public class MockEimsTcpServer {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MockEimsTcpServer(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Value("${eims.tcp.port:8090}")
|
||||
private int port;
|
||||
|
||||
@Value("${server.port:8081}")
|
||||
private int serverPort;
|
||||
|
||||
private ServerSocket serverSocket;
|
||||
private boolean running = true;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void startTcpServer() {
|
||||
// MSA 환경에서는 각 툴 Pod 내부에서 독립적인 가짜 EIMS 서버가 실행되도록 허용
|
||||
log.info(" [가짜 EIMS 서버] 로컬 테스트 환경용 EIMS TCP 서버 가동을 준비합니다.");
|
||||
|
||||
// Java 21 가상 스레드를 사용하여 메인 서버 가동에 방해 없이 백그라운드에서 실행
|
||||
Thread.ofVirtual().start(() -> {
|
||||
try {
|
||||
serverSocket = new ServerSocket(port);
|
||||
log.info(" [가짜 EIMS 서버] 로컬 TCP 소켓 서버 가동 완료 (Port: {})", port);
|
||||
|
||||
while (running) {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
|
||||
// 연결된 요청을 별도 스레드로 처리
|
||||
Thread.ofVirtual().start(() -> handleClient(clientSocket));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (running) log.error(" 가짜 TCP 서버 에러: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleClient(Socket socket) {
|
||||
try (socket) {
|
||||
InputStream is = socket.getInputStream();
|
||||
OutputStream os = socket.getOutputStream();
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int readByte = is.read(buffer);
|
||||
|
||||
if (readByte != -1) {
|
||||
String receivedData = new String(buffer, 0, readByte, "EUC-KR");
|
||||
log.info(" [가짜 EIMS 서버] TCP 데이터 수신 완료!");
|
||||
log.info(" 수신된 전문: [{}]", receivedData);
|
||||
|
||||
String responseData = "{\"status\":\"404\",\"message\":\"MOCK 데이터가 정의되지 않았습니다.\"}";
|
||||
try {
|
||||
int braceIndex = receivedData.indexOf('{');
|
||||
String interfaceId = "";
|
||||
if (braceIndex > 0) {
|
||||
interfaceId = receivedData.substring(0, braceIndex).trim();
|
||||
} else if (braceIndex == -1) {
|
||||
interfaceId = receivedData.trim();
|
||||
}
|
||||
|
||||
ClassPathResource resource = new ClassPathResource("mock-responses.json");
|
||||
Map<String, Object> mockDataMap = objectMapper.readValue(resource.getInputStream(), Map.class);
|
||||
|
||||
if (mockDataMap.containsKey(interfaceId)) {
|
||||
responseData = objectMapper.writeValueAsString(mockDataMap.get(interfaceId));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("TCP JSON 파싱 에러 또는 파일 읽기 실패", e);
|
||||
}
|
||||
|
||||
// 응답 데이터 송신
|
||||
os.write(responseData.getBytes("EUC-KR"));
|
||||
os.flush();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(" 가짜 TCP 클라이언트 처리 에러: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopTcpServer() {
|
||||
this.running = false;
|
||||
try {
|
||||
if (serverSocket != null) serverSocket.close();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.test;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.test
|
||||
* @className MockJspServer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/mock")
|
||||
public class MockJspServer {
|
||||
|
||||
// ==========================================
|
||||
// 1. Form Data 방식 테스트 수신부 (jsp-form)
|
||||
// ==========================================
|
||||
@PostMapping(value = "/jsp-form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public String mockJspFormReceiver(
|
||||
@RequestParam("interfaceId") String interfaceId,
|
||||
@RequestParam("data") String data) {
|
||||
|
||||
log.info(" [가짜 JSP 서버] 폼 데이터(Form) 수신 완료!");
|
||||
log.info(" 파라미터 파싱 확인 - ID: {}, DATA: [{}]", interfaceId, data);
|
||||
|
||||
// 실제 JSP 서버처럼 앞뒤에 의미 없는 줄바꿈(엔터)과 공백을 잔뜩 넣어서 리턴합니다.
|
||||
return "\n\n SUCCESS_FROM_MOCK_JSP_FORM \n\n";
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. JSON 방식 테스트 수신부 (jsp-json)
|
||||
// ==========================================
|
||||
@PostMapping(value = "/jsp-json", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public String mockJspJsonReceiver(@RequestBody Map<String, String> request) {
|
||||
|
||||
String interfaceId = request.get("interfaceId");
|
||||
String data = request.get("data");
|
||||
|
||||
log.info(" [가짜 JSP 서버] 제이슨(JSON) 수신 완료!");
|
||||
log.info(" JSON 파싱 확인 - ID: {}, DATA: [{}]", interfaceId, data);
|
||||
|
||||
// 여기도 마찬가지로 쓰레기 여백을 넣어줍니다.
|
||||
return "\n\n SUCCESS_FROM_MOCK_JSP_JSON \n\n";
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package io.shinhanlife.dap.lib.adapter.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.util
|
||||
* @className LegacyDataTransformer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
public class LegacyDataTransformer {
|
||||
|
||||
/**
|
||||
* AI 모델이 보낸 자유로운 형태의 data를 레거시 시스템 규격(spec)에 맞게 강제 변환합니다.
|
||||
*
|
||||
* @param data AI가 보낸 파라미터 맵
|
||||
* @param spec 레거시 시스템이 요구하는 파라미터 스펙 (name, type, maxLength, defaultValue, required 등)
|
||||
* @return 엄격하게 정제된 파라미터 맵
|
||||
*/
|
||||
public static Map<String, Object> transform(Map<String, Object> data, List<Map<String, Object>> spec) {
|
||||
if (spec == null || spec.isEmpty()) {
|
||||
return data != null ? data : new HashMap<>();
|
||||
}
|
||||
|
||||
Map<String, Object> transformedData = new HashMap<>();
|
||||
|
||||
for (Map<String, Object> fieldSpec : spec) {
|
||||
String fieldName = (String) fieldSpec.get("name");
|
||||
if (fieldName == null) continue;
|
||||
|
||||
Object rawValue = data != null ? data.get(fieldName) : null;
|
||||
Object finalValue = rawValue;
|
||||
|
||||
// 1. 기본값(Default Value) 주입
|
||||
if (finalValue == null && fieldSpec.containsKey("defaultValue")) {
|
||||
finalValue = fieldSpec.get("defaultValue");
|
||||
log.debug(" [DataTransformer] '{}' 필드 누락 -> 기본값 '{}' 주입", fieldName, finalValue);
|
||||
}
|
||||
|
||||
// 2. 강제 형변환 (Type Coercion)
|
||||
String type = (String) fieldSpec.getOrDefault("type", "string");
|
||||
if (finalValue != null) {
|
||||
if ("string".equalsIgnoreCase(type) && !(finalValue instanceof String)) {
|
||||
finalValue = String.valueOf(finalValue);
|
||||
log.debug(" [DataTransformer] '{}' 필드 강제 String 형변환", fieldName);
|
||||
} else if ("number".equalsIgnoreCase(type) && finalValue instanceof String) {
|
||||
try {
|
||||
finalValue = Long.parseLong((String) finalValue);
|
||||
log.debug(" [DataTransformer] '{}' 필드 강제 Number 형변환", fieldName);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn(" [DataTransformer] '{}' 필드 Number 형변환 실패. 기존 값 유지", fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 길이 제한 (Truncation / MaxLength)
|
||||
if (finalValue instanceof String && fieldSpec.containsKey("maxLength")) {
|
||||
int maxLength = (Integer) fieldSpec.get("maxLength");
|
||||
String strVal = (String) finalValue;
|
||||
if (strVal.length() > maxLength) {
|
||||
finalValue = strVal.substring(0, maxLength);
|
||||
log.warn(" [DataTransformer] '{}' 필드 길이 초과! {}자로 강제 자름 (Truncated)", fieldName, maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 필수값(Required) 누락 체크 (에러를 던지지 않고 빈 문자열 강제 주입하여 레거시 팅김 방지)
|
||||
boolean isRequired = (Boolean) fieldSpec.getOrDefault("required", false);
|
||||
if (isRequired && finalValue == null) {
|
||||
log.error(" [DataTransformer] 필수 필드 '{}' 누락! 강제 공백 주입하여 시스템 장애 방지", fieldName);
|
||||
finalValue = "string".equalsIgnoreCase(type) ? "" : 0;
|
||||
}
|
||||
|
||||
if (finalValue != null) {
|
||||
transformedData.put(fieldName, finalValue);
|
||||
}
|
||||
}
|
||||
|
||||
return transformedData;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user