Skip to content

Commit 40e0b3a

Browse files
committed
✅ test: 분산 락 통합 테스트 추가
- 멀티스레드에서 하나의 스레드만 락 획득 성공하는지 확인 - 단일 스레드 락 획득 정상 동작 테스트 - 서로 다른 키로 락 사용 시 동시 실행 가능 여부 검증 - gameStart와 handlePlayerReady 메서드 간 동일 roomId 락 충돌 테스트 - 테스트용 서비스 클래스 추가
1 parent e764b1a commit 40e0b3a

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
package io.f1.backend.global.lock;
2+
3+
import static org.junit.jupiter.api.Assertions.assertAll;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import io.f1.backend.global.config.RedissonTestContainerConfig;
7+
import io.f1.backend.global.exception.CustomException;
8+
import java.util.concurrent.CountDownLatch;
9+
import java.util.concurrent.ExecutorService;
10+
import java.util.concurrent.Executors;
11+
import java.util.concurrent.atomic.AtomicInteger;
12+
import java.util.concurrent.atomic.AtomicReference;
13+
import org.junit.jupiter.api.DisplayName;
14+
import org.junit.jupiter.api.Test;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.boot.test.context.SpringBootTest;
17+
import org.springframework.context.annotation.Import;
18+
import org.springframework.stereotype.Service;
19+
import org.springframework.test.context.DynamicPropertyRegistry;
20+
import org.springframework.test.context.DynamicPropertySource;
21+
22+
@SpringBootTest
23+
@Import({RedissonTestContainerConfig.class, DistributedLockIntegrationTest.TestLockService.class})
24+
class DistributedLockIntegrationTest {
25+
26+
@DynamicPropertySource
27+
static void redisProperties(DynamicPropertyRegistry registry) {
28+
registry.add("spring.datasource.data.redis.host", RedissonTestContainerConfig.redisContainer::getHost);
29+
registry.add("spring.datasource.data.redis.port", () -> RedissonTestContainerConfig.redisContainer.getFirstMappedPort());
30+
}
31+
32+
@Autowired
33+
private TestLockService testLockService;
34+
35+
private final Long ROOM_ID = 1L;
36+
37+
@DisplayName("멀티스레드 환경에서 하나의 스레드만 락 획득에 성공하고, 나머지는 모두 실패하는지 검증")
38+
@Test
39+
void testDistributedLock_WhenMultipleThreads_OnlyOneSuccess() throws Exception {
40+
// Given: 5개의 쓰레드로 구성된 고정된 쓰레드 풀과 동기화를 위한 CountDownLatch 준비
41+
int threadCount = 5;
42+
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
43+
CountDownLatch latch = new CountDownLatch(threadCount);
44+
45+
AtomicInteger successCount = new AtomicInteger(0);
46+
AtomicInteger failCount = new AtomicInteger(0);
47+
48+
// When: 여러 스레드가 동시에 락 획득 시도
49+
for (int i = 0; i < threadCount; i++) {
50+
executorService.submit(() -> {
51+
try {
52+
testLockService.executeWithLock(ROOM_ID);
53+
successCount.incrementAndGet();
54+
} catch (IllegalStateException e) {
55+
// 락 획득 실패로 인한 예외는 예상된 동작
56+
failCount.incrementAndGet();
57+
} catch (Exception e) {
58+
// 기타 예외는 실패로 간주
59+
failCount.incrementAndGet();
60+
e.printStackTrace();
61+
} finally {
62+
latch.countDown();
63+
}
64+
});
65+
}
66+
67+
latch.await();
68+
executorService.shutdown();
69+
70+
// Then: 하나의 스레드만 락 획득에 성공하고, 나머지는 모두 실패하는지 검증
71+
assertAll(
72+
() -> assertEquals(1, successCount.get(), "락 획득에 성공한 스레드는 1개여야 합니다"),
73+
() -> assertEquals(threadCount - 1, failCount.get(), "락 획득에 실패한 스레드는 " + (threadCount - 1) + "개여야 합니다")
74+
);
75+
}
76+
77+
@DisplayName("단일 스레드에서 락 획득이 정상적으로 동작하는지 검증")
78+
@Test
79+
void testDistributedLock_SingleThread_Success() {
80+
// Given & When & Then
81+
String result = testLockService.executeWithLock(ROOM_ID);
82+
assertEquals("락 획득 및 실행 성공 : " + ROOM_ID, result);
83+
}
84+
85+
@DisplayName("다른 키로 락을 사용할 때 동시 실행이 가능한지 검증")
86+
@Test
87+
void testDistributedLock_DifferentKeys_BothSuccess() throws Exception {
88+
// Given
89+
int threadCount = 2;
90+
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
91+
CountDownLatch latch = new CountDownLatch(threadCount);
92+
93+
AtomicInteger successCount = new AtomicInteger(0);
94+
AtomicInteger failCount = new AtomicInteger(0);
95+
96+
// When: 서로 다른 키로 락 획득 시도
97+
executorService.submit(() -> {
98+
try {
99+
testLockService.executeWithLock(1L);
100+
successCount.incrementAndGet();
101+
} catch (CustomException e) {
102+
failCount.incrementAndGet();
103+
e.getMessage();
104+
} finally {
105+
latch.countDown();
106+
}
107+
});
108+
109+
executorService.submit(() -> {
110+
try {
111+
testLockService.executeWithLock(2L);
112+
successCount.incrementAndGet();
113+
} catch (Exception e) {
114+
failCount.incrementAndGet();
115+
e.getMessage();
116+
} finally {
117+
latch.countDown();
118+
}
119+
});
120+
121+
latch.await();
122+
executorService.shutdown();
123+
124+
// Then: 서로 다른 키이므로 둘 다 성공해야 함
125+
assertAll(
126+
() -> assertEquals(2, successCount.get(), "서로 다른 키로 락을 사용하면 둘 다 성공해야 한다"),
127+
() -> assertEquals(0, failCount.get(), "락 획득을 실패한 스레드는 없어야 한다")
128+
);
129+
}
130+
131+
@DisplayName("gameStart 중에는 handlePlayerReady가 동일한 roomId로 락을 획득할 수 없어야 한다")
132+
@Test
133+
void testHandlePlayerReadyFailsWhenGameStartHoldsLock() throws Exception {
134+
// Given
135+
ExecutorService executorService = Executors.newFixedThreadPool(2);
136+
CountDownLatch latch = new CountDownLatch(2);
137+
CountDownLatch gameStartLocked = new CountDownLatch(1);
138+
139+
AtomicInteger successCount = new AtomicInteger(0);
140+
AtomicInteger failCount = new AtomicInteger(0);
141+
AtomicReference<String> successMethod = new AtomicReference<>();
142+
143+
// Thread A: gameStart 락 선점
144+
executorService.submit(() -> {
145+
try {
146+
testLockService.gameStartSimulate(ROOM_ID, gameStartLocked);
147+
successMethod.set("gameStart");
148+
successCount.incrementAndGet();
149+
} catch (Exception e) {
150+
failCount.incrementAndGet();
151+
e.getMessage();
152+
} finally {
153+
latch.countDown();
154+
}
155+
});
156+
157+
// Thread A: gameStart가 락 획득한 후에 handlePlayerReady 시도
158+
executorService.submit(() -> {
159+
try {
160+
gameStartLocked.await();
161+
testLockService.handlePlayerReadySimulate(ROOM_ID, gameStartLocked);
162+
successMethod.set("handlePlayerReady");
163+
successCount.incrementAndGet();
164+
} catch (Exception e) {
165+
failCount.incrementAndGet();
166+
e.getMessage();
167+
} finally {
168+
latch.countDown();
169+
}
170+
});
171+
172+
latch.await();
173+
executorService.shutdown();
174+
175+
// Then
176+
assertAll(
177+
() -> assertEquals(1, successCount.get(), "성공한 스레드는 1개여야 한다."),
178+
() -> assertEquals(1, failCount.get(), "실패한 스레드는 1개여야 한다."),
179+
() -> assertEquals("gameStart", successMethod.get(), "gameStart 락을 획득해야 한다")
180+
);
181+
}
182+
183+
@DisplayName("handlePlayerReady 중에는 gameStart가 동일한 roomId로 락을 획득할 수 없어야 한다")
184+
@Test
185+
void testGameStartFailsWhenHandlePlayerReadyHoldsLock() throws Exception {
186+
// Given
187+
ExecutorService executorService = Executors.newFixedThreadPool(2);
188+
CountDownLatch latch = new CountDownLatch(2);
189+
CountDownLatch handlePlayerReadyLocked = new CountDownLatch(1); // handlePlayerReady가 락 획득 신호용
190+
191+
AtomicInteger successCount = new AtomicInteger(0);
192+
AtomicInteger failCount = new AtomicInteger(0);
193+
AtomicReference<String> successMethod = new AtomicReference<>();
194+
195+
// Thread B: handlePlayerReady가 락 먼저 획득
196+
executorService.submit(() -> {
197+
try {
198+
testLockService.handlePlayerReadySimulate(ROOM_ID, handlePlayerReadyLocked);
199+
successMethod.set("handlePlayerReady");
200+
successCount.incrementAndGet();
201+
} catch (CustomException e) {
202+
failCount.incrementAndGet();
203+
e.getMessage();
204+
} finally {
205+
handlePlayerReadyLocked.countDown(); // 락 획득 알림
206+
latch.countDown();
207+
}
208+
});
209+
210+
// Thread A: handlePlayerReady가 락 획득한 후에 gameStart 시도
211+
executorService.submit(() -> {
212+
try {
213+
handlePlayerReadyLocked.await(); // handlePlayerReady 락 획득 신호 기다림
214+
testLockService.gameStartSimulate(ROOM_ID, handlePlayerReadyLocked);
215+
successMethod.set("gameStart");
216+
successCount.incrementAndGet();
217+
} catch (CustomException e) {
218+
failCount.incrementAndGet();
219+
e.getMessage();
220+
} catch (Exception e) {
221+
failCount.incrementAndGet();
222+
223+
} finally {
224+
latch.countDown();
225+
}
226+
});
227+
228+
latch.await();
229+
executorService.shutdown();
230+
231+
// Then
232+
assertAll(
233+
() -> assertEquals(1, successCount.get(), "성공한 스레드는 1개여야 한다."),
234+
() -> assertEquals(1, failCount.get(), "실패한 스레드는 1개여야 한다."),
235+
() -> assertEquals("handlePlayerReady", successMethod.get(), "handlePlayerReady만 락을 획득해야 한다")
236+
);
237+
}
238+
239+
/**
240+
* 테스트용 서비스 클래스
241+
*/
242+
@Service
243+
static class TestLockService {
244+
245+
@DistributedLock(prefix = "room", key = "#roomId", waitTime = 0)
246+
public String executeWithLock(Long roomId) {
247+
// 락이 획득된 상태에서 실행되는 비즈니스 로직 시뮬레이션
248+
try {
249+
Thread.sleep(100); // 짧은 작업 시간 시뮬레이션
250+
} catch (InterruptedException e) {
251+
Thread.currentThread().interrupt();
252+
throw new RuntimeException("작업 중단", e);
253+
}
254+
return "락 획득 및 실행 성공 : " + roomId;
255+
}
256+
257+
@DistributedLock(prefix = "room", key = "#roomId", waitTime = 0)
258+
public String gameStartSimulate(Long roomId, CountDownLatch lockAcquiredSignal) {
259+
String threadName = Thread.currentThread().getName();
260+
lockAcquiredSignal.countDown();
261+
try {
262+
Thread.sleep(300); // 락 점유 시간 시뮬레이션
263+
} catch (InterruptedException e) {
264+
Thread.currentThread().interrupt();
265+
}
266+
return "gameStart 실행 스레드 : " + threadName;
267+
}
268+
269+
@DistributedLock(prefix = "room", key = "#roomId", waitTime = 0)
270+
public String handlePlayerReadySimulate(Long roomId, CountDownLatch lockAcquiredSignal) {
271+
String threadName = Thread.currentThread().getName();
272+
lockAcquiredSignal.countDown();
273+
try {
274+
Thread.sleep(300); // 락 점유 시간 시뮬레이션
275+
} catch (InterruptedException e) {
276+
Thread.currentThread().interrupt();
277+
}
278+
return "handlePlayerReady 실행 스레드 : " + threadName;
279+
}
280+
}
281+
}

0 commit comments

Comments
 (0)