From c44b331a3296cd2377686465bd7d9a43c147b9bc Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 22 Jul 2026 09:11:20 +0900 Subject: [PATCH] =?UTF-8?q?DJBErpNsmapiAccessTokenService=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=A7=8C=EB=A3=8C=EC=8B=9C=EA=B0=84=20=EB=B0=8F=20?= =?UTF-8?q?HTTP=20=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하드코딩된 30초 만료를 제거하고 oAuthCredentialVo.getIntervalSec() 기준으로 복원 (미설정 시 24시간 폴백), 스케줄러 주기와 불일치하던 문제 해결 - execute() 호출마다 PoolingHttpClientConnectionManager/CloseableHttpClient를 새로 만들고 즉시 닫던 것을, mTLS 여부+clientId 조합별로 1회만 생성해 재사용하도록 변경. 이 서비스는 HttpClientAccessTokenServiceFactoryByDB에 싱글턴으로 캐시되어 재사용되므로 인스턴스 필드로 안전하게 캐시 가능 --- .../impl/DJBErpNsmapiAccessTokenService.java | 186 ++++++++++-------- 1 file changed, 107 insertions(+), 79 deletions(-) diff --git a/src/main/java/com/eactive/eai/custom/authoutbound/client/impl/DJBErpNsmapiAccessTokenService.java b/src/main/java/com/eactive/eai/custom/authoutbound/client/impl/DJBErpNsmapiAccessTokenService.java index e9e19e6..c226f04 100644 --- a/src/main/java/com/eactive/eai/custom/authoutbound/client/impl/DJBErpNsmapiAccessTokenService.java +++ b/src/main/java/com/eactive/eai/custom/authoutbound/client/impl/DJBErpNsmapiAccessTokenService.java @@ -10,6 +10,7 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Date; import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; import javax.net.ssl.SSLContext; @@ -62,6 +63,13 @@ public class DJBErpNsmapiAccessTokenService implements HttpClientAccessTokenServ private static boolean testMode = TestModeChecker.isTestMode(); + /** + * mTLS 여부/clientId 조합별로 CloseableHttpClient(및 내부 PoolingHttpClientConnectionManager)를 + * 1회만 생성해 재사용한다. 이 서비스 인스턴스는 HttpClientAccessTokenServiceFactoryByDB에 + * className 기준으로 캐시되어 재사용되므로, 이 필드도 인스턴스 생명주기 동안 안전하게 재사용된다. + */ + private final ConcurrentHashMap httpClientCache = new ConcurrentHashMap<>(); + /** * 1. 기능 : 법인검증 토큰 발급에 사용 2. 처리 개요 : - 속성 정보를 설정 하고 토큰 발급 URL 호출 한다. 3. 주의사항 * - JSON 방식 @@ -121,80 +129,11 @@ public class DJBErpNsmapiAccessTokenService implements HttpClientAccessTokenServ logger.info("adapterGroupName. : {}, useMtls. : {}, clientId : {}", name, useMtls, clientId); } - int maxTotalConnections = HttpClientAdapterServiceKey.DEFAULT_MAX_TOTAL_CONNECTIONS; - int maxHostConnections = HttpClientAdapterServiceKey.DEFAULT_MAX_CONNECTION_PER_HOST; - - HttpOutTlsInfoVO mtlsInfo = null; - SSLContext sslContext = null; - PoolingHttpClientConnectionManagerBuilder cmBuilder = PoolingHttpClientConnectionManagerBuilder.create(); - PoolingHttpClientConnectionManager connectionManager = null; - - try { - if (useMtls) { - HttpOutTlsInfoManager tlsManager = HttpOutTlsInfoManager.getInstance(); - if (StringUtils.isNotEmpty(clientId)) { - mtlsInfo = tlsManager.getHttpOutTlsInfo(clientId); - } - } - - if (useMtls && mtlsInfo != null) { - String storeType = mtlsInfo.getStoreType(); - String keyStoreInfo = mtlsInfo.getKeystoreInfo(); - String keyStorePassword = mtlsInfo.getKeystorePassword(); - String trustStoreInfo = mtlsInfo.getTruststoreInfo(); - String trustStorePassword = mtlsInfo.getTruststorePassword(); - - String[] tlsVersions = null; - String[] cipherSuites = null; - - if (StringUtils.isAnyEmpty(keyStoreInfo, keyStorePassword)) { - throw new Exception("mTLS keyStore config error"); - } - - boolean skipTrust = false; - if (StringUtils.isAnyEmpty(trustStoreInfo, trustStorePassword)) { - if (logger.isWarn()) - logger.warn("Skip trustStore validation adapterGroupName : " + name); - skipTrust = true; - } - - sslContext = HttpClient5SSLContextFactory.createMTLSContextFromContent(storeType, keyStoreInfo, - keyStorePassword, trustStoreInfo, trustStorePassword, skipTrust, tlsVersions, cipherSuites); - - SSLConnectionSocketFactory sslSocketFactory = null; - if (testMode) { - sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); - } else { - sslSocketFactory = new SSLConnectionSocketFactory(sslContext); - } - cmBuilder.setSSLSocketFactory(sslSocketFactory); - } else { - sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); - SSLConnectionSocketFactory sslSocketFactory = null; - if(testMode) { - // Hostname verifier 비활성화 (테스트용) - sslSocketFactory = new SSLConnectionSocketFactory( - sslContext - , NoopHostnameVerifier.INSTANCE - ); - } - else { - sslSocketFactory = new SSLConnectionSocketFactory( - sslContext - ); - } - cmBuilder.setSSLSocketFactory(sslSocketFactory); - } - } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException - | IOException | UnrecoverableKeyException e) { - throw new RuntimeException(e); - } catch (Exception e) { - throw new RuntimeException(e); - } - - connectionManager = cmBuilder.build(); - connectionManager.setMaxTotal(maxTotalConnections); - connectionManager.setDefaultMaxPerRoute(maxHostConnections); + // mTLS 여부(및 clientId)별로 CloseableHttpClient를 재사용한다. 매 호출마다 새로 만들면 + // PoolingHttpClientConnectionManager를 쓰는 의미가 없어지고 SSL 핸드셰이크 비용만 반복된다. + String httpClientCacheKey = useMtls ? "mtls:" + clientId : "default"; + CloseableHttpClient httpClient = httpClientCache.computeIfAbsent(httpClientCacheKey, + key -> buildHttpClient(useMtls, clientId, name)); //json body setting // Map jsonMap = new HashMap<>(); @@ -203,7 +142,7 @@ public class DJBErpNsmapiAccessTokenService implements HttpClientAccessTokenServ // ObjectMapper objMapper = new ObjectMapper(); // String authJsonBody = objMapper.writeValueAsString(jsonMap); - try (CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();) { + { HttpPost httpPost = new HttpPost(uri); // httpPost.setEntity(new StringEntity(authJsonBody)); httpPost.setHeader("Content-Type", contentType+" charset=" + encode); @@ -266,10 +205,13 @@ public class DJBErpNsmapiAccessTokenService implements HttpClientAccessTokenServ JsonNode data = responseJSON.path("data"); if(data.has("access_token")) { String token = data.path("access_token").asText(); + long intervalSec = oAuthCredentialVo.getIntervalSec() > 0 + ? oAuthCredentialVo.getIntervalSec() + : 24 * 60 * 60; accessToken = new OAuth2AccessTokenVO(); accessToken.setAccessToken(token); - accessToken.setExpiration(new Date(currentTime + 30_000L)); - logger.info("oauthToken =" + accessToken.toString()); + accessToken.setExpiration(new Date(currentTime + intervalSec * 1000L)); + logger.debug("oauthToken =" + accessToken.toString()); return accessToken; } else { throw new Exception("oauth token return null"); @@ -277,8 +219,6 @@ public class DJBErpNsmapiAccessTokenService implements HttpClientAccessTokenServ } else { throw new Exception("oauth token return null"); } -// accessToken.setExpiration(new Date(currentTime + oAuthCredentialVo.getIntervalSec() * 1000L)); - } else { throw new Exception("oauth token return null"); } @@ -289,6 +229,94 @@ public class DJBErpNsmapiAccessTokenService implements HttpClientAccessTokenServ } } + /** + * mTLS 여부/clientId 조합에 맞는 SSLContext로 PoolingHttpClientConnectionManager와 + * CloseableHttpClient를 생성한다. httpClientCache에 의해 조합당 1회만 호출되며, 반환된 + * CloseableHttpClient는 재사용을 위해 닫지 않는다(닫으면 커넥션 풀이 함께 종료된다). + */ + private CloseableHttpClient buildHttpClient(boolean useMtls, String clientId, String adapterGroupName) { + int maxTotalConnections = HttpClientAdapterServiceKey.DEFAULT_MAX_TOTAL_CONNECTIONS; + int maxHostConnections = HttpClientAdapterServiceKey.DEFAULT_MAX_CONNECTION_PER_HOST; + + HttpOutTlsInfoVO mtlsInfo = null; + SSLContext sslContext = null; + PoolingHttpClientConnectionManagerBuilder cmBuilder = PoolingHttpClientConnectionManagerBuilder.create(); + + try { + if (useMtls) { + HttpOutTlsInfoManager tlsManager = HttpOutTlsInfoManager.getInstance(); + if (StringUtils.isNotEmpty(clientId)) { + mtlsInfo = tlsManager.getHttpOutTlsInfo(clientId); + } + } + + if (useMtls && mtlsInfo != null) { + String storeType = mtlsInfo.getStoreType(); + String keyStoreInfo = mtlsInfo.getKeystoreInfo(); + String keyStorePassword = mtlsInfo.getKeystorePassword(); + String trustStoreInfo = mtlsInfo.getTruststoreInfo(); + String trustStorePassword = mtlsInfo.getTruststorePassword(); + + String[] tlsVersions = null; + String[] cipherSuites = null; + + if (StringUtils.isAnyEmpty(keyStoreInfo, keyStorePassword)) { + throw new Exception("mTLS keyStore config error"); + } + + boolean skipTrust = false; + if (StringUtils.isAnyEmpty(trustStoreInfo, trustStorePassword)) { + if (logger.isWarn()) + logger.warn("Skip trustStore validation adapterGroupName : " + adapterGroupName); + skipTrust = true; + } + + sslContext = HttpClient5SSLContextFactory.createMTLSContextFromContent(storeType, keyStoreInfo, + keyStorePassword, trustStoreInfo, trustStorePassword, skipTrust, tlsVersions, cipherSuites); + + SSLConnectionSocketFactory sslSocketFactory = null; + if (testMode) { + sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); + } else { + sslSocketFactory = new SSLConnectionSocketFactory(sslContext); + } + cmBuilder.setSSLSocketFactory(sslSocketFactory); + } else { + sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); + SSLConnectionSocketFactory sslSocketFactory = null; + if(testMode) { + // Hostname verifier 비활성화 (테스트용) + sslSocketFactory = new SSLConnectionSocketFactory( + sslContext + , NoopHostnameVerifier.INSTANCE + ); + } + else { + sslSocketFactory = new SSLConnectionSocketFactory( + sslContext + ); + } + cmBuilder.setSSLSocketFactory(sslSocketFactory); + } + } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException + | IOException | UnrecoverableKeyException e) { + throw new RuntimeException(e); + } catch (Exception e) { + throw new RuntimeException(e); + } + + PoolingHttpClientConnectionManager connectionManager = cmBuilder.build(); + connectionManager.setMaxTotal(maxTotalConnections); + connectionManager.setDefaultMaxPerRoute(maxHostConnections); + + if (logger.isInfo()) { + logger.info("DJBErpNsmapiAccessTokenService] HttpClient(재사용) 생성. adapterGroupName={}, useMtls={}, clientId={}", + adapterGroupName, useMtls, clientId); + } + + return HttpClients.custom().setConnectionManager(connectionManager).build(); + } + /** * URL에 경로를 추가합니다. 중복되는 슬래시를 방지합니다. *