From d275aa12c4c9f7f4be8a7d5454079da196cec56f Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 1 Sep 2026 14:50:39 +0900 Subject: [PATCH] =?UTF-8?q?Admin=20GatewayClient=20=EB=8B=A4=EC=A4=91=20ba?= =?UTF-8?q?se=20URL=20=EB=B0=8F=20failover=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80:=20-=20admin.base-url=20=EB=8B=A4=EC=A4=91?= =?UTF-8?q?=20=EC=A3=BC=EC=86=8C=20=EC=A7=80=EC=9B=90=20=EB=B0=8F=20?= =?UTF-8?q?=EC=88=9C=EC=B0=A8=20failover=20=EA=B5=AC=ED=98=84=20-=20RestTe?= =?UTF-8?q?mplate=20=ED=83=80=EC=9E=84=EC=95=84=EC=9B=83=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B3=80=EA=B2=BD=20(=EC=97=B0=EA=B2=B0/=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0:=205=EC=B4=88=EB=A1=9C=20=EB=8B=A8=EC=B6=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/app/service/AdminGatewayClient.java | 91 ++++++++++++++++--- .../portal/config/RestTemplateConfig.java | 8 +- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/eactive/apim/portal/apps/app/service/AdminGatewayClient.java b/src/main/java/com/eactive/apim/portal/apps/app/service/AdminGatewayClient.java index 7e140f3..7915a04 100644 --- a/src/main/java/com/eactive/apim/portal/apps/app/service/AdminGatewayClient.java +++ b/src/main/java/com/eactive/apim/portal/apps/app/service/AdminGatewayClient.java @@ -1,18 +1,28 @@ package com.eactive.apim.portal.apps.app.service; import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; /** * 관리자(admin) 포털의 내부 API를 호출하는 클라이언트. * *

GW 인증서버(TSEAIAU01) 제어는 포털이 직접 하지 않고, broadcast 인프라를 갖춘 admin 에 위임한다. - * admin base URL 은 {@code PTL_PROPERTY} (group={@code Portal}, name={@code djb.admin.base-url}) 에서 조회한다.

+ * admin base URL 은 {@code PTL_PROPERTY} (group={@code Portal}, name={@code admin.base-url}) 에서 조회한다.

+ * + *

다중 base URL / failover — 프로퍼티 값에 콤마({@code ,}) 또는 개행으로 여러 admin 주소를 넣을 수 있다. + * 호출은 기재된 순서대로 시도하며, 통신 오류(연결 실패/타임아웃/HTTP 오류)면 다음 주소로 넘어간다. + * 모두 실패하면 마지막 오류를 던진다. 반면 admin 이 정상 응답하면서 {@code success=false} 를 준 것은 + * 업무 처리 실패이므로 failover 하지 않고 즉시 실패시킨다(다른 노드도 같은 결과이며 중복 처리 위험).

*/ @Slf4j @Service @@ -22,6 +32,8 @@ public class AdminGatewayClient { private static final String PROP_GROUP = "Portal"; private static final String PROP_ADMIN_BASE_URL = "admin.base-url"; private static final String DEFAULT_ADMIN_BASE_URL = "http://localhost:39120"; + private static final String PROP_ADMIN_BASE_URL_DESC = + "admin(관리자포털) 내부 API base URL. 콤마(,) 또는 줄바꿈으로 여러 개 지정 시 앞에서부터 failover"; private static final String CLIENT_BLOCK_PATH = "/onl/admin/authserver/clientBlock.json?clientId={clientId}"; private final RestTemplate restTemplate; @@ -30,26 +42,79 @@ public class AdminGatewayClient { /** * clientId 의 GW 인증 클라이언트 차단(appstatus=0) + GW 캐시 리로드를 admin 에 요청한다. * + *

base URL 이 여러 개면 앞에서부터 순서대로 시도한다(failover).

+ * * @param clientId 차단할 클라이언트 ID - * @throws RuntimeException admin 미응답/네트워크 오류 또는 admin 처리 실패 시 (호출측에서 처리) + * @throws RuntimeException admin 미응답/네트워크 오류(전 노드 실패) 또는 admin 처리 실패 시 (호출측에서 처리) */ public void blockClient(String clientId) { - String baseUrl = portalPropertyService.getOrCreateProperty( - PROP_GROUP, PROP_ADMIN_BASE_URL, DEFAULT_ADMIN_BASE_URL, "admin(관리자포털) 내부 API base URL"); + List baseUrls = resolveBaseUrls(); - String url = stripTrailingSlashes(baseUrl) + CLIENT_BLOCK_PATH; + RestClientException lastError = null; + for (int i = 0; i < baseUrls.size(); i++) { + String baseUrl = baseUrls.get(i); + String url = baseUrl + CLIENT_BLOCK_PATH; - // 네트워크/HTTP 오류는 RestTemplate 이 예외로 던진다. - ResponseEntity response = restTemplate.postForEntity(url, null, Map.class, clientId); + ResponseEntity response; + try { + response = restTemplate.postForEntity(url, null, Map.class, clientId); + } catch (RestClientException e) { + // 통신 계층 실패 — 다음 admin 주소로 failover + lastError = e; + log.warn("admin clientBlock 호출 실패({}/{}) - baseUrl={}, clientId={}, cause={}", + i + 1, baseUrls.size(), baseUrl, clientId, e.toString()); + continue; + } - Map body = response.getBody(); - boolean success = body != null && Boolean.TRUE.equals(body.get("success")); - if (!success) { - String msg = body != null ? String.valueOf(body.get("msg")) : "응답 본문 없음"; - throw new IllegalStateException("admin clientBlock 처리 실패 - clientId=" + clientId + ", msg=" + msg); + // 여기까지 왔으면 admin 이 응답한 것 — 업무 실패는 failover 대상이 아니다. + Map body = response.getBody(); + boolean success = body != null && Boolean.TRUE.equals(body.get("success")); + if (!success) { + String msg = body != null ? String.valueOf(body.get("msg")) : "응답 본문 없음"; + throw new IllegalStateException( + "admin clientBlock 처리 실패 - baseUrl=" + baseUrl + ", clientId=" + clientId + ", msg=" + msg); + } + + log.info("admin GW 차단/리로드 위임 성공 - baseUrl={}, clientId={}", baseUrl, clientId); + return; } - log.info("admin GW 차단/리로드 위임 성공 - clientId={}", clientId); + throw new IllegalStateException( + "admin clientBlock 호출 실패 - 모든 admin 주소 응답 없음 (" + baseUrls + "), clientId=" + clientId, lastError); + } + + /** + * 프로퍼티의 admin base URL 목록을 순서대로 반환한다. + * + *

구분자는 콤마({@code ,})와 개행({@code \r\n})이며, 공백 항목과 중복은 제거하고 순서는 보존한다. + * 값이 비어 있거나 유효 항목이 없으면 기본값 하나만 반환한다.

+ */ + private List resolveBaseUrls() { + String raw = portalPropertyService.getOrCreateProperty( + PROP_GROUP, PROP_ADMIN_BASE_URL, DEFAULT_ADMIN_BASE_URL, PROP_ADMIN_BASE_URL_DESC); + + List urls = parseBaseUrls(raw); + if (urls.isEmpty()) { + log.warn("admin.base-url 프로퍼티가 비어 있어 기본값 사용 - {}", DEFAULT_ADMIN_BASE_URL); + return parseBaseUrls(DEFAULT_ADMIN_BASE_URL); + } + return urls; + } + + /** + * 콤마/개행으로 구분된 base URL 문자열을 정규화된 목록으로 파싱한다. + */ + static List parseBaseUrls(String raw) { + Set ordered = new LinkedHashSet<>(); + if (raw != null) { + for (String token : raw.split("[,\\r\\n]")) { + String url = stripTrailingSlashes(token.trim()); + if (!url.isEmpty()) { + ordered.add(url); + } + } + } + return new ArrayList<>(ordered); } /** diff --git a/src/main/java/com/eactive/apim/portal/config/RestTemplateConfig.java b/src/main/java/com/eactive/apim/portal/config/RestTemplateConfig.java index 314d12d..006522f 100644 --- a/src/main/java/com/eactive/apim/portal/config/RestTemplateConfig.java +++ b/src/main/java/com/eactive/apim/portal/config/RestTemplateConfig.java @@ -24,11 +24,11 @@ public class RestTemplateConfig { public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - // 연결 타임아웃: 10초 - factory.setConnectTimeout(10000); + // 연결 타임아웃: 5초 (admin base-url failover 시 다음 후보로 빨리 넘어가기 위함) + factory.setConnectTimeout(5000); - // 읽기 타임아웃: 30초 - factory.setReadTimeout(30000); + // 읽기 타임아웃: 5초 + factory.setReadTimeout(5000); return new RestTemplate(factory); }