From ad7764ecd3278891823b2799b906f6b4e451f0e6 Mon Sep 17 00:00:00 2001 From: devmrko Date: Tue, 23 Jun 2026 11:50:20 +0900 Subject: [PATCH] [Developer] #424 require explicit ORDS endpoint Refs #424 --- .env.example | 4 +++ .../domain/probe/ProbeStatus.java | 2 ++ .../service/OrdsProbeService.java | 35 +++++++++++++++++-- .../service/ProbeErrorClassifier.java | 17 +++++++-- src/main/resources/application.yml | 2 +- .../service/ProbeErrorClassifierTest.java | 19 ++++++++++ 6 files changed, 74 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index d6cb043..a952162 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,10 @@ export ADB_PASSWORD="" # 비워두면 # 편의: sqlplus 한 줄 connect 문자열 (자동 합성됨; 직접 안 건드려도 됨) # export ADB_CONN="${ADB_USER}/${ADB_PASSWORD}@${ADB_TNS}" +# --- (2b) Spring Boot 백오피스 --- +export BACKOFFICE_ORDS_BASE_URL="" # 예: https:///ords +export BACKOFFICE_ORDS_TIMEOUT_SECONDS="10" + # --- (3) 데모용 ADB 엔드유저 비밀번호 (sql/adb/07_end_users.sql 에서 사용) --- # ADB 비번 정책: 12자 이상, 대/소/숫자/특수 조합. # 4명의 데모 유저: diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeStatus.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeStatus.java index 146f1fa..287c8a5 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeStatus.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeStatus.java @@ -8,6 +8,8 @@ public enum ProbeStatus { OBJECT_DISABLED, INVALID_TOKEN, OBJECT_NOT_ACCESSIBLE, + ORDS_NOT_CONFIGURED, + ORDS_UNAVAILABLE, ORDS_TIMEOUT, INVALID_ORDS_RESPONSE, UNKNOWN_ERROR diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java index 04ad755..f303327 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java @@ -67,6 +67,14 @@ public class OrdsProbeService { } public ProbeResult runProbe(ProbeCommand command) { + if (properties.ords().baseUrl() == null || properties.ords().baseUrl().isBlank()) { + return auditAndReturn(command, ProbeResult.blocked( + ProbeStatus.ORDS_NOT_CONFIGURED, + ProbeStatus.ORDS_NOT_CONFIGURED.name(), + "ORDS base URL이 설정되지 않았습니다. 실제 ORDS 도메인을 BACKOFFICE_ORDS_BASE_URL에 설정한 뒤 백오피스를 재시작하세요." + )); + } + BearerTokenRecord token = tokenService.findById(command.keyId()); if (token == null) { return auditAndReturn(command, ProbeResult.blocked( @@ -102,8 +110,8 @@ public class OrdsProbeService { return auditAndReturn(command, ProbeResult.blocked( status, status.name(), trimMessage(e.getResponseBodyAsString()))); } catch (ResourceAccessException e) { - ProbeStatus status = errorClassifier.isTimeout(e) ? ProbeStatus.ORDS_TIMEOUT : ProbeStatus.UNKNOWN_ERROR; - return auditAndReturn(command, ProbeResult.blocked(status, status.name(), e.getMessage())); + ProbeStatus status = classifyResourceAccess(e); + return auditAndReturn(command, ProbeResult.blocked(status, status.name(), resourceAccessMessage(status, e))); } catch (Exception e) { return auditAndReturn(command, ProbeResult.blocked( ProbeStatus.INVALID_ORDS_RESPONSE, "INVALID_ORDS_RESPONSE", e.getMessage())); @@ -193,6 +201,29 @@ public class OrdsProbeService { return result; } + private ProbeStatus classifyResourceAccess(ResourceAccessException exception) { + if (errorClassifier.isTimeout(exception)) { + return ProbeStatus.ORDS_TIMEOUT; + } + if (errorClassifier.isUnavailable(exception)) { + return ProbeStatus.ORDS_UNAVAILABLE; + } + return ProbeStatus.UNKNOWN_ERROR; + } + + private String resourceAccessMessage(ProbeStatus status, ResourceAccessException exception) { + String detail = trimMessage(exception.getMessage()); + if (status == ProbeStatus.ORDS_UNAVAILABLE) { + return "ORDS 서버에 연결할 수 없습니다. BACKOFFICE_ORDS_BASE_URL의 실제 ORDS 도메인, ORDS 실행 상태, 네트워크 접근을 확인하세요. 상세: " + + detail; + } + if (status == ProbeStatus.ORDS_TIMEOUT) { + return "ORDS 응답 시간이 초과되었습니다. ORDS 상태와 BACKOFFICE_ORDS_TIMEOUT_SECONDS 설정을 확인하세요. 상세: " + + detail; + } + return detail; + } + private String trimMessage(String body) { if (body == null) { return null; diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/ProbeErrorClassifier.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/ProbeErrorClassifier.java index f14f243..e700111 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/ProbeErrorClassifier.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/ProbeErrorClassifier.java @@ -1,7 +1,10 @@ package com.cloudhandson.vpdbackoffice.service; import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus; +import java.net.ConnectException; +import java.net.NoRouteToHostException; import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Component; import org.springframework.web.client.ResourceAccessException; @@ -24,9 +27,19 @@ public class ProbeErrorClassifier { } public boolean isTimeout(ResourceAccessException exception) { - Throwable cause = exception; + return hasCause(exception, SocketTimeoutException.class); + } + + public boolean isUnavailable(ResourceAccessException exception) { + return hasCause(exception, ConnectException.class) + || hasCause(exception, NoRouteToHostException.class) + || hasCause(exception, UnknownHostException.class); + } + + private boolean hasCause(Throwable throwable, Class type) { + Throwable cause = throwable; while (cause != null) { - if (cause instanceof SocketTimeoutException) { + if (type.isInstance(cause)) { return true; } cause = cause.getCause(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 712e3c6..b75dd9c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -29,5 +29,5 @@ backoffice: token: max-days: ${BACKOFFICE_TOKEN_MAX_DAYS:365} ords: - base-url: ${BACKOFFICE_ORDS_BASE_URL:http://localhost:8081/ords} + base-url: ${BACKOFFICE_ORDS_BASE_URL:} timeout-seconds: ${BACKOFFICE_ORDS_TIMEOUT_SECONDS:10} diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/service/ProbeErrorClassifierTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/service/ProbeErrorClassifierTest.java index 4ca9420..214281a 100644 --- a/src/test/java/com/cloudhandson/vpdbackoffice/service/ProbeErrorClassifierTest.java +++ b/src/test/java/com/cloudhandson/vpdbackoffice/service/ProbeErrorClassifierTest.java @@ -3,8 +3,11 @@ package com.cloudhandson.vpdbackoffice.service; import static org.assertj.core.api.Assertions.assertThat; import com.cloudhandson.vpdbackoffice.domain.probe.ProbeStatus; +import java.net.ConnectException; +import java.net.SocketTimeoutException; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; +import org.springframework.web.client.ResourceAccessException; class ProbeErrorClassifierTest { @@ -27,4 +30,20 @@ class ProbeErrorClassifierTest { assertThat(classifier.classify(HttpStatus.UNAUTHORIZED, "unauthorized")) .isEqualTo(ProbeStatus.INVALID_TOKEN); } + + @Test + void detectsOrdsConnectionRefused() { + assertThat(classifier.isUnavailable(new ResourceAccessException( + "I/O error", + new ConnectException("Connection refused") + ))).isTrue(); + } + + @Test + void detectsOrdsTimeout() { + assertThat(classifier.isTimeout(new ResourceAccessException( + "I/O error", + new SocketTimeoutException("Read timed out") + ))).isTrue(); + } }