Skip to content

Commit 9157f7c

Browse files
committed
✨ 동시성 추가 및 테스트
1 parent f1b1308 commit 9157f7c

File tree

2 files changed

+254
-45
lines changed

2 files changed

+254
-45
lines changed

backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,15 @@
3434
import java.util.List;
3535
import java.util.Map;
3636
import java.util.Optional;
37+
import java.util.concurrent.ConcurrentHashMap;
3738
import java.util.concurrent.atomic.AtomicLong;
3839
import lombok.RequiredArgsConstructor;
40+
import lombok.extern.slf4j.Slf4j;
41+
import org.hibernate.boot.model.naming.IllegalIdentifierException;
3942
import org.springframework.context.ApplicationEventPublisher;
4043
import org.springframework.stereotype.Service;
4144

45+
@Slf4j
4246
@Service
4347
@RequiredArgsConstructor
4448
public class RoomService {
@@ -47,6 +51,8 @@ public class RoomService {
4751
private final RoomRepository roomRepository;
4852
private final AtomicLong roomIdGenerator = new AtomicLong(0);
4953
private final ApplicationEventPublisher eventPublisher;
54+
private final Map<Long, Object> roomLocks = new ConcurrentHashMap<>();
55+
private static final String PENDING_SESSION_ID = "PENDING_SESSION_ID";
5056

5157
public RoomCreateResponse saveRoom(RoomCreateRequest request) {
5258

@@ -63,6 +69,8 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request) {
6369

6470
Room room = new Room(newId, roomSetting, gameSetting, host);
6571

72+
room.getUserIdSessionMap().put(host.id,PENDING_SESSION_ID);
73+
6674
roomRepository.saveRoom(room);
6775

6876
eventPublisher.publishEvent(new RoomCreatedEvent(room, quiz));
@@ -72,24 +80,31 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request) {
7280

7381
public void enterRoom(RoomValidationRequest request) {
7482

75-
Room room = findRoom(request.roomId());
83+
Long roomId = request.roomId();
7684

77-
if (room.getState().equals(RoomState.PLAYING)) {
78-
throw new IllegalArgumentException("403 게임이 진행중입니다.");
79-
}
85+
Object lock = roomLocks.computeIfAbsent(roomId, k -> new Object());
8086

81-
int maxUserCnt = room.getRoomSetting().maxUserCount();
82-
int currentCnt = room.getUserIdSessionMap().size();
83-
if (maxUserCnt == currentCnt) {
84-
throw new IllegalArgumentException("403 정원이 모두 찼습니다.");
85-
}
87+
synchronized (lock) {
8688

87-
if (room.getRoomSetting().locked()
88-
&& !room.getRoomSetting().password().equals(request.password())) {
89-
throw new IllegalArgumentException("401 비밀번호가 일치하지 않습니다.");
90-
}
89+
Room room = findRoom(request.roomId());
90+
91+
if (room.getState().equals(RoomState.PLAYING)) {
92+
throw new IllegalArgumentException("403 게임이 진행중입니다.");
93+
}
9194

92-
room.getUserIdSessionMap().put(getCurrentUserId(), "");
95+
int maxUserCnt = room.getRoomSetting().maxUserCount();
96+
int currentCnt = room.getUserIdSessionMap().size();
97+
if (maxUserCnt == currentCnt) {
98+
throw new IllegalArgumentException("403 정원이 모두 찼습니다.");
99+
}
100+
101+
if (room.getRoomSetting().locked()
102+
&& !room.getRoomSetting().password().equals(request.password())) {
103+
throw new IllegalArgumentException("401 비밀번호가 일치하지 않습니다.");
104+
}
105+
106+
room.getUserIdSessionMap().put(getCurrentUserId(), PENDING_SESSION_ID);
107+
}
93108
}
94109

95110
public RoomInitialData initializeRoomSocket(Long roomId, String sessionId) {
@@ -102,7 +117,11 @@ public RoomInitialData initializeRoomSocket(Long roomId, String sessionId) {
102117
Map<Long, String> userIdSessionMap = room.getUserIdSessionMap();
103118

104119
playerSessionMap.put(sessionId, player);
105-
userIdSessionMap.put(roomId, sessionId);
120+
String existingSession = userIdSessionMap.get(player.getId());
121+
/* 정상 흐름 or 재연결 */
122+
if (existingSession.equals(PENDING_SESSION_ID) || !existingSession.equals(sessionId)) {
123+
userIdSessionMap.put(player.getId(), sessionId);
124+
}
106125

107126
RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);
108127

@@ -125,50 +144,59 @@ public RoomInitialData initializeRoomSocket(Long roomId, String sessionId) {
125144
}
126145

127146
public RoomExitData exitRoom(Long roomId, String sessionId) {
128-
Room room = findRoom(roomId);
129147

130-
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
148+
Object lock = roomLocks.computeIfAbsent(roomId, k -> new Object());
149+
150+
synchronized (lock) {
151+
152+
Room room = findRoom(roomId);
131153

132-
String destination = getDestination(roomId);
154+
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
133155

134-
Player removePlayer = playerSessionMap.get(sessionId);
156+
String destination = getDestination(roomId);
135157

136-
// if (removePlayer == null) {
137-
// room.getUserIdSessionMap().remove(getCurrentUserId());
138-
// throw new IllegalIdentifierException("404 세션 없음 비정상적인 퇴장 요청");
139-
// }
158+
Player removePlayer = playerSessionMap.get(sessionId);
140159

141-
/* 방 삭제 */
142-
if (playerSessionMap.size() == 1 && playerSessionMap.containsKey(sessionId)) {
143-
roomRepository.removeRoom(roomId);
144-
return RoomExitData.builder().destination(destination).removedRoom(true).build();
160+
if (removePlayer == null) {
161+
room.getUserIdSessionMap().remove(getCurrentUserId());
162+
throw new IllegalIdentifierException("404 세션 없음 비정상적인 퇴장 요청");
145163
}
146164

147-
/* 방장 변경 */
148-
if (room.getHost().getId().equals(removePlayer.getId())) {
165+
/* 방 삭제 */
166+
if (playerSessionMap.size() == 1 && playerSessionMap.containsKey(sessionId)) {
167+
roomRepository.removeRoom(roomId);
168+
roomLocks.remove(roomId);
169+
log.info("{}번 방 삭제", roomId);
170+
return RoomExitData.builder().destination(destination).removedRoom(true).build();
171+
}
149172

150-
Optional<String> nextHostSessionId = playerSessionMap.keySet().stream()
151-
.filter(key -> !key.equals(sessionId)).findFirst();
173+
/* 방장 변경 */
174+
if (room.getHost().getId().equals(removePlayer.getId())) {
152175

153-
Player nextHost =
154-
playerSessionMap.get(
155-
nextHostSessionId.orElseThrow(
156-
() ->
157-
new IllegalArgumentException(
158-
"방장 교체 불가 - 404 해당 세션 플레이어는 존재하지않습니다.")));
176+
Optional<String> nextHostSessionId = playerSessionMap.keySet().stream()
177+
.filter(key -> !key.equals(sessionId)).findFirst();
159178

160-
room.updateHost(nextHost);
161-
}
179+
Player nextHost =
180+
playerSessionMap.get(
181+
nextHostSessionId.orElseThrow(
182+
() ->
183+
new IllegalArgumentException(
184+
"방장 교체 불가 - 404 해당 세션 플레이어는 존재하지않습니다.")));
162185

163-
room.getUserIdSessionMap().remove(removePlayer.getId());
164-
playerSessionMap.remove(sessionId);
186+
room.updateHost(nextHost);
187+
log.info("user_id:{} 방장 변경 완료 ", nextHost.getId());
188+
}
165189

166-
SystemNoticeResponse systemNoticeResponse =
167-
ofPlayerEvent(removePlayer, RoomEventType.EXIT);
190+
room.getUserIdSessionMap().remove(removePlayer.getId());
191+
playerSessionMap.remove(sessionId);
168192

169-
PlayerListResponse playerListResponse = toPlayerListResponse(room);
193+
SystemNoticeResponse systemNoticeResponse =
194+
ofPlayerEvent(removePlayer, RoomEventType.EXIT);
170195

171-
return new RoomExitData(destination, playerListResponse, systemNoticeResponse, false);
196+
PlayerListResponse playerListResponse = toPlayerListResponse(room);
197+
198+
return new RoomExitData(destination, playerListResponse, systemNoticeResponse, false);
199+
}
172200
}
173201

174202
public RoomListResponse getAllRooms() {
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package io.f1.backend.domain.game.app;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.Mockito.doNothing;
5+
import static org.mockito.Mockito.when;
6+
7+
import io.f1.backend.domain.game.dto.request.RoomValidationRequest;
8+
import io.f1.backend.domain.game.model.GameSetting;
9+
import io.f1.backend.domain.game.model.Player;
10+
import io.f1.backend.domain.game.model.Room;
11+
import io.f1.backend.domain.game.model.RoomSetting;
12+
import io.f1.backend.domain.game.store.RoomRepository;
13+
import io.f1.backend.domain.quiz.app.QuizService;
14+
import io.f1.backend.domain.user.entity.User;
15+
import io.f1.backend.global.util.SecurityUtils;
16+
import java.time.LocalDateTime;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import java.util.Optional;
20+
import java.util.concurrent.CountDownLatch;
21+
import java.util.concurrent.ExecutorService;
22+
import java.util.concurrent.Executors;
23+
import lombok.extern.slf4j.Slf4j;
24+
import org.junit.jupiter.api.AfterEach;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.DisplayName;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.extension.ExtendWith;
29+
import org.mockito.Mock;
30+
import org.mockito.MockitoAnnotations;
31+
import org.mockito.junit.jupiter.MockitoExtension;
32+
import org.springframework.context.ApplicationEventPublisher;
33+
import org.springframework.security.core.context.SecurityContextHolder;
34+
35+
@Slf4j
36+
@ExtendWith(MockitoExtension.class)
37+
class RoomServiceTests {
38+
39+
private RoomService roomService;
40+
41+
@Mock
42+
private RoomRepository roomRepository;
43+
@Mock
44+
private QuizService quizService;
45+
@Mock
46+
private ApplicationEventPublisher eventPublisher;
47+
48+
49+
@BeforeEach
50+
void setUp(){
51+
MockitoAnnotations.openMocks(this); // @Mock 어노테이션이 붙은 필드들을 초기화합니다.
52+
roomService = new RoomService(quizService, roomRepository, eventPublisher);
53+
54+
SecurityContextHolder.clearContext();
55+
}
56+
57+
@AfterEach
58+
void afterEach() {
59+
SecurityContextHolder.clearContext();
60+
}
61+
62+
@Test
63+
@DisplayName("enterRoom_동시성_테스트")
64+
void enterRoom_synchronized() throws Exception {
65+
Long roomId = 1L;
66+
Long quizId = 1L;
67+
Long playerId = 1L;
68+
String password = "123";
69+
70+
RoomSetting roomSetting = new RoomSetting("방제목", 5, true, password);
71+
GameSetting gameSetting = new GameSetting(quizId, 10, 60);
72+
Player host = new Player(playerId, "닉네임");
73+
74+
Room room = new Room(roomId, roomSetting, gameSetting, host);
75+
76+
when(roomRepository.findRoom(roomId)).thenReturn(Optional.of(room));
77+
78+
int threadCount = 10;
79+
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
80+
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
81+
RoomValidationRequest roomValidationRequest = new RoomValidationRequest(roomId, password);
82+
for (int i = 1; i <= threadCount; i++) {
83+
Long userId = i + 1L;
84+
String provider = "provider +" + i;
85+
String providerId = "providerId" + i;
86+
LocalDateTime lastLogin = LocalDateTime.now();
87+
executorService.submit(() -> {
88+
try {
89+
User user = User.builder()
90+
.provider(provider)
91+
.provider(providerId).lastLogin(lastLogin).build();
92+
93+
user.setId(userId);
94+
95+
SecurityUtils.setAuthentication(user);
96+
97+
roomService.enterRoom(roomValidationRequest);
98+
} catch (Exception e) {
99+
//e.printStackTrace();
100+
} finally {
101+
SecurityContextHolder.clearContext();
102+
countDownLatch.countDown();
103+
}
104+
});
105+
}
106+
countDownLatch.await();
107+
assertThat(room.getUserIdSessionMap()).hasSize(room.getRoomSetting().maxUserCount());
108+
}
109+
110+
111+
@Test
112+
@DisplayName("exitRoom_동시성_테스트")
113+
void exitRoom_synchronized() throws Exception {
114+
Long roomId = 1L;
115+
Long quizId = 1L;
116+
int threadCount = 10;
117+
118+
String password = "123";
119+
120+
RoomSetting roomSetting = new RoomSetting("방제목", 5, true, password);
121+
GameSetting gameSetting = new GameSetting(quizId, 10, 60);
122+
123+
List<Player> players = new ArrayList<>();
124+
for (int i = 1; i <=threadCount; i++) {
125+
Long id = i + 1L;
126+
String nickname = "nickname " + i;
127+
128+
Player player = new Player(id, nickname);
129+
players.add(player);
130+
131+
}
132+
Player host = players.getFirst();
133+
Room room = new Room(roomId, roomSetting, gameSetting, host);
134+
135+
for (int i = 1; i <=threadCount; i++) {
136+
String sessionId = "sessionId" + i;
137+
Player player = players.get(i - 1);
138+
room.getPlayerSessionMap().put(sessionId,player);
139+
room.getUserIdSessionMap().put(player.getId(),sessionId);
140+
}
141+
142+
log.info("room.getPlayerSessionMap().size() = {}", room.getPlayerSessionMap().size());
143+
144+
when(roomRepository.findRoom(roomId)).thenReturn(Optional.of(room));
145+
doNothing().when(roomRepository).removeRoom(roomId);
146+
147+
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
148+
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
149+
150+
for (int i = 1; i <= threadCount; i++) {
151+
Long userId = i + 1L;
152+
String sessionId = "sessionId" + i;
153+
String provider = "provider +" + i;
154+
String providerId = "providerId" + i;
155+
LocalDateTime lastLogin = LocalDateTime.now();
156+
executorService.submit(() -> {
157+
try {
158+
User user = User.builder()
159+
.provider(provider)
160+
.provider(providerId).lastLogin(lastLogin).build();
161+
user.setId(userId);
162+
SecurityUtils.setAuthentication(user);
163+
164+
log.info("userId = {}", userId);
165+
log.info("room.getHost().getId() = {}", room.getHost().getId());
166+
roomService.exitRoom(roomId, sessionId);
167+
} catch (Exception e) {
168+
e.printStackTrace();
169+
} finally {
170+
SecurityContextHolder.clearContext();
171+
countDownLatch.countDown();
172+
}
173+
});
174+
}
175+
countDownLatch.await();
176+
assertThat(room.getUserIdSessionMap()).hasSize(1);
177+
}
178+
179+
180+
181+
}

0 commit comments

Comments
 (0)