DJBErpNsmapiAccessTokenService 토큰 만료시간 및 HTTP 클라이언트 재사용 수정

- 하드코딩된 30초 만료를 제거하고 oAuthCredentialVo.getIntervalSec() 기준으로
  복원 (미설정 시 24시간 폴백), 스케줄러 주기와 불일치하던 문제 해결
- execute() 호출마다 PoolingHttpClientConnectionManager/CloseableHttpClient를
  새로 만들고 즉시 닫던 것을, mTLS 여부+clientId 조합별로 1회만 생성해 재사용하도록
  변경. 이 서비스는 HttpClientAccessTokenServiceFactoryByDB에 싱글턴으로 캐시되어
  재사용되므로 인스턴스 필드로 안전하게 캐시 가능
This commit is contained in:
curry772
2026-07-22 09:11:20 +09:00
parent 88f774e6b5
commit c44b331a32
@@ -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<String, CloseableHttpClient> 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<String,Object> 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에 경로를 추가합니다. 중복되는 슬래시를 방지합니다.
*