Merge remote-tracking branch 'origin/jenkins_with_weblogic' of C:/KJB_DEV/eapim-bundle/bundles/251216/eapim-admin_incremental_2025-12-01.bundle into jenkins_with_weblogic

This commit is contained in:
Rinjae
2025-12-16 19:45:27 +09:00
11 changed files with 415 additions and 56 deletions
+1
View File
@@ -102,6 +102,7 @@
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/_onl/**" />
<mvc:exclude-mapping path="/kjb/**" />
<bean class="com.eactive.eai.rms.common.interceptor.AuthorizeInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
+8
View File
@@ -0,0 +1,8 @@
{
"dev": {
"emsUrl": "http://192.168.240.176:30100/monitoring"
},
"local": {
"emsUrl": "http://127.0.0.1:30100/monitoring"
}
}
+6
View File
@@ -0,0 +1,6 @@
### Retrieve Gateway Metrics After a Specific Time Slice
GET {{emsUrl}}/kjb/gw-metrics/after-timeslice/2025-12-02_01:00:00.json
### Pretty Print
GET {{emsUrl}}/kjb/gw-metrics/after-timeslice/2025-12-02_01:00:00.json?pretty=true
+75
View File
@@ -0,0 +1,75 @@
# 🚀 Spring Legacy (Non-Boot) 핫스왑 환경 구축 가이드
본 가이드는 폐쇄망(Air-gapped) 환경에서 **JRebel** 대안으로 \*\*JetBrains Runtime(JBR)\*\*과 **HotswapAgent**를 사용하여, 서버 재시작 없이 Java 코드를 즉시 반영하는 방법을 설명합니다.
## 1\. 외부망 준비물 (반입 리스트)
폐쇄망으로 반입해야 할 필수 파일입니다.
| 항목 | 용도 | 다운로드 링크 (예시) |
| :--- | :--- | :--- |
| **JBR (JetBrains Runtime)** | DCEVM이 내장된 JDK | [JBR GitHub Releases](https://github.com/JetBrains/JetBrainsRuntime/releases) |
| **HotswapAgent.jar** | Spring Bean 설정 갱신용 | [HotswapAgent Releases](https://github.com/HotswapProjects/HotswapAgent/releases) |
> **주의**: JBR 버전은 프로젝트의 Java 버전(8, 11, 17 등)과 일치시켜 다운로드하세요.
-----
## 2\. IDE(IntelliJ / Eclipse) 환경 설정
### 2.1 JDK(JBR) 등록
1. 반입한 JBR 압축을 풉니다 (예: `C:\dev\jbr`).
2. **IntelliJ**: `Project Structure` \> `SDKs` \> `Add JDK` 후 해당 경로 지정.
3. **Eclipse**: `Preferences` \> `Java` \> `Installed JREs` \> `Add` 후 해당 경로 지정.
### 2.2 VM 옵션 추가
서버(Tomcat 등)의 실행 구성(Run/Debug Configuration)에 아래의 **VM Options**를 추가합니다.
```bash
# 클래스 구조 변경(필드/메서드 추가) 허용
-XX:+AllowEnhancedClassRedefinition
# Spring Bean 설정 및 리소스 실시간 반영 (경로 주의)
-javaagent:C:\path\to\hotswap-agent.jar
```
-----
## 3\. IDE별 세부 최적화 설정
### IntelliJ IDEA
1. **컴파일러 설정**:
- `Settings` \> `Build, Execution, Deployment` \> `Compiler`
- **Build project automatically** 체크
2. **고급 설정**:
- `Settings` \> `Advanced Settings`
- **Allow auto-make to start even if developed application is currently running** 체크
3. **업데이트 정책**:
- Tomcat 설정 내 `On 'Update' action``On frame deactivation`을 **Update classes and resources**로 변경.
### Eclipse
1. **Auto Build**: `Project` \> `Build Automatically` 체크 확인.
2. **Debug 모드**: 반드시 **Debug** 모드로 서버를 실행해야 핫스왑이 작동합니다.
-----
## 4\. 실제 개발 워크플로우
1. **서버 실행**: 반드시 **Debug 모드**로 실행합니다.
2. **코드 수정**: 메서드 로직, 필드 추가, 새로운 `@Service` 생성 등을 수행합니다.
3. **반영 (Compile)**:
- **IntelliJ**: `Ctrl + Shift + F9` (현재 파일 재컴파일) 또는 `Ctrl + F9` (전체 빌드).
- **Eclipse**: 저장(`Ctrl + S`) 시 자동 컴파일 및 반영.
4. **확인**: 서버 재시작 없이 브라우저에서 새로고침하여 결과를 확인합니다.
-----
## 5\. 주요 주의사항 (Troubleshooting)
* **상속 구조 변경**: 클래스의 부모를 바꾸거나 인터페이스를 변경하는 수준의 큰 수정은 서버 재시작이 필요할 수 있습니다.
* **정적 변수(Static)**: `static` 필드의 초기값 변경은 핫스왑 후에도 이전 값이 유지될 수 있습니다.
* **폐쇄망 라이브러리**: `hotswap-agent.jar` 외에 특정 프레임워크(예: MyBatis, Hibernate) 전용 플러그인이 필요한 경우, HotswapAgent 설정 파일(`hotswap-agent.properties`)을 통해 제어할 수 있습니다.
@@ -0,0 +1,58 @@
package com.eactive.eai.rms.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.Map;
/**
* 서버 시작 시 등록된 HTTP 엔드포인트 목록을 로그로 출력
*/
@Component
public class EndpointLogger implements ApplicationListener<ContextRefreshedEvent> {
private static final Logger log = LoggerFactory.getLogger(EndpointLogger.class);
private boolean logged = false;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 중복 출력 방지 (parent/child context)
if (logged) {
return;
}
ApplicationContext ctx = event.getApplicationContext();
try {
RequestMappingHandlerMapping mapping = ctx.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> methods = mapping.getHandlerMethods();
log.debug("========== Registered HTTP Endpoints ({}) ==========", methods.size());
methods.entrySet().stream()
.sorted((e1, e2) -> e1.getKey().toString().compareTo(e2.getKey().toString()))
.forEach(entry -> {
RequestMappingInfo info = entry.getKey();
HandlerMethod method = entry.getValue();
log.debug("{} -> {}.{}",
info.getPatternsCondition(),
method.getBeanType().getSimpleName(),
method.getMethod().getName());
});
log.debug("========== End of Endpoints ==========");
logged = true;
} catch (Exception e) {
log.warn("Failed to log endpoints: {}", e.getMessage());
}
}
}
@@ -1,39 +0,0 @@
package com.eactive.eai.rms.data.entity.onl.apim.obp;
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* ObpGwMetric Repository (eapim-admin 위치)
* API Gateway 시간별 거래량 지표 조회/저장을 위한 Spring Data JPA Repository
*/
@EMSDataSource
public interface ObpGwMetricRepository extends BaseRepository<ObpGwMetric, String> {
/**
* UPSERT 판단을 위한 기존 데이터 조회
* 동일한 timeslice + apiId + clientId + hostname + orgId 조합이 존재하는지 확인
*/
Optional<ObpGwMetric> findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId(
LocalDateTime timeslice,
String apiId,
String clientId,
String hostname,
String orgId
);
/**
* 특정 시간대의 모든 GwMetric 데이터 조회
*/
List<ObpGwMetric> findByTimeslice(LocalDateTime timeslice);
/**
* 특정 시간 범위의 GwMetric 데이터 조회
*/
List<ObpGwMetric> findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime);
}
@@ -1,6 +1,7 @@
package com.eactive.eai.rms.data.entity.onl.apim.obp;
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
import com.eactive.apim.portal.obp.repository.ObpGwMetricRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,6 +44,13 @@ public class ObpGwMetricService {
return repository.findByTimesliceBetween(startTime, endTime);
}
/**
* 특정 시간대 이후의 모든 GwMetric 데이터 조회
*/
public List<ObpGwMetric> findByTimesliceGreaterThanEqual(LocalDateTime timeslice) {
return repository.findByTimesliceGreaterThanEqual(timeslice);
}
/**
* 엔티티 저장 (INSERT 또는 UPDATE)
*/
@@ -0,0 +1,96 @@
package com.eactive.eai.rms.onl.apim.obp;
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* ObpGwMetric JSON 응답 DTO (after-timeslice 엔드포인트용)
*
* <p>예시 출력 형식에 맞춤:</p>
* <pre>
* {
* "apiId": "1ba56b82-a9c9-41e2-b3ab-ff9377667d56",
* "apiName": "[해외송금] 가상계좌 입금실패 정보조회",
* "apiRoutingUri": "/api/obs/remittance/remittancefail*",
* "orgId": "ORG001",
* "partnerCode": "000008-01",
* "appId": "APP001",
* "appKey": "l7xx2fd7efc523794be5a1b126873a5de613",
* "requestUri": "-1",
* "timeslice": "2025-12-02T03:00:00.000+0000",
* "attemptedCount": 3,
* "completedCount": 3
* }
* </pre>
*/
@Data
public class ObpGwMetricAfterDTO {
private static final DateTimeFormatter ISO8601_MILLIS_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'+0000'");
@SerializedName("apiId")
private String apiId;
@SerializedName("apiName")
private String apiName;
@SerializedName("apiRoutingUri")
private String apiRoutingUri;
@SerializedName("orgId")
private String orgId;
@SerializedName("partnerCode")
private String partnerCode;
@SerializedName("appId")
private String appId;
@SerializedName("appKey")
private String appKey;
@SerializedName("requestUri")
private String requestUri;
@SerializedName("timeslice")
private String timeslice;
@SerializedName("attemptedCount")
private Long attemptedCount;
@SerializedName("completedCount")
private Long completedCount;
/**
* Entity → DTO 변환
*/
public static ObpGwMetricAfterDTO from(ObpGwMetric entity) {
ObpGwMetricAfterDTO dto = new ObpGwMetricAfterDTO();
dto.setApiId(entity.getApiId());
dto.setApiName(entity.getApiName());
dto.setApiRoutingUri(entity.getUri());
dto.setOrgId(entity.getOrgId());
dto.setPartnerCode(entity.getPartnerCode());
dto.setAppId(entity.getAppId());
dto.setAppKey(entity.getClientId());
dto.setRequestUri(entity.getRequestUri());
dto.setTimeslice(formatTimeslice(entity.getTimeslice()));
dto.setAttemptedCount(entity.getAttemptedCount());
dto.setCompletedCount(entity.getCompletedCount());
return dto;
}
/**
* timeslice를 ISO8601 형식 (밀리초 포함) + UTC 시간대로 변환
* 예: 2025-12-02T03:00:00.000+0000
*/
private static String formatTimeslice(LocalDateTime timeslice) {
if (timeslice == null) return null;
return timeslice.format(ISO8601_MILLIS_FORMATTER);
}
}
@@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.nio.charset.StandardCharsets;
@@ -27,7 +28,7 @@ public class ObpGwMetricController implements InterceptorSkipController {
private static final Logger log = LoggerFactory.getLogger(ObpGwMetricController.class);
private static final DateTimeFormatter PATH_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss");
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss");
private final ObpGwMetricManService obpGwMetricManService;
@@ -39,15 +40,18 @@ public class ObpGwMetricController implements InterceptorSkipController {
* 특정 시간대의 GwMetric 데이터 조회
*
* @param datetime 조회 시간대 (형식: yyyy-MM-dd_HH:mm:ss)
* @param pretty true면 JSON 들여쓰기 적용
* @return JSON Array
*/
@RequestMapping(
value = "/kjb/gw-metrics/{datetime}",
value = "/kjb/gw-metrics/{datetime}.json",
produces = "application/json;charset=utf-8"
)
@ResponseBody
public String getGwMetrics(@PathVariable("datetime") String datetime) {
log.debug("GwMetric 조회 요청: datetime={}", datetime);
public String getGwMetrics(
@PathVariable("datetime") String datetime,
@RequestParam(value = "pretty", required = false) Boolean pretty) {
log.debug("GwMetric 조회 요청: datetime={}, pretty={}", datetime, pretty);
long startTime = System.currentTimeMillis();
try {
@@ -59,11 +63,7 @@ public class ObpGwMetricController implements InterceptorSkipController {
List<ObpGwMetricDTO> metrics = obpGwMetricManService.findByTimeslice(timeslice);
// JSON 변환 (GSON)
Gson gson = new GsonBuilder()
.serializeNulls()
.create();
String json = gson.toJson(metrics);
String json = toJson(metrics, pretty);
// 응답 크기 계산 (UTF-8 기준)
int jsonBytes = json.getBytes(StandardCharsets.UTF_8).length;
@@ -81,6 +81,66 @@ public class ObpGwMetricController implements InterceptorSkipController {
}
}
/**
* 특정 시간대 이후의 GwMetric 데이터 조회
*
* @param datetime 조회 시작 시간대 (형식: yyyy-MM-dd_HH:mm:ss)
* @param pretty true면 JSON 들여쓰기 적용
* @return JSON Array (해당 시간대 이후의 모든 지표)
*/
@RequestMapping(
value = "/kjb/gw-metrics/after-timeslice/{datetime}.json",
produces = "application/json;charset=utf-8"
)
@ResponseBody
public String getGwMetricsAfterTimeslice(
@PathVariable("datetime") String datetime,
@RequestParam(value = "pretty", required = false) Boolean pretty) {
log.debug("GwMetric after-timeslice 조회 요청: datetime={}, pretty={}", datetime, pretty);
long startTime = System.currentTimeMillis();
try {
// Path 파라미터 파싱
LocalDateTime targetTime = LocalDateTime.parse(datetime, PATH_FORMATTER);
LocalDateTime timeslice = targetTime.withMinute(0).withSecond(0).withNano(0);
// 조회
List<ObpGwMetricAfterDTO> metrics = obpGwMetricManService.findByTimesliceAfter(timeslice);
// JSON 변환 (GSON)
String json = toJson(metrics, pretty);
// 응답 크기 계산 (UTF-8 기준)
int jsonBytes = json.getBytes(StandardCharsets.UTF_8).length;
String jsonSize = formatByteSize(jsonBytes);
long elapsed = System.currentTimeMillis() - startTime;
log.info("GwMetric after-timeslice 조회 완료: datetime={}, count={}, size={}, elapsed={}ms",
datetime, metrics.size(), jsonSize, elapsed);
return json;
} catch (Exception e) {
log.error("GwMetric after-timeslice 조회 실패: datetime={}", datetime, e);
throw e;
}
}
/**
* 객체를 JSON 문자열로 변환
*
* @param object 변환할 객체
* @param pretty true면 들여쓰기 적용
* @return JSON 문자열
*/
private String toJson(Object object, Boolean pretty) {
GsonBuilder builder = new GsonBuilder().serializeNulls();
if (Boolean.TRUE.equals(pretty)) {
builder.setPrettyPrinting();
}
return builder.create().toJson(object);
}
/**
* 바이트 크기를 읽기 쉬운 형식으로 변환
* 예: 1024 -> "1.0 KB", 1048576 -> "1.0 MB"
@@ -279,4 +279,18 @@ public class ObpGwMetricManService extends BaseService {
.map(ObpGwMetricDTO::from)
.collect(Collectors.toList());
}
/**
* 특정 시간대 이후의 GwMetric 데이터 조회 (REST API용)
* after-timeslice 엔드포인트에서 사용
*/
public List<ObpGwMetricAfterDTO> findByTimesliceAfter(LocalDateTime timeslice) {
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING));
List<ObpGwMetric> entities = obpGwMetricService.findByTimesliceGreaterThanEqual(timeslice);
return entities.stream()
.map(ObpGwMetricAfterDTO::from)
.collect(Collectors.toList());
}
}
+80 -8
View File
@@ -1,9 +1,81 @@
# HotSwapAgent 설정
# Spring 관련
Spring.reload=true
# Hibernate 관련
Hibernate.reload=true
# 로그 레벨
logLevel=INFO
# 자동 Hot Swap 활성화
# ============================================================
# HotswapAgent Configuration for eapim-admin (Spring Legacy)
# ============================================================
# \uC790\uB3D9 \uD56B\uC2A4\uC651 \uD65C\uC131\uD654
autoHotswap=true
# \uB85C\uAE45 \uB808\uBCA8 (TRACE, DEBUG, INFO, WARNING, ERROR)
LOGGER.level=INFO
# ============================================================
# \uBA85\uC2DC\uC801 \uD50C\uB7EC\uADF8\uC778 \uBE44\uD65C\uC131\uD654 (\uC27C\uD45C \uAD6C\uBD84)
# ============================================================
disabledPlugins=org.hotswap.agent.plugin.ibatis.IBatisPlugin,org.hotswap.agent.plugin.mybatis.MyBatisPlugin
# ============================================================
# Core Plugin \uD65C\uC131\uD654
# ============================================================
# Spring Framework (\uD544\uC218) - Bean \uC7AC\uB4F1\uB85D
spring.enabled=true
spring.basePackagePrefix=com.eactive.
# Hibernate/JPA - \uC5D4\uD2F0\uD2F0 \uBCC0\uACBD \uAC10\uC9C0
hibernate.enabled=true
# Logback - \uB85C\uADF8 \uC124\uC815 \uB9AC\uB85C\uB4DC
logback.enabled=true
# Proxy - AOP \uD504\uB85D\uC2DC \uC7AC\uC0DD\uC131
proxy.enabled=true
# ============================================================
# \uBE44\uD65C\uC131\uD654 Plugin (\uBBF8\uC0AC\uC6A9 \uB610\uB294 \uCDA9\uB3CC \uBC29\uC9C0)
# ============================================================
# iBatis (Spring\uC758 SqlMapClientFactoryBean\uACFC \uCDA9\uB3CC)
ibatis.enabled=false
mybatis.enabled=false
jsf.enabled=false
seam.enabled=false
wicket.enabled=false
mojarra.enabled=false
omnifaces.enabled=false
el.enabled=false
weld.enabled=false
owb.enabled=false
cdi.enabled=false
resteasy.enabled=false
jersey1.enabled=false
jersey2.enabled=false
# ============================================================
# Watch Resources (\uB9AC\uC18C\uC2A4 \uBCC0\uACBD \uAC10\uC9C0)
# ============================================================
# \uD074\uB798\uC2A4\uD328\uC2A4 \uC678 \uCD94\uAC00 \uAC10\uC2DC \uACBD\uB85C (XML \uC124\uC815 \uB4F1)
watchResources=src/main/resources,WebContent/WEB-INF
# ============================================================
# Spring \uC804\uC6A9 \uC124\uC815
# ============================================================
# \uCEF4\uD3EC\uB10C\uD2B8 \uC2A4\uCE94 \uBCA0\uC774\uC2A4 \uD328\uD0A4\uC9C0
spring.scanBasePackage=com.eactive.eai,com.eactive.apim
# \uD504\uB85D\uC2DC \uC7AC\uC0DD\uC131 (AOP, @Transactional \uC0AC\uC6A9 \uC2DC \uD544\uC218)
spring.proxyRegeneration=true
# Bean \uC815\uC758 \uBCC0\uACBD \uC2DC \uB9AC\uB85C\uB4DC
spring.reloadBeanDefinition=true
# ============================================================
# \uB514\uBC84\uAE45 (\uBB38\uC81C \uBC1C\uC0DD \uC2DC \uD65C\uC131\uD654)
# ============================================================
# \uC0C1\uC138 \uB85C\uADF8 \uCD9C\uB825 (\uD544\uC694\uC2DC DEBUG\uB85C \uBCC0\uACBD)
# LOGGER.level=DEBUG
# \uD074\uB798\uC2A4 \uBCC0\uACBD \uCD94\uC801
# LOGGER.org.hotswap.agent.plugin.spring=DEBUG