From 06f0d389a659a14853300315ddadce95c43dc4d7 Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 29 Apr 2026 13:32:44 +0900 Subject: [PATCH 1/2] =?UTF-8?q?test:=20TypedCircuitBreakerManager=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20idCBConfigMap=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TypedCircuitBreakerManagerTest 추가 (JUnit5/Mockito, DB/Spring 없이 동작) - getCircuitBreaker 조회/fallback/비활성화/normalizeUseYn 검증 (1-1~1-5) - apiId 별 CB 인스턴스 독립성 및 상태 격리 검증 (2-1~2-2) - CLOSED/OPEN/HALF_OPEN 상태 전이 전체 검증 (3-1~3-6) - reload(key) 설정 갱신/비활성화 제거, reload() 레지스트리 재생성 검증 (4-1~4-3) - 생명주기 isStarted 검증 (5-1) - init()에서 idCBConfigMap.remove를 put으로 수정 (reload(key) 동작 오류 버그 수정) Co-Authored-By: Claude Sonnet 4.6 --- .../type/TypedCircuitBreakerManager.java | 2 +- .../type/TypedCircuitBreakerManagerTest.java | 450 ++++++++++++++++++ 2 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManagerTest.java diff --git a/src/main/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManager.java b/src/main/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManager.java index 6028b9d..f2c36bf 100644 --- a/src/main/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManager.java +++ b/src/main/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManager.java @@ -116,7 +116,7 @@ public class TypedCircuitBreakerManager implements Lifecycle { CircuitBreakerConfig circuitBreakerConfig = this.createCircuitBreakerConfig(vo); vo.setCircuitBreakerConfig(circuitBreakerConfig); this.apiIdCBConfigMap.put(vo.getName(), vo); - this.idCBConfigMap.remove(vo.getId(), vo); + this.idCBConfigMap.put(vo.getId(), vo); } } diff --git a/src/test/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManagerTest.java b/src/test/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManagerTest.java new file mode 100644 index 0000000..cefa02c --- /dev/null +++ b/src/test/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManagerTest.java @@ -0,0 +1,450 @@ +package com.eactive.eai.common.circuitBreaker.type; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.context.support.GenericApplicationContext; + +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.ApplicationContextProvider; + +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; + +/** + * TypedCircuitBreakerManager 단위 테스트 + * + * DB / 실 Spring 컨텍스트 없이 동작한다. + * - PropManager : Mockito Mock (DB 없이 설정값 반환) + * - TypedCircuitBreakerDAO : Mockito Mock (JPA 없이 VO 반환) + * - Spring Bean : GenericApplicationContext 로 수동 구성 + * + * 테스트 격리: + * 각 테스트는 고유한 apiId 를 사용하거나 manager.reload() 로 레지스트리를 초기화한다. + * manager.reload() 는 CircuitBreakerRegistry 를 새로 생성하므로 이전 상태가 제거된다. + */ +@TestMethodOrder(MethodOrderer.DisplayName.class) +class TypedCircuitBreakerManagerTest { + + // 슬라이딩 윈도우·최소 호출을 4로 설정해 빠른 상태 전이를 유도한다. + private static final int DEFAULT_FAILURE_RATE = 50; + private static final int DEFAULT_SLIDING_WINDOW = 4; + private static final int DEFAULT_MIN_CALLS = 4; + private static final int DEFAULT_WAIT_DURATION_SEC = 1; // OPEN 대기 1초 (테스트용) + private static final int DEFAULT_HALF_OPEN_PERMITS = 2; + private static final int DEFAULT_SLOW_RATE = 100; + private static final int DEFAULT_SLOW_DURATION_SEC = 5; + + private static GenericApplicationContext ctx; + private static PropManager mockPropManager; + private static TypedCircuitBreakerDAO mockDao; + private static TypedCircuitBreakerManager manager; + + // ------------------------------------------------------------------------- + // 픽스처 설정 / 해제 + // ------------------------------------------------------------------------- + + @BeforeAll + static void setUpClass() throws Exception { + mockPropManager = mock(PropManager.class); + mockDao = mock(TypedCircuitBreakerDAO.class); + + setupDefaultPropManagerMock(); + when(mockDao.getList()).thenReturn(Collections.emptyList()); + + // TypedCircuitBreakerManager 인스턴스 생성 후 DAO 리플렉션 주입 + manager = new TypedCircuitBreakerManager(); + injectField(manager, "dao", mockDao); + + // DB / 실 Spring 없이 최소 컨텍스트 구성 + ctx = new GenericApplicationContext(); + ctx.getBeanFactory().registerSingleton("propManager", mockPropManager); + ctx.getBeanFactory().registerSingleton("typedCircuitBreakerManager", manager); + ctx.registerBeanDefinition("applicationContextProvider", + BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class) + .getBeanDefinition()); + ctx.refresh(); + + manager.start(); + } + + @AfterAll + static void tearDownClass() { + if (ctx != null) ctx.close(); + } + + @BeforeEach + void resetMocks() throws Exception { + reset(mockDao); + setupDefaultPropManagerMock(); + when(mockDao.getList()).thenReturn(Collections.emptyList()); + } + + // ------------------------------------------------------------------------- + // 헬퍼 + // ------------------------------------------------------------------------- + + private static void setupDefaultPropManagerMock() { + when(mockPropManager.getProperty("CircuitBreaker", "failureRateThreshold")) + .thenReturn(String.valueOf(DEFAULT_FAILURE_RATE)); + when(mockPropManager.getProperty("CircuitBreaker", "slidingWindowSize")) + .thenReturn(String.valueOf(DEFAULT_SLIDING_WINDOW)); + when(mockPropManager.getProperty("CircuitBreaker", "minimumNumberOfCalls")) + .thenReturn(String.valueOf(DEFAULT_MIN_CALLS)); + when(mockPropManager.getProperty("CircuitBreaker", "waitDurationInOpenState")) + .thenReturn(String.valueOf(DEFAULT_WAIT_DURATION_SEC)); + when(mockPropManager.getProperty("CircuitBreaker", "permittedNumberOfCallsInHalfOpenState")) + .thenReturn(String.valueOf(DEFAULT_HALF_OPEN_PERMITS)); + when(mockPropManager.getProperty("CircuitBreaker", "slowCallRateThreshold")) + .thenReturn(String.valueOf(DEFAULT_SLOW_RATE)); + when(mockPropManager.getProperty("CircuitBreaker", "slowCallDurationThreshold")) + .thenReturn(String.valueOf(DEFAULT_SLOW_DURATION_SEC)); + when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("1"); + } + + private static void injectField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + /** 테스트용 TypedCircuitBreakerVO 생성 (기본 설정값 사용) */ + private TypedCircuitBreakerVO buildVo(String id, String apiId, String useYn) { + return TypedCircuitBreakerVO.builder() + .id(id).name(apiId).type("COUNT_BASED").desc(apiId + " 테스트 설정") + .failureRateThreshold(DEFAULT_FAILURE_RATE) + .slidingWindowSize(DEFAULT_SLIDING_WINDOW) + .minimumNumberOfCalls(DEFAULT_MIN_CALLS) + .waitDurationInOpenState(DEFAULT_WAIT_DURATION_SEC) + .permittedNumberOfCallsInHalfOpenState(DEFAULT_HALF_OPEN_PERMITS) + .slowCallRateThreshold(DEFAULT_SLOW_RATE) + .slowCallDurationThreshold(DEFAULT_SLOW_DURATION_SEC) + .useYn(useYn) + .build(); + } + + /** CB를 count 회 실패 처리한다 */ + private void recordFailures(CircuitBreaker cb, int count) { + for (int i = 0; i < count; i++) { + try { + cb.executeCallable(() -> { throw new RuntimeException("테스트 강제 오류"); }); + } catch (Exception ignored) {} + } + } + + // ========================================================================= + // 1. getCircuitBreaker - 기본 조회 동작 + // ========================================================================= + + @Test + @DisplayName("1-1. 등록된 apiId → 해당 설정의 CircuitBreaker 반환") + void testGetCircuitBreaker_registeredApiId_returnsTypedCircuitBreaker() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-001", "API_REG", "1"))); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_REG"); + + assertNotNull(cb); + assertEquals("API_REG", cb.getName()); + } + + @Test + @DisplayName("1-2. 미등록 apiId → default CircuitBreaker 로 fallback") + void testGetCircuitBreaker_unregisteredApiId_returnsDefaultCircuitBreaker() throws Exception { + when(mockDao.getList()).thenReturn(Collections.emptyList()); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_UNKNOWN"); + + assertNotNull(cb, "미등록 apiId 는 default CB 를 반환해야 한다"); + assertEquals("default", cb.getName()); + } + + @Test + @DisplayName("1-3. useYn=0 인 apiId → default CircuitBreaker 로 fallback") + void testGetCircuitBreaker_disabledApiId_returnsDefaultCircuitBreaker() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-002", "API_DISABLED", "0"))); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_DISABLED"); + + assertNotNull(cb, "비활성 apiId 는 default CB 로 fallback 되어야 한다"); + assertEquals("default", cb.getName()); + } + + @Test + @DisplayName("1-4. apiId 도 default 도 비활성(useYn=0) → null 반환") + void testGetCircuitBreaker_defaultDisabled_returnsNull() throws Exception { + when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("0"); + when(mockDao.getList()).thenReturn(Collections.emptyList()); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("ANY_API"); + + assertNull(cb, "default 도 비활성이면 null 을 반환해야 한다"); + } + + @Test + @DisplayName("1-5. PropManager useYn=Y → normalizeUseYn 으로 '1' 정규화 → default 활성") + void testGetCircuitBreaker_defaultUseYn_Y_isNormalizedToEnabled() throws Exception { + when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("Y"); + when(mockDao.getList()).thenReturn(Collections.emptyList()); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("ANY_API"); + + assertNotNull(cb, "PropManager useYn=Y 는 활성으로 정규화되어야 한다"); + assertEquals("default", cb.getName()); + } + + // ========================================================================= + // 2. apiId 별 독립성 + // ========================================================================= + + @Test + @DisplayName("2-1. 서로 다른 apiId → 독립적인 CircuitBreaker 인스턴스 반환") + void testDifferentApiIds_haveIndependentCircuitBreakerInstances() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList( + buildVo("uuid-A", "API_INDEP_A", "1"), + buildVo("uuid-B", "API_INDEP_B", "1"))); + manager.reload(); + + CircuitBreaker cbA = manager.getCircuitBreaker("API_INDEP_A"); + CircuitBreaker cbB = manager.getCircuitBreaker("API_INDEP_B"); + + assertNotNull(cbA); + assertNotNull(cbB); + assertNotSame(cbA, cbB, "서로 다른 apiId 는 다른 CB 인스턴스여야 한다"); + assertEquals("API_INDEP_A", cbA.getName()); + assertEquals("API_INDEP_B", cbB.getName()); + } + + @Test + @DisplayName("2-2. API_A 실패 → OPEN, API_B 는 CLOSED 유지 (상태 격리)") + void testDifferentApiIds_statesAreIsolated() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList( + buildVo("uuid-ISO-A", "API_ISO_A", "1"), + buildVo("uuid-ISO-B", "API_ISO_B", "1"))); + manager.reload(); + + CircuitBreaker cbA = manager.getCircuitBreaker("API_ISO_A"); + CircuitBreaker cbB = manager.getCircuitBreaker("API_ISO_B"); + + // API_ISO_A 만 minimumNumberOfCalls=4 회 실패 + recordFailures(cbA, 4); + + assertEquals(CircuitBreaker.State.OPEN, cbA.getState(), "API_ISO_A 는 OPEN 이어야 한다"); + assertEquals(CircuitBreaker.State.CLOSED, cbB.getState(), "API_ISO_B 는 CLOSED 를 유지해야 한다"); + } + + // ========================================================================= + // 3. 상태 전이 + // ========================================================================= + + @Test + @DisplayName("3-1. 실패율 50% 초과(4/4 실패) → CLOSED → OPEN") + void testCircuitBreaker_closedToOpen_afterFailureThresholdExceeded() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-001", "API_ST_OPEN", "1"))); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_ST_OPEN"); + assertEquals(CircuitBreaker.State.CLOSED, cb.getState(), "초기 상태는 CLOSED 여야 한다"); + + // minimumNumberOfCalls=4, failureRateThreshold=50% → 4/4 실패(100%) → OPEN + recordFailures(cb, 4); + + assertEquals(CircuitBreaker.State.OPEN, cb.getState(), "실패율 초과 시 OPEN 상태여야 한다"); + } + + @Test + @DisplayName("3-2. 실패율 미달(1/4 실패, 25%) → CLOSED 유지") + void testCircuitBreaker_belowFailureThreshold_remainsClosed() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-002", "API_ST_BELOW", "1"))); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_ST_BELOW"); + + // 4회 중 3회 성공, 1회 실패 → 실패율 25% < 50% → CLOSED 유지 + // Resilience4j 는 >= 비교: 50% >= 50% 는 OPEN 이므로 25% 로 낮춰야 CLOSED 유지 + try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {} + try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {} + try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {} + recordFailures(cb, 1); + + assertEquals(CircuitBreaker.State.CLOSED, cb.getState(), "실패율 25% (< threshold 50%) 이면 CLOSED 를 유지해야 한다"); + } + + @Test + @DisplayName("3-3. CB OPEN 상태 → 모든 요청 즉시 차단 (CallNotPermittedException)") + void testCircuitBreaker_open_blocksAllRequests() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-003", "API_ST_BLOCK", "1"))); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_ST_BLOCK"); + recordFailures(cb, 4); + assertEquals(CircuitBreaker.State.OPEN, cb.getState()); + + assertThrows(CallNotPermittedException.class, + () -> cb.executeCallable(() -> "도달하면 안 됨"), + "OPEN 상태에서 CallNotPermittedException 이 발생해야 한다"); + } + + @Test + @DisplayName("3-4. OPEN → waitDuration(1초) 경과 → 첫 요청 시 HALF_OPEN 전이") + void testCircuitBreaker_open_transitionsToHalfOpenAfterWaitDuration() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-004", "API_ST_HALFOPEN", "1"))); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_ST_HALFOPEN"); + recordFailures(cb, 4); + assertEquals(CircuitBreaker.State.OPEN, cb.getState()); + + // waitDurationInOpenState = 1초 대기 후 요청 → HALF_OPEN 전이 + Thread.sleep(1200); + try { cb.executeCallable(() -> "HALF_OPEN 트리거"); } catch (Exception ignored) {} + + assertEquals(CircuitBreaker.State.HALF_OPEN, cb.getState(), + "waitDuration 경과 후 첫 요청 시 HALF_OPEN 상태여야 한다"); + } + + @Test + @DisplayName("3-5. HALF_OPEN → 성공(permittedCalls=2회) → CLOSED 복구") + void testCircuitBreaker_halfOpen_transitionsToClosedOnSuccess() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-005", "API_ST_RECOVER", "1"))); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_ST_RECOVER"); + recordFailures(cb, 4); + + Thread.sleep(1200); + + // HALF_OPEN 에서 permittedNumberOfCallsInHalfOpenState=2 회 성공 + for (int i = 0; i < DEFAULT_HALF_OPEN_PERMITS; i++) { + final String result = cb.executeCallable(() -> "성공"); + assertEquals("성공", result); + } + + assertEquals(CircuitBreaker.State.CLOSED, cb.getState(), + "HALF_OPEN 에서 허용 호출 수 이상 성공하면 CLOSED 로 복구되어야 한다"); + } + + @Test + @DisplayName("3-6. HALF_OPEN → 실패 → 다시 OPEN 재전이") + void testCircuitBreaker_halfOpen_transitionsBackToOpenOnFailure() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-006", "API_ST_REOPEN", "1"))); + manager.reload(); + + CircuitBreaker cb = manager.getCircuitBreaker("API_ST_REOPEN"); + recordFailures(cb, 4); + + Thread.sleep(1200); + + // HALF_OPEN 에서 permittedNumberOfCallsInHalfOpenState=2 회 모두 실패해야 OPEN 으로 재전이된다. + // Resilience4j 는 허용 호출 수(2회)를 모두 소진한 후에 실패율을 평가하므로 + // 1회만 실패하면 HALF_OPEN 상태가 유지된다. + recordFailures(cb, DEFAULT_HALF_OPEN_PERMITS); + + assertEquals(CircuitBreaker.State.OPEN, cb.getState(), + "HALF_OPEN 에서 허용 호출 수(2회) 모두 실패하면 다시 OPEN 으로 전이되어야 한다"); + } + + // ========================================================================= + // 4. reload + // ========================================================================= + + @Test + @DisplayName("4-1. reload(key) → CircuitBreakerConfig 갱신 (waitDuration 1초 → 60초)") + void testReload_key_updatesCircuitBreakerConfig() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-RLD-001", "API_RELOAD", "1"))); + manager.reload(); + + // waitDurationInOpenState 60초로 변경 + TypedCircuitBreakerVO updated = TypedCircuitBreakerVO.builder() + .id("uuid-RLD-001").name("API_RELOAD").type("COUNT_BASED") + .failureRateThreshold(50).slidingWindowSize(4).minimumNumberOfCalls(4) + .waitDurationInOpenState(60) + .permittedNumberOfCallsInHalfOpenState(2) + .slowCallRateThreshold(100).slowCallDurationThreshold(5) + .useYn("1").build(); + when(mockDao.get("uuid-RLD-001")).thenReturn(updated); + manager.reload("uuid-RLD-001"); + + CircuitBreaker cb = manager.getCircuitBreaker("API_RELOAD"); + assertEquals(60, + cb.getCircuitBreakerConfig().getWaitDurationInOpenState().getSeconds(), + "reload 후 waitDurationInOpenState 가 60초로 갱신되어야 한다"); + } + + @Test + @DisplayName("4-2. reload(key) useYn=0 → 맵에서 제거 → default fallback") + void testReload_key_disabledVo_removesFromMap() throws Exception { + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-RLD-002", "API_RM", "1"))); + manager.reload(); + assertEquals("API_RM", manager.getCircuitBreaker("API_RM").getName()); + + // useYn=0 으로 비활성화 + TypedCircuitBreakerVO disabled = TypedCircuitBreakerVO.builder() + .id("uuid-RLD-002").name("API_RM").type("COUNT_BASED") + .failureRateThreshold(50).slidingWindowSize(4).minimumNumberOfCalls(4) + .waitDurationInOpenState(1).permittedNumberOfCallsInHalfOpenState(2) + .slowCallRateThreshold(100).slowCallDurationThreshold(5) + .useYn("0").build(); + when(mockDao.get("uuid-RLD-002")).thenReturn(disabled); + manager.reload("uuid-RLD-002"); + + CircuitBreaker cb = manager.getCircuitBreaker("API_RM"); + assertEquals("default", cb.getName(), + "비활성화된 CB 는 맵에서 제거 후 default 로 fallback 되어야 한다"); + } + + @Test + @DisplayName("4-3. reload() 전체 → CircuitBreakerRegistry 재생성으로 이전 CB 상태 초기화") + void testReload_all_refreshesAllCircuitBreakers() throws Exception { + // 초기: API_FULL_A 등록 후 OPEN 상태로 만들기 + when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-FULL-A", "API_FULL_A", "1"))); + manager.reload(); + + CircuitBreaker cbA = manager.getCircuitBreaker("API_FULL_A"); + recordFailures(cbA, 4); + assertEquals(CircuitBreaker.State.OPEN, cbA.getState(), "전체 reload 전 API_FULL_A 는 OPEN 이어야 한다"); + + // 전체 reload → CircuitBreakerRegistry 재생성 + // init() 은 apiIdCBConfigMap 을 초기화하지 않으므로 API_FULL_A 는 맵에 남아있다. + // 하지만 새 레지스트리를 생성하므로 CB 상태(OPEN)는 초기화(CLOSED)된다. + when(mockDao.getList()).thenReturn(Arrays.asList( + buildVo("uuid-FULL-A", "API_FULL_A", "1"), + buildVo("uuid-FULL-B", "API_FULL_B", "1"))); + manager.reload(); + + // 레지스트리 재생성으로 API_FULL_A CB 상태가 CLOSED 로 초기화되어야 한다 + CircuitBreaker cbAAfter = manager.getCircuitBreaker("API_FULL_A"); + assertEquals(CircuitBreaker.State.CLOSED, cbAAfter.getState(), + "전체 reload 후 CircuitBreakerRegistry 재생성으로 이전 CB 상태가 CLOSED 로 초기화되어야 한다"); + + // 새 apiId 도 정상 등록 + assertEquals("API_FULL_B", manager.getCircuitBreaker("API_FULL_B").getName(), + "전체 reload 후 새 apiId 는 정상 등록되어야 한다"); + } + + // ========================================================================= + // 5. 생명주기 + // ========================================================================= + + @Test + @DisplayName("5-1. start() 후 isStarted() = true") + void testIsStarted_afterStart_returnsTrue() { + assertTrue(manager.isStarted(), "start() 후 isStarted() 는 true 여야 한다"); + } +} From f36f78d63c3b2e2671b2f53e3cfa5a7b55fc1c94 Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 29 Apr 2026 14:47:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20TypedCircuitBreakerManager=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - urlCBConfigMap 선언 주석 처리 (URL 기반 CB 기능 미사용) - AlarmStateManager 연동 코드 주석 처리 (AlarmEvent/AlarmCondition/CoreAlarmKey import 제거) Co-Authored-By: Claude Sonnet 4.6 --- .../type/TypedCircuitBreakerManager.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManager.java b/src/main/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManager.java index f2c36bf..00ea5c0 100644 --- a/src/main/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManager.java +++ b/src/main/java/com/eactive/eai/common/circuitBreaker/type/TypedCircuitBreakerManager.java @@ -17,10 +17,6 @@ import com.eactive.eai.common.lifecycle.LifecycleSupport; import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.common.util.Logger; -import com.eactive.eai.custom.alarm.AlarmStateManager; -import com.eactive.eai.custom.alarm.condition.AlarmCondition; -import com.eactive.eai.custom.alarm.event.AlarmEvent; -import com.eactive.eai.custom.alarm.key.CoreAlarmKey; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -45,7 +41,7 @@ public class TypedCircuitBreakerManager implements Lifecycle { TypedCircuitBreakerVO defaultConfigVo; HashMap idCBConfigMap = new HashMap<>(); HashMap apiIdCBConfigMap = new HashMap<>(); - HashMap urlCBConfigMap = new HashMap<>(); +// HashMap urlCBConfigMap = new HashMap<>(); private boolean started; @Autowired @@ -359,14 +355,15 @@ public class TypedCircuitBreakerManager implements Lifecycle { // } else { // this.sendAlert(stateTransitionEvent); // } - AlarmEvent alarmEvent = AlarmEvent.builder().key(CoreAlarmKey.CircuitBreaker) - .condition(AlarmCondition.builder().build()) - .message(String.format("CircuitBreaker state changed. apiId=%s, %s -> %s", event.getCircuitBreakerName(), fromState, toState)) - .build(); - - AlarmStateManager.getAlarmStateManager().onEvnet(alarmEvent); + +// AlarmEvent alarmEvent = AlarmEvent.builder().key(CoreAlarmKey.CircuitBreaker) +// .condition(AlarmCondition.builder().build()) +// .message(String.format("CircuitBreaker state changed. apiId=%s, %s -> %s", event.getCircuitBreakerName(), fromState, toState)) +// .build(); +// +// AlarmStateManager.getAlarmStateManager().onEvnet(alarmEvent); - logger.warn("CircuitBreaker state changed. apiId={}, {} -> {}", event.getCircuitBreakerName(), fromState, toState); + TypedCircuitBreakerManager.logger.warn("CircuitBreaker state changed. apiId={}, {} -> {}", event.getCircuitBreakerName(), fromState, toState); } else { TypedCircuitBreakerManager.logger.error(