From 11b439aaf121e813390688ce417e88de4acf546e Mon Sep 17 00:00:00 2001 From: curry772 Date: Fri, 11 Sep 2026 15:05:18 +0900 Subject: [PATCH] =?UTF-8?q?=ED=81=B4=EB=9F=AC=EC=8A=A4=ED=84=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=86=A0=ED=81=B0=EC=9D=B4=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=EB=90=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=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 - near 캐시 사본 때문에 다른 노드가 방금 교체한 토큰을 못 보고 같은 주기에 두 번 발급되던 문제 (분산락을 잡은 뒤의 조회에서도 옛 값을 읽어 락만으로는 막지 못했음) - 캐시 조회를 짧은 비관적 트랜잭션으로 감싸 primary 값을 읽도록 함 발급 HTTP 호출은 트랜잭션 밖에 두어 장기 트랜잭션이 파티션 맵 교환을 막지 않게 함 - 재발급 판정에 validUntilTime 추가 : 요구 시각까지 유효한 토큰이 있으면 발급하지 않음 거래 중 재발급은 0 을 넘겨 만료 기준 검사를 건너뛴다 (만료 전이어도 기관이 거부한 상황) - 2노드 시험에서 주기당 발급 1회, 만료 창 0건 확인 - 단위테스트 3건 추가 --- .../authoutbound/AccessTokenManagerByDB.java | 9 +- .../eai/common/session/SessionManager.java | 10 ++- .../session/SessionManagerForEhcache.java | 2 +- .../session/SessionManagerForIgnite.java | 61 +++++++++++-- .../AccessTokenManagerByDBRetryTest.java | 8 +- .../AccessTokenManagerByDBScheduleTest.java | 10 +-- .../SessionManagerForIgniteReissueTest.java | 87 +++++++++++++++++-- 7 files changed, 157 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDB.java b/src/main/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDB.java index e433226..02d42f3 100644 --- a/src/main/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDB.java +++ b/src/main/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDB.java @@ -267,11 +267,12 @@ public class AccessTokenManagerByDB implements Lifecycle { } else if (cached.getExpiration().before(new Date(intervalTime))) { // 다음 스케줄 전에 만료되므로 미리 갱신한다. - // 여러 노드가 동시에 들어와도 분산락 안에서 oldToken 을 비교해 한 번만 발급된다. + // 여러 노드가 동시에 들어와도 분산락 안에서 다시 확인해 한 번만 발급된다. + // 다른 노드가 이미 넣어둔 토큰이 다음 틱까지 유효하면 발급하지 않는다. logger.debug("Token expires before next schedule : {}, expiration date: {}", adapterGroupName, cached.getExpiration()); sessionManager.reissueOutboundAccessToken(adapterGroupName, cached.getAccessToken(), - token -> issueToken(credential)); + intervalTime, token -> issueToken(credential)); } else { logger.debug( @@ -625,7 +626,9 @@ public class AccessTokenManagerByDB implements Lifecycle { // 클러스터 전역에서 한 번만 발급되도록 분산락 안에서 처리한다. 발급 결과는 캐시에 반영되므로 // 뒤따르는 거래는 재발급하지 않는다. 구현체는 호출자가 넘긴 어댑터 속성 기준으로 고른다. - return SessionManager.getInstance().reissueOutboundAccessToken(adapterGroupName, oldToken, + // 거부된 토큰을 바꾸는 것이 목적이므로 만료 기준 검사는 하지 않고 oldToken 비교만 한다. + // (만료 전이어도 상대 기관이 거부한 상황이다) + return SessionManager.getInstance().reissueOutboundAccessToken(adapterGroupName, oldToken, 0L, new Function() { @Override diff --git a/src/main/java/com/eactive/eai/common/session/SessionManager.java b/src/main/java/com/eactive/eai/common/session/SessionManager.java index ebe1fb7..e45c037 100644 --- a/src/main/java/com/eactive/eai/common/session/SessionManager.java +++ b/src/main/java/com/eactive/eai/common/session/SessionManager.java @@ -228,12 +228,14 @@ public abstract class SessionManager implements Lifecycle { * 분산락 안에서 캐시의 토큰이 oldToken 과 같은지 확인한 뒤에만 발급하므로, 여러 노드가 동시에 * 재발급을 시도해도 실제 발급은 한 번만 일어난다. 발급에 성공하면 캐시에 반영한다. * - * @param key 어댑터그룹명 - * @param oldToken 거부된 accessToken 값 - * @param tokenSupplier 실제 발급 처리 + * @param key 어댑터그룹명 + * @param oldToken 거부된(또는 갱신 대상인) accessToken 값 + * @param validUntilTime 이 시각까지 유효한 토큰이 캐시에 있으면 발급하지 않는다. + * 0 이하면 만료 기준 검사를 하지 않고 oldToken 비교만 한다. + * @param tokenSupplier 실제 발급 처리 * @return 재발급된 토큰. 다른 노드가 이미 갱신했다면 그 토큰. */ - public abstract AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, + public abstract AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, long validUntilTime, Function tokenSupplier); public abstract void removeOutboundAccessToken(String key); diff --git a/src/main/java/com/eactive/eai/common/session/SessionManagerForEhcache.java b/src/main/java/com/eactive/eai/common/session/SessionManagerForEhcache.java index effb325..e34cc0e 100644 --- a/src/main/java/com/eactive/eai/common/session/SessionManagerForEhcache.java +++ b/src/main/java/com/eactive/eai/common/session/SessionManagerForEhcache.java @@ -957,7 +957,7 @@ public class SessionManagerForEhcache extends SessionManager { } @Override - public AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, + public AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, long validUntilTime, Function tokenSupplier) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/com/eactive/eai/common/session/SessionManagerForIgnite.java b/src/main/java/com/eactive/eai/common/session/SessionManagerForIgnite.java index 21948ed..c0cac5f 100644 --- a/src/main/java/com/eactive/eai/common/session/SessionManagerForIgnite.java +++ b/src/main/java/com/eactive/eai/common/session/SessionManagerForIgnite.java @@ -1,6 +1,7 @@ package com.eactive.eai.common.session; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Properties; import java.util.concurrent.TimeUnit; @@ -44,6 +45,9 @@ import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; import org.apache.ignite.spi.discovery.tcp.ipfinder.kubernetes.TcpDiscoveryKubernetesIpFinder; import org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder; import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; +import org.apache.ignite.transactions.Transaction; +import org.apache.ignite.transactions.TransactionConcurrency; +import org.apache.ignite.transactions.TransactionIsolation; import com.eactive.eai.adapter.socket2.common.Env; import com.eactive.eai.authserver.service.BearerTokenInfo; @@ -837,19 +841,64 @@ public class SessionManagerForIgnite extends SessionManager { return cacheOutBoundAccessToken.get(key); } + /** + * 아웃바운드 토큰을 primary 노드 기준으로 읽는다. + * + * 일반 get() 은 near 캐시 사본을 돌려줄 수 있어, 다른 노드가 방금 교체한 토큰을 놓칠 수 있다. + * 비관적 트랜잭션 안에서 읽으면 primary 의 값을 보장받는다. 트랜잭션은 읽기 직후 바로 닫는다. + * (발급 HTTP 호출 구간까지 열어두면 장기 트랜잭션이 되어 파티션 맵 교환을 막는다) + * + * @param key 어댑터그룹명 + * @return 캐시에 있는 토큰. 없으면 null. + */ + private AccessTokenVO readFromPrimary(String key) { + try (Transaction tx = manager.transactions().txStart(TransactionConcurrency.PESSIMISTIC, + TransactionIsolation.REPEATABLE_READ)) { + AccessTokenVO token = cacheOutBoundAccessToken.get(key); + tx.commit(); + return token; + } catch (Throwable e) { + logger.error("occuring exception in readFromPrimary. key=" + key, e); + return cacheOutBoundAccessToken.get(key); + } + } + /** 토큰으로 쓸 수 있는 값인지 (빈 토큰은 발급 실패로 본다) */ private boolean isUsableToken(AccessTokenVO token) { return token != null && StringUtils.isNotBlank(token.getAccessToken()); } + /** + * 캐시의 토큰이 이미 갱신된 것이라 다시 발급할 필요가 없는지 판단한다. + * + * 값이 바뀌었는지(oldToken 비교)와, 요구하는 시각까지 유효한지를 함께 본다. + * 값 비교만 하면 서로 다른 시점에 조회한 노드들이 각자 발급할 수 있다. + * + * @param token 캐시에 있는 토큰 + * @param oldToken 갱신 대상으로 보고 들어온 accessToken 값 + * @param validUntilTime 이 시각까지 유효하면 갱신 불필요. 0 이하면 만료 기준 검사 생략. + * @return 갱신이 필요 없으면 true + */ + private boolean isAlreadyReissued(AccessTokenVO token, String oldToken, long validUntilTime) { + if (!isUsableToken(token)) { + return false; + } + if (validUntilTime > 0 && token.getExpiration() != null + && !token.getExpiration().before(new Date(validUntilTime))) { + // 다른 노드가 넣어둔 토큰이 요구 시각까지 유효하다. + return true; + } + return !StringUtils.equals(token.getAccessToken(), oldToken); + } + @Override - public AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, + public AccessTokenVO reissueOutboundAccessToken(String key, String oldToken, long validUntilTime, Function tokenSupplier) { - AccessTokenVO token = cacheOutBoundAccessToken.get(key); + AccessTokenVO token = readFromPrimary(key); // 락을 잡기 전에 먼저 확인한다. 다른 노드가 이미 갱신했으면 그 토큰을 쓴다. - if (isUsableToken(token) && !StringUtils.equals(token.getAccessToken(), oldToken)) { + if (isAlreadyReissued(token, oldToken, validUntilTime)) { return token; } @@ -866,8 +915,10 @@ public class SessionManagerForIgnite extends SessionManager { } // 락 획득 사이에 다른 노드가 갱신했을 수 있으므로 다시 확인한다. - token = cacheOutBoundAccessToken.get(key); - if (isUsableToken(token) && !StringUtils.equals(token.getAccessToken(), oldToken)) { + // near 캐시 사본은 무효화가 늦을 수 있어 그냥 get() 하면 이미 교체된 옛 토큰을 읽는다. + // 짧은 비관적 트랜잭션으로 읽어 primary 의 값을 보장받는다. (발급 구간까지 끌고 가지 않는다) + token = readFromPrimary(key); + if (isAlreadyReissued(token, oldToken, validUntilTime)) { logger.debug("reissueOutboundAccessToken already reissued by another node. key=" + key); return token; } diff --git a/src/test/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDBRetryTest.java b/src/test/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDBRetryTest.java index 0d00d11..15fa87c 100644 --- a/src/test/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDBRetryTest.java +++ b/src/test/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDBRetryTest.java @@ -49,9 +49,9 @@ class AccessTokenManagerByDBRetryTest { injectSessionManager(mockSessionManager); // 분산락 구간을 흉내 낸다. supplier 를 그대로 실행해 결과를 돌려준다. - Mockito.when(mockSessionManager.reissueOutboundAccessToken(anyString(), any(), any())) + Mockito.when(mockSessionManager.reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any())) .thenAnswer(invocation -> { - Function supplier = invocation.getArgument(2); + Function supplier = invocation.getArgument(3); return supplier.apply(null); }); } @@ -95,7 +95,7 @@ class AccessTokenManagerByDBRetryTest { AccessTokenVO result = manager.retryAccessTokenVO(GROUP, adapterProp(SERVICE_CLASS), "OLD"); assertEquals("NEW", result.getAccessToken()); - Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD"), any()); + Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD"), Mockito.anyLong(), any()); Mockito.verify(mockSessionManager, Mockito.never()).getOutboundAccessToken(anyString(), any()); } @@ -136,7 +136,7 @@ class AccessTokenManagerByDBRetryTest { AccessTokenVO result = manager.retryAccessTokenVO(GROUP, adapterProp("no.such.TokenService"), "OLD"); assertEquals("CACHED", result.getAccessToken()); - Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), any()); + Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any()); } @Test diff --git a/src/test/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDBScheduleTest.java b/src/test/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDBScheduleTest.java index 2a43cfe..6cde62c 100644 --- a/src/test/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDBScheduleTest.java +++ b/src/test/java/com/eactive/eai/common/authoutbound/AccessTokenManagerByDBScheduleTest.java @@ -87,7 +87,7 @@ class AccessTokenManagerByDBScheduleTest { runTask(); Mockito.verify(mockSessionManager).getOutboundAccessToken(eq(GROUP), any()); - Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), any()); + Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any()); } @Test @@ -98,7 +98,7 @@ class AccessTokenManagerByDBScheduleTest { runTask(); - Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD-TOKEN"), any()); + Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD-TOKEN"), Mockito.anyLong(), any()); Mockito.verify(mockSessionManager, Mockito.never()).getOutboundAccessToken(anyString(), any()); } @@ -109,7 +109,7 @@ class AccessTokenManagerByDBScheduleTest { runTask(); - Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD-TOKEN"), any()); + Mockito.verify(mockSessionManager).reissueOutboundAccessToken(eq(GROUP), eq("OLD-TOKEN"), Mockito.anyLong(), any()); } @Test @@ -119,7 +119,7 @@ class AccessTokenManagerByDBScheduleTest { runTask(); - Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), any()); + Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any()); Mockito.verify(mockSessionManager, Mockito.never()).getOutboundAccessToken(anyString(), any()); } @@ -130,7 +130,7 @@ class AccessTokenManagerByDBScheduleTest { runTask(); - Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), any()); + Mockito.verify(mockSessionManager, Mockito.never()).reissueOutboundAccessToken(anyString(), any(), Mockito.anyLong(), any()); Mockito.verify(mockSessionManager, Mockito.never()).getOutboundAccessToken(anyString(), any()); } } diff --git a/src/test/java/com/eactive/eai/common/session/SessionManagerForIgniteReissueTest.java b/src/test/java/com/eactive/eai/common/session/SessionManagerForIgniteReissueTest.java index 447abe2..07409de 100644 --- a/src/test/java/com/eactive/eai/common/session/SessionManagerForIgniteReissueTest.java +++ b/src/test/java/com/eactive/eai/common/session/SessionManagerForIgniteReissueTest.java @@ -20,7 +20,12 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; +import org.apache.ignite.Ignite; import org.apache.ignite.IgniteCache; +import org.apache.ignite.IgniteTransactions; +import org.apache.ignite.transactions.Transaction; +import org.apache.ignite.transactions.TransactionConcurrency; +import org.apache.ignite.transactions.TransactionIsolation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -50,6 +55,9 @@ class SessionManagerForIgniteReissueTest { /** 어댑터그룹별 락 (같은 키면 같은 인스턴스를 돌려줘야 경합 테스트가 성립한다) */ private Map locks; + /** readFromPrimary 가 여는 트랜잭션 */ + private Transaction transaction; + @BeforeEach void setUp() throws Exception { sessionManager = new SessionManagerForIgnite(); @@ -69,6 +77,25 @@ class SessionManagerForIgniteReissueTest { key -> new ReentrantLock())); injectCache(cache); + injectIgnite(); + } + + /** + * readFromPrimary 가 쓰는 Ignite 트랜잭션을 mock 으로 주입한다. + * 주입하지 않으면 NPE 로 폴백 경로를 타게 되어 실제 동작을 검증하지 못한다. + */ + private void injectIgnite() throws Exception { + transaction = Mockito.mock(Transaction.class); + IgniteTransactions transactions = Mockito.mock(IgniteTransactions.class); + Mockito.when(transactions.txStart(any(TransactionConcurrency.class), any(TransactionIsolation.class))) + .thenReturn(transaction); + + Ignite ignite = Mockito.mock(Ignite.class); + Mockito.when(ignite.transactions()).thenReturn(transactions); + + Field field = SessionManagerForIgnite.class.getDeclaredField("manager"); + field.setAccessible(true); + field.set(null, ignite); } /** private static cacheOutBoundAccessToken 에 mock 을 주입한다. */ @@ -99,7 +126,7 @@ class SessionManagerForIgniteReissueTest { void 캐시비었을때_발급() { AtomicInteger issued = new AtomicInteger(); - AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, "NEW")); + AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, "NEW")); assertEquals(1, issued.get()); assertEquals("NEW", result.getAccessToken()); @@ -112,7 +139,7 @@ class SessionManagerForIgniteReissueTest { store.put(GROUP, token("OLD")); AtomicInteger issued = new AtomicInteger(); - AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, "NEW")); + AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, "NEW")); assertEquals(1, issued.get()); assertEquals("NEW", result.getAccessToken()); @@ -125,7 +152,7 @@ class SessionManagerForIgniteReissueTest { store.put(GROUP, token("ALREADY-NEW")); AtomicInteger issued = new AtomicInteger(); - AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, "NEW")); + AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, "NEW")); assertEquals(0, issued.get(), "다른 토큰이 이미 캐시에 있으면 발급하지 않아야 한다"); assertEquals("ALREADY-NEW", result.getAccessToken()); @@ -137,7 +164,7 @@ class SessionManagerForIgniteReissueTest { store.put(GROUP, token("OLD")); AtomicInteger issued = new AtomicInteger(); - AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, null)); + AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, null)); assertEquals(1, issued.get()); assertNull(result); @@ -154,13 +181,57 @@ class SessionManagerForIgniteReissueTest { locks.put(GROUP, neverAcquired); AtomicInteger issued = new AtomicInteger(); - AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", supplier(issued, "NEW")); + AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(issued, "NEW")); assertEquals(0, issued.get()); assertEquals("OLD", result.getAccessToken()); Mockito.verify(neverAcquired, Mockito.never()).unlock(); } + @Test + @DisplayName("5-1. 캐시 토큰이 요구 시각까지 유효하면 값이 같아도 발급하지 않는다") + void 충분히유효하면_발급안함() { + // 다른 노드가 방금 넣어둔 토큰. 조회 시점 차이로 oldToken 과 값이 같게 들어와도 + // 다음 틱까지 유효하면 다시 발급할 이유가 없다. + store.put(GROUP, token("SAME-TOKEN")); + AtomicInteger issued = new AtomicInteger(); + + long validUntil = System.currentTimeMillis() + 60_000L; + AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "SAME-TOKEN", validUntil, + supplier(issued, "NEW")); + + assertEquals(0, issued.get(), "요구 시각까지 유효하면 발급하지 않아야 한다"); + assertEquals("SAME-TOKEN", result.getAccessToken()); + } + + @Test + @DisplayName("5-2. 요구 시각 전에 만료되면 발급한다") + void 요구시각전_만료면_발급() { + store.put(GROUP, token("SAME-TOKEN")); + AtomicInteger issued = new AtomicInteger(); + + // 캐시 토큰은 10분 뒤 만료인데 20분 뒤까지 유효해야 한다면 갱신 대상이다. + long validUntil = System.currentTimeMillis() + 1_200_000L; + AccessTokenVO result = sessionManager.reissueOutboundAccessToken(GROUP, "SAME-TOKEN", validUntil, + supplier(issued, "NEW")); + + assertEquals(1, issued.get()); + assertEquals("NEW", result.getAccessToken()); + } + + @Test + @DisplayName("5-3. 캐시 조회는 비관적 트랜잭션 안에서 하고 바로 커밋한다") + void 트랜잭션으로_읽는다() { + store.put(GROUP, token("OLD")); + + sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(new AtomicInteger(), "NEW")); + + // 락 전 1회 + 락 안 1회 + Mockito.verify(transaction, Mockito.times(2)).commit(); + // 발급(HTTP) 구간까지 트랜잭션을 열어두면 장기 트랜잭션이 되어 파티션 맵 교환을 막는다. + Mockito.verify(transaction, Mockito.never()).rollback(); + } + @Test @DisplayName("6. 경합 - 여러 스레드가 같은 토큰으로 동시에 재발급해도 발급은 한 번만 일어난다") void 동시재발급_한번만() throws Exception { @@ -190,7 +261,7 @@ class SessionManagerForIgniteReissueTest { executor.submit(() -> { try { start.await(); - results[index] = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", slowSupplier); + results[index] = sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, slowSupplier); } catch (Exception e) { // 결과가 null 로 남아 아래 검증에서 걸린다. } finally { @@ -233,8 +304,8 @@ class SessionManagerForIgniteReissueTest { ExecutorService executor = Executors.newFixedThreadPool(2); try { - executor.submit(() -> sessionManager.reissueOutboundAccessToken("GRP-A", "OLD", blockingSupplier)); - executor.submit(() -> sessionManager.reissueOutboundAccessToken("GRP-B", "OLD", blockingSupplier)); + executor.submit(() -> sessionManager.reissueOutboundAccessToken("GRP-A", "OLD", 0L, blockingSupplier)); + executor.submit(() -> sessionManager.reissueOutboundAccessToken("GRP-B", "OLD", 0L, blockingSupplier)); assertTrue(bothInside.await(10, TimeUnit.SECONDS), "두 그룹이 동시에 발급 구간에 들어가야 한다"); } finally {