Skip to content

Commit be05cb9

Browse files
committed
[TEST] Service 계층 단위 테스트 추가
1 parent dc73d41 commit be05cb9

7 files changed

Lines changed: 1689 additions & 0 deletions

File tree

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package com.example.be.concurrency;
2+
3+
import com.example.be.domain.Room;
4+
import com.example.be.repository.RoomRepository;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.context.SpringBootTest;
10+
import org.springframework.test.context.ActiveProfiles;
11+
import org.springframework.transaction.support.TransactionTemplate;
12+
13+
import java.util.concurrent.CountDownLatch;
14+
import java.util.concurrent.ExecutorService;
15+
import java.util.concurrent.Executors;
16+
import java.util.concurrent.atomic.AtomicInteger;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
/**
21+
* 스터디룸 참여자 수 동시성 테스트
22+
*
23+
* 비관적 락(Pessimistic Lock)을 사용하여 동시에 여러 사용자가
24+
* 방에 입장할 때 참여자 수의 정합성이 보장되는지 검증합니다.
25+
*/
26+
@SpringBootTest
27+
@ActiveProfiles("test")
28+
class RoomConcurrencyTest {
29+
30+
@Autowired
31+
private RoomRepository roomRepository;
32+
33+
@Autowired
34+
private TransactionTemplate transactionTemplate;
35+
36+
private Room testRoom;
37+
38+
@BeforeEach
39+
void setUp() {
40+
// 기존 데이터 정리
41+
roomRepository.deleteAll();
42+
43+
// 테스트용 방 생성
44+
testRoom = Room.builder()
45+
.title("동시성테스트방")
46+
.description("동시 입장 테스트")
47+
.maxParticipants(100)
48+
.participantCount(0)
49+
.createDate(System.currentTimeMillis())
50+
.build();
51+
roomRepository.save(testRoom);
52+
}
53+
54+
@Test
55+
@DisplayName("동시에 10명이 입장해도 참여자 수가 정확히 10이 되어야 한다")
56+
void concurrentJoin_WithPessimisticLock_ShouldMaintainCorrectCount() throws InterruptedException {
57+
// Given
58+
int threadCount = 10; // 동시 입장 인원
59+
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
60+
CountDownLatch latch = new CountDownLatch(threadCount);
61+
AtomicInteger successCount = new AtomicInteger(0);
62+
AtomicInteger failCount = new AtomicInteger(0);
63+
64+
// When: 10명이 동시에 입장 시도
65+
for (int i = 0; i < threadCount; i++) {
66+
executorService.submit(() -> {
67+
try {
68+
transactionTemplate.execute(status -> {
69+
// 비관적 락을 사용하여 방 조회
70+
Room room = roomRepository.findByTitleWithLock("동시성테스트방");
71+
if (room != null) {
72+
room.setParticipantCount(room.getParticipantCount() + 1);
73+
roomRepository.save(room);
74+
successCount.incrementAndGet();
75+
}
76+
return null;
77+
});
78+
} catch (Exception e) {
79+
failCount.incrementAndGet();
80+
} finally {
81+
latch.countDown();
82+
}
83+
});
84+
}
85+
86+
// 모든 스레드 완료 대기
87+
latch.await();
88+
executorService.shutdown();
89+
90+
// Then: 참여자 수가 정확히 10이어야 함
91+
Room result = roomRepository.findByTitle("동시성테스트방");
92+
93+
System.out.println("========== 동시성 테스트 결과 ==========");
94+
System.out.println("동시 요청 수: " + threadCount);
95+
System.out.println("성공 횟수: " + successCount.get());
96+
System.out.println("실패 횟수: " + failCount.get());
97+
System.out.println("최종 참여자 수: " + result.getParticipantCount());
98+
System.out.println("=======================================");
99+
100+
assertThat(result.getParticipantCount()).isEqualTo(threadCount);
101+
assertThat(successCount.get()).isEqualTo(threadCount);
102+
}
103+
104+
@Test
105+
@DisplayName("동시에 50명이 입장해도 참여자 수가 정확히 50이 되어야 한다")
106+
void concurrentJoin_50Users_ShouldMaintainCorrectCount() throws InterruptedException {
107+
// Given
108+
int threadCount = 50;
109+
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
110+
CountDownLatch latch = new CountDownLatch(threadCount);
111+
AtomicInteger successCount = new AtomicInteger(0);
112+
113+
// When
114+
for (int i = 0; i < threadCount; i++) {
115+
executorService.submit(() -> {
116+
try {
117+
transactionTemplate.execute(status -> {
118+
Room room = roomRepository.findByTitleWithLock("동시성테스트방");
119+
if (room != null) {
120+
room.setParticipantCount(room.getParticipantCount() + 1);
121+
roomRepository.save(room);
122+
successCount.incrementAndGet();
123+
}
124+
return null;
125+
});
126+
} catch (Exception e) {
127+
System.err.println("오류 발생: " + e.getMessage());
128+
} finally {
129+
latch.countDown();
130+
}
131+
});
132+
}
133+
134+
latch.await();
135+
executorService.shutdown();
136+
137+
// Then
138+
Room result = roomRepository.findByTitle("동시성테스트방");
139+
140+
System.out.println("========== 50명 동시 입장 테스트 ==========");
141+
System.out.println("최종 참여자 수: " + result.getParticipantCount());
142+
System.out.println("==========================================");
143+
144+
assertThat(result.getParticipantCount()).isEqualTo(threadCount);
145+
}
146+
147+
@Test
148+
@DisplayName("동시 입장/퇴장 시에도 참여자 수가 정확해야 한다")
149+
void concurrentJoinAndLeave_ShouldMaintainCorrectCount() throws InterruptedException {
150+
// Given: 먼저 10명이 입장한 상태로 설정
151+
transactionTemplate.execute(status -> {
152+
Room room = roomRepository.findByTitle("동시성테스트방");
153+
room.setParticipantCount(10);
154+
roomRepository.save(room);
155+
return null;
156+
});
157+
158+
int joinCount = 5; // 입장할 인원
159+
int leaveCount = 3; // 퇴장할 인원
160+
int totalThreads = joinCount + leaveCount;
161+
162+
ExecutorService executorService = Executors.newFixedThreadPool(totalThreads);
163+
CountDownLatch latch = new CountDownLatch(totalThreads);
164+
165+
// When: 5명 입장 + 3명 퇴장 동시 실행
166+
// 입장 스레드
167+
for (int i = 0; i < joinCount; i++) {
168+
executorService.submit(() -> {
169+
try {
170+
transactionTemplate.execute(status -> {
171+
Room room = roomRepository.findByTitleWithLock("동시성테스트방");
172+
if (room != null) {
173+
room.setParticipantCount(room.getParticipantCount() + 1);
174+
roomRepository.save(room);
175+
}
176+
return null;
177+
});
178+
} finally {
179+
latch.countDown();
180+
}
181+
});
182+
}
183+
184+
// 퇴장 스레드
185+
for (int i = 0; i < leaveCount; i++) {
186+
executorService.submit(() -> {
187+
try {
188+
transactionTemplate.execute(status -> {
189+
Room room = roomRepository.findByTitleWithLock("동시성테스트방");
190+
if (room != null && room.getParticipantCount() > 0) {
191+
room.setParticipantCount(room.getParticipantCount() - 1);
192+
roomRepository.save(room);
193+
}
194+
return null;
195+
});
196+
} finally {
197+
latch.countDown();
198+
}
199+
});
200+
}
201+
202+
latch.await();
203+
executorService.shutdown();
204+
205+
// Then: 10 + 5 - 3 = 12명이어야 함
206+
Room result = roomRepository.findByTitle("동시성테스트방");
207+
int expectedCount = 10 + joinCount - leaveCount;
208+
209+
System.out.println("========== 동시 입장/퇴장 테스트 ==========");
210+
System.out.println("초기 인원: 10");
211+
System.out.println("입장 인원: " + joinCount);
212+
System.out.println("퇴장 인원: " + leaveCount);
213+
System.out.println("예상 인원: " + expectedCount);
214+
System.out.println("실제 인원: " + result.getParticipantCount());
215+
System.out.println("==========================================");
216+
217+
assertThat(result.getParticipantCount()).isEqualTo(expectedCount);
218+
}
219+
220+
@Test
221+
@DisplayName("최대 인원 초과 시 입장이 제한되어야 한다")
222+
void concurrentJoin_ShouldNotExceedMaxParticipants() throws InterruptedException {
223+
// Given: 최대 인원 10명인 방
224+
transactionTemplate.execute(status -> {
225+
Room room = roomRepository.findByTitle("동시성테스트방");
226+
room.setMaxParticipants(10);
227+
room.setParticipantCount(8); // 이미 8명 있음
228+
roomRepository.save(room);
229+
return null;
230+
});
231+
232+
int threadCount = 5; // 5명이 동시에 입장 시도 (2명만 성공해야 함)
233+
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
234+
CountDownLatch latch = new CountDownLatch(threadCount);
235+
AtomicInteger successCount = new AtomicInteger(0);
236+
AtomicInteger rejectedCount = new AtomicInteger(0);
237+
238+
// When
239+
for (int i = 0; i < threadCount; i++) {
240+
executorService.submit(() -> {
241+
try {
242+
transactionTemplate.execute(status -> {
243+
Room room = roomRepository.findByTitleWithLock("동시성테스트방");
244+
if (room != null) {
245+
if (room.getParticipantCount() < room.getMaxParticipants()) {
246+
room.setParticipantCount(room.getParticipantCount() + 1);
247+
roomRepository.save(room);
248+
successCount.incrementAndGet();
249+
} else {
250+
rejectedCount.incrementAndGet();
251+
}
252+
}
253+
return null;
254+
});
255+
} finally {
256+
latch.countDown();
257+
}
258+
});
259+
}
260+
261+
latch.await();
262+
executorService.shutdown();
263+
264+
// Then
265+
Room result = roomRepository.findByTitle("동시성테스트방");
266+
267+
System.out.println("========== 최대 인원 제한 테스트 ==========");
268+
System.out.println("최대 인원: 10");
269+
System.out.println("초기 인원: 8");
270+
System.out.println("입장 시도: " + threadCount);
271+
System.out.println("입장 성공: " + successCount.get());
272+
System.out.println("입장 거부: " + rejectedCount.get());
273+
System.out.println("최종 인원: " + result.getParticipantCount());
274+
System.out.println("==========================================");
275+
276+
// 최대 인원(10)을 초과하지 않아야 함
277+
assertThat(result.getParticipantCount()).isLessThanOrEqualTo(10);
278+
// 정확히 10명이어야 함 (8 + 2)
279+
assertThat(result.getParticipantCount()).isEqualTo(10);
280+
// 2명만 성공, 3명은 거부되어야 함
281+
assertThat(successCount.get()).isEqualTo(2);
282+
assertThat(rejectedCount.get()).isEqualTo(3);
283+
}
284+
}

0 commit comments

Comments
 (0)