diff --git a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java index 8afe1e1..becbfeb 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java @@ -4,7 +4,7 @@ import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; import com.eactive.eai.common.inflow.AbstractInflowControlManager; import com.eactive.eai.common.inflow.InflowControlUtil; -import com.eactive.eai.common.inflow.dual.DualInflowControlManager; +import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager; import com.eactive.eai.common.util.Logger; public class ReloadInflowClientControlCommand extends Command { @@ -31,10 +31,10 @@ public class ReloadInflowClientControlCommand extends Command { String keyName = (String) args; try { AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager(); - if (!(base instanceof DualInflowControlManager)) { - throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다."); + if (!(base instanceof ClientDualInflowControlManager)) { + throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다."); } - DualInflowControlManager manager = (DualInflowControlManager) base; + ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base; if (keyName != null) { if ("ALL".equals(keyName)) { diff --git a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java index fe02028..97fe72c 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java @@ -4,7 +4,7 @@ import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; import com.eactive.eai.common.inflow.AbstractInflowControlManager; import com.eactive.eai.common.inflow.InflowControlUtil; -import com.eactive.eai.common.inflow.dual.DualInflowControlManager; +import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager; import com.eactive.eai.common.util.Logger; public class RemoveInflowClientControlCommand extends Command { @@ -31,10 +31,10 @@ public class RemoveInflowClientControlCommand extends Command { String key = (String) args; try { AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager(); - if (!(base instanceof DualInflowControlManager)) { - throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다."); + if (!(base instanceof ClientDualInflowControlManager)) { + throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다."); } - DualInflowControlManager manager = (DualInflowControlManager) base; + ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base; manager.removeClient(key); if (logger.isWarn()) logger.warn(this.name + "] " + key + " removed."); diff --git a/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualBucket.java b/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualBucket.java new file mode 100644 index 0000000..da8d686 --- /dev/null +++ b/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualBucket.java @@ -0,0 +1,22 @@ +package com.eactive.eai.common.inflow.dual; + +import com.eactive.eai.common.inflow.InflowTargetVO; + +/** + * DualBucket에 클라이언트 유량제어 메서드를 추가한 인터페이스. + * {@link ClientDualInflowControlManager}가 구현한다. + */ +public interface ClientDualBucket extends DualBucket { + + /** + * 클라이언트 유량제어 체크 — 차단 원인 포함. + * @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단 + */ + String isClientPassDetail(String clientId); + + /** + * 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용. + * @return 등록된 설정이 없으면 null + */ + InflowTargetVO getClientInflowThreshold(String clientId); +} diff --git a/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManager.java b/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManager.java new file mode 100644 index 0000000..606d1e7 --- /dev/null +++ b/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManager.java @@ -0,0 +1,112 @@ +package com.eactive.eai.common.inflow.dual; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; + +import com.eactive.eai.common.inflow.Bucket; +import com.eactive.eai.common.inflow.InflowTargetVO; +import com.eactive.eai.common.inflow.InflowType; +import com.eactive.eai.common.lifecycle.LifecycleException; +import com.eactive.eai.common.util.ApplicationContextProvider; +import com.eactive.eai.common.util.Logger; + +/** + * 어댑터/인터페이스 이중 버킷(DualInflowControlManager)에 클라이언트 유량제어를 추가한 관리자. + * + *
적용 방법: INFLOW.properties에 아래 한 줄 추가. + *
inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager+ */ +@Component +public class ClientDualInflowControlManager extends DualInflowControlManager implements ClientDualBucket { + + private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + + public static synchronized Bucket getBucket() { + return ApplicationContextProvider.getContext().getBean(ClientDualInflowControlManager.class); + } + + private volatile Map
클라이언트 유량제어는 {@link ClientDualBucket}으로 분리됨. */ public interface DualBucket extends Bucket { @@ -22,16 +22,4 @@ public interface DualBucket extends Bucket { * @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단 */ String isInterfacePassDetail(String inter); - - /** - * 클라이언트 유량제어 체크 — 차단 원인 포함. - * @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단 - */ - String isClientPassDetail(String clientId); - - /** - * 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용. - * @return 등록된 설정이 없으면 null - */ - InflowTargetVO getClientInflowThreshold(String clientId); } diff --git a/src/main/java/com/eactive/eai/common/inflow/dual/DualInflowControlManager.java b/src/main/java/com/eactive/eai/common/inflow/dual/DualInflowControlManager.java index de1b827..d6c415a 100644 --- a/src/main/java/com/eactive/eai/common/inflow/dual/DualInflowControlManager.java +++ b/src/main/java/com/eactive/eai/common/inflow/dual/DualInflowControlManager.java @@ -9,7 +9,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; import com.eactive.eai.common.inflow.AbstractInflowControlManager; import com.eactive.eai.common.inflow.Bucket; @@ -22,21 +21,17 @@ import com.eactive.eai.common.util.Logger; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; import io.github.bucket4j.local.LocalBucket; /** - * 어댑터/인터페이스/클라이언트 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자. + * 어댑터/인터페이스 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자. * - *
기존 InflowControlManager는 어댑터/인터페이스에 단일 LocalBucket(복수 Bandwidth)을 사용하여 - * 차단 시 어느 한도(초당/기간)를 초과했는지 알 수 없었다. - * 이 클래스는 그룹 버킷(CustomGroupBucket)과 동일하게 두 버킷을 분리하고 - * {@link DualBucket#isAdapterPassDetail}/{@link DualBucket#isInterfacePassDetail}로 - * 차단 원인을 반환한다. 그룹 버킷 통과 여부는 {@link TargetMetrics}로 카운팅된다. + *
클라이언트 유량제어는 {@link ClientDualInflowControlManager}로 분리됨. * *
적용 방법: INFLOW.properties에 아래 한 줄 추가. - *
inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager+ *
inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager*/ -@Component public class DualInflowControlManager extends AbstractInflowControlManager implements DualBucket { private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); @@ -48,11 +43,9 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple private volatile Map
DualInflowControlManager와 함께 사용: - *
- * inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager - *- * - *
Spring Bean 등록 후 기존 RequestProcessor 대신 이 클래스를 어댑터에 설정한다. - */ -public class DualRequestProcessor extends RequestProcessor { - - @Override - protected String checkClientInflow(Bucket bucket, String clientId) { - if (StringUtils.isBlank(clientId)) return null; - if (bucket instanceof DualBucket) { - return ((DualBucket) bucket).isClientPassDetail(clientId); - } - return null; - } - - @Override - protected InflowTargetVO resolveClientInflowThreshold(Bucket bucket, String clientId) { - if (bucket instanceof DualBucket) { - return ((DualBucket) bucket).getClientInflowThreshold(clientId); - } - return null; - } - - @Override - protected String checkAdapterInflow(Bucket bucket, String adapterGroupName) { - if (bucket instanceof DualBucket) { - return ((DualBucket) bucket).isAdapterPassDetail(adapterGroupName); - } - return super.checkAdapterInflow(bucket, adapterGroupName); - } - - @Override - protected String checkInterfaceInflow(Bucket bucket, String eaiSvcCd) { - if (bucket instanceof DualBucket) { - return ((DualBucket) bucket).isInterfacePassDetail(eaiSvcCd); - } - return super.checkInterfaceInflow(bucket, eaiSvcCd); - } - - /** - * 차단 원인에 따라 에러 메시지에 한도 정보를 포함. - *
start() 없이 버킷 맵을 ReflectionTestUtils로 직접 주입하여 - * isAdapterPassDetail / isInterfacePassDetail / isClientPassDetail / isGroupPass() 의 - * 카운팅 동작과 getXxxMetrics / resetXxxMetrics 메서드를 검증한다. + *
어댑터/인터페이스/그룹 메트릭을 검증한다.
+ * 클라이언트 메트릭은 ClientDualInflowControlManagerMetricsTest 에서 검증한다.
*/
class DualInflowControlManagerMetricsTest {
@@ -100,10 +99,6 @@ class DualInflowControlManagerMetricsTest {
ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
}
- private void injectClientMap(Map RequestProcessor 클래스는 static 초기화 블록에서 EAIServerManager.getInstance()를 호출한다.
- * @BeforeAll에서 mock ApplicationContext를 주입한 뒤 DualRequestProcessor를 최초 로딩시킨다.
- *
- * 주의: DualRequestProcessor를 필드 타입으로 선언하면 테스트 클래스 로딩 시 함께 로딩되어
- * @BeforeAll 실행 전에 NPE가 발생한다. 반드시 메서드 본문 내에서만 참조해야 한다.
- */
-class DualRequestProcessorTest {
-
- @BeforeAll
- static void setupMockApplicationContext() {
- ApplicationContext mockContext = mock(ApplicationContext.class);
- EAIServerManager mockServerManager = mock(EAIServerManager.class);
- when(mockContext.getBean(EAIServerManager.class)).thenReturn(mockServerManager);
- when(mockServerManager.getLocalServerName()).thenReturn("TEST-SERVER");
- when(mockServerManager.getGroupInstId()).thenReturn("TEST-INST");
-
- ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext);
- }
-
- private InflowTargetVO makeVO(String name, long perSec, long threshold, String unit) {
- InflowTargetVO vo = new InflowTargetVO();
- vo.setName(name);
- vo.setThresholdPerSecond(perSec);
- vo.setThreshold(threshold);
- vo.setThresholdTimeUnit(unit);
- vo.setActivate(true);
- return vo;
- }
-
- // -------------------------------------------------------------------------
- // checkAdapterInflow
- // -------------------------------------------------------------------------
-
- @Test
- void checkAdapterInflow_dualBucket_pass_returnsNull() {
- DualRequestProcessor processor = new DualRequestProcessor();
- DualBucket mockBucket = mock(DualBucket.class);
- when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(null);
-
- assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
- verify(mockBucket).isAdapterPassDetail("ADAPTER_A");
- }
-
- @Test
- void checkAdapterInflow_dualBucket_blockedPerSecond() {
- DualRequestProcessor processor = new DualRequestProcessor();
- DualBucket mockBucket = mock(DualBucket.class);
- when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
-
- assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND,
- processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
- }
-
- @Test
- void checkAdapterInflow_dualBucket_blockedThreshold() {
- DualRequestProcessor processor = new DualRequestProcessor();
- DualBucket mockBucket = mock(DualBucket.class);
- when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
-
- assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD,
- processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
- }
-
- @Test
- void checkAdapterInflow_nonDualBucket_pass_returnsNull() {
- DualRequestProcessor processor = new DualRequestProcessor();
- Bucket mockBucket = mock(Bucket.class);
- when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(true);
-
- assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
- }
-
- @Test
- void checkAdapterInflow_nonDualBucket_blocked_returnsBlocked() {
- DualRequestProcessor processor = new DualRequestProcessor();
- Bucket mockBucket = mock(Bucket.class);
- when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(false);
-
- assertEquals("BLOCKED", processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
- }
-
- // -------------------------------------------------------------------------
- // checkInterfaceInflow
- // -------------------------------------------------------------------------
-
- @Test
- void checkInterfaceInflow_dualBucket_pass_returnsNull() {
- DualRequestProcessor processor = new DualRequestProcessor();
- DualBucket mockBucket = mock(DualBucket.class);
- when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(null);
-
- assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001"));
- verify(mockBucket).isInterfacePassDetail("IF_001");
- }
-
- @Test
- void checkInterfaceInflow_dualBucket_blockedPerSecond() {
- DualRequestProcessor processor = new DualRequestProcessor();
- DualBucket mockBucket = mock(DualBucket.class);
- when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
-
- assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND,
- processor.checkInterfaceInflow(mockBucket, "IF_001"));
- }
-
- @Test
- void checkInterfaceInflow_nonDualBucket_pass_returnsNull() {
- DualRequestProcessor processor = new DualRequestProcessor();
- Bucket mockBucket = mock(Bucket.class);
- when(mockBucket.isInterfacePass("IF_001")).thenReturn(true);
-
- assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001"));
- }
-
- @Test
- void checkInterfaceInflow_nonDualBucket_blocked_returnsBlocked() {
- DualRequestProcessor processor = new DualRequestProcessor();
- Bucket mockBucket = mock(Bucket.class);
- when(mockBucket.isInterfacePass("IF_001")).thenReturn(false);
-
- assertEquals("BLOCKED", processor.checkInterfaceInflow(mockBucket, "IF_001"));
- }
-
- // -------------------------------------------------------------------------
- // buildInflowTargetErrorMsg
- // -------------------------------------------------------------------------
-
- @Test
- void buildInflowTargetErrorMsg_perSecond_includesReqPerSec() {
- DualRequestProcessor processor = new DualRequestProcessor();
- InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
-
- String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
-
- assertEquals("API 호출 한도 초과 [결제API: 30req/sec]", msg);
- }
-
- @Test
- void buildInflowTargetErrorMsg_threshold_includesReqPerUnit() {
- DualRequestProcessor processor = new DualRequestProcessor();
- InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
-
- String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
-
- assertEquals("API 호출 한도 초과 [결제API: 1000req/MIN]", msg);
- }
-
- @Test
- void buildInflowTargetErrorMsg_blockedFallback_simpleFormat() {
- DualRequestProcessor processor = new DualRequestProcessor();
- InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
-
- String msg = processor.buildInflowTargetErrorMsg(vo, "BLOCKED");
-
- assertEquals("API 호출 한도 초과 [결제API]", msg);
- }
-
- @Test
- void buildInflowTargetErrorMsg_nullBlockedType_simpleFormat() {
- DualRequestProcessor processor = new DualRequestProcessor();
- InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
-
- String msg = processor.buildInflowTargetErrorMsg(vo, null);
-
- assertEquals("API 호출 한도 초과 [결제API]", msg);
- }
-
- @Test
- void buildInflowTargetErrorMsg_hourUnit() {
- DualRequestProcessor processor = new DualRequestProcessor();
- InflowTargetVO vo = makeVO("조회API", 10, 500, "HOUR");
-
- String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
-
- assertEquals("API 호출 한도 초과 [조회API: 500req/HOUR]", msg);
- }
-}