Skip to content

Commit 693240f

Browse files
authored
✨ feat: 입장 로직 변경 및 동시성 적용
* ♻️ 소켓 연결 전 validation 체크 + enter 로직으로 변경 * ♻️ NPE 수정 * ✨ 동시성 추가 및 테스트 * chore: Java 스타일 수정 * ♻️ 리뷰 반영(메소드 분리) * ✨ 방장은 입장 시 ready=true * ♻️ test 중복 코드 정리 * chore: Java 스타일 수정 * ♻️ test code 빌더 수정 * chore: Java 스타일 수정 * ♻️ 컨트롤러 이름 통일 --------- Co-authored-by: github-actions <>
1 parent 26ecfe9 commit 693240f

File tree

6 files changed

+331
-56
lines changed

6 files changed

+331
-56
lines changed

backend/src/main/java/io/f1/backend/domain/game/api/RoomController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ public RoomCreateResponse saveRoom(@RequestBody @Valid RoomCreateRequest request
3131
return roomService.saveRoom(request);
3232
}
3333

34-
@PostMapping("/validation")
34+
@PostMapping("/enterRoom")
3535
@ResponseStatus(HttpStatus.NO_CONTENT)
36-
public void validateRoom(@RequestBody @Valid RoomValidationRequest request) {
37-
roomService.validateRoom(request);
36+
public void enterRoom(@RequestBody @Valid RoomValidationRequest request) {
37+
roomService.enterRoom(request);
3838
}
3939

4040
@GetMapping

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

Lines changed: 116 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,19 @@
3333
import io.f1.backend.domain.quiz.entity.Quiz;
3434

3535
import lombok.RequiredArgsConstructor;
36+
import lombok.extern.slf4j.Slf4j;
3637

38+
import org.hibernate.boot.model.naming.IllegalIdentifierException;
3739
import org.springframework.context.ApplicationEventPublisher;
3840
import org.springframework.stereotype.Service;
3941

4042
import java.util.List;
4143
import java.util.Map;
4244
import java.util.Optional;
45+
import java.util.concurrent.ConcurrentHashMap;
4346
import java.util.concurrent.atomic.AtomicLong;
4447

48+
@Slf4j
4549
@Service
4650
@RequiredArgsConstructor
4751
public class RoomService {
@@ -50,6 +54,8 @@ public class RoomService {
5054
private final RoomRepository roomRepository;
5155
private final AtomicLong roomIdGenerator = new AtomicLong(0);
5256
private final ApplicationEventPublisher eventPublisher;
57+
private final Map<Long, Object> roomLocks = new ConcurrentHashMap<>();
58+
private static final String PENDING_SESSION_ID = "PENDING_SESSION_ID";
5359

5460
public RoomCreateResponse saveRoom(RoomCreateRequest request) {
5561

@@ -66,48 +72,62 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request) {
6672

6773
Room room = new Room(newId, roomSetting, gameSetting, host);
6874

75+
room.getUserIdSessionMap().put(host.id, PENDING_SESSION_ID);
76+
6977
roomRepository.saveRoom(room);
7078

7179
eventPublisher.publishEvent(new RoomCreatedEvent(room, quiz));
7280

7381
return new RoomCreateResponse(newId);
7482
}
7583

76-
public void validateRoom(RoomValidationRequest request) {
84+
public void enterRoom(RoomValidationRequest request) {
7785

78-
Room room =
79-
roomRepository
80-
.findRoom(request.roomId())
81-
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다.-1"));
86+
Long roomId = request.roomId();
8287

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

87-
int maxUserCnt = room.getRoomSetting().maxUserCount();
88-
int currentCnt = room.getPlayerSessionMap().size();
89-
if (maxUserCnt == currentCnt) {
90-
throw new IllegalArgumentException("403 정원이 모두 찼습니다.");
91-
}
90+
synchronized (lock) {
91+
Room room = findRoom(request.roomId());
92+
93+
if (room.getState().equals(RoomState.PLAYING)) {
94+
throw new IllegalArgumentException("403 게임이 진행중입니다.");
95+
}
96+
97+
int maxUserCnt = room.getRoomSetting().maxUserCount();
98+
int currentCnt = room.getUserIdSessionMap().size();
99+
if (maxUserCnt == currentCnt) {
100+
throw new IllegalArgumentException("403 정원이 모두 찼습니다.");
101+
}
92102

93-
if (room.getRoomSetting().locked()
94-
&& !room.getRoomSetting().password().equals(request.password())) {
95-
throw new IllegalArgumentException("401 비밀번호가 일치하지 않습니다.");
103+
if (room.getRoomSetting().locked()
104+
&& !room.getRoomSetting().password().equals(request.password())) {
105+
throw new IllegalArgumentException("401 비밀번호가 일치하지 않습니다.");
106+
}
107+
108+
room.getUserIdSessionMap().put(getCurrentUserId(), PENDING_SESSION_ID);
96109
}
97110
}
98111

99-
public RoomInitialData enterRoom(Long roomId, String sessionId) {
112+
public RoomInitialData initializeRoomSocket(Long roomId, String sessionId) {
100113

101-
Room room =
102-
roomRepository
103-
.findRoom(roomId)
104-
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));
114+
Room room = findRoom(roomId);
105115

106116
Player player = createPlayer();
107117

108118
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
119+
Map<Long, String> userIdSessionMap = room.getUserIdSessionMap();
120+
121+
if (room.isHost(player.getId())) {
122+
player.toggleReady();
123+
}
109124

110125
playerSessionMap.put(sessionId, player);
126+
String existingSession = userIdSessionMap.get(player.getId());
127+
/* 정상 흐름 or 재연결 */
128+
if (existingSession.equals(PENDING_SESSION_ID) || !existingSession.equals(sessionId)) {
129+
userIdSessionMap.put(player.getId(), sessionId);
130+
}
111131

112132
RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);
113133

@@ -130,42 +150,36 @@ public RoomInitialData enterRoom(Long roomId, String sessionId) {
130150
}
131151

132152
public RoomExitData exitRoom(Long roomId, String sessionId) {
133-
Room room =
134-
roomRepository
135-
.findRoom(roomId)
136-
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));
137153

138-
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
154+
Object lock = roomLocks.computeIfAbsent(roomId, k -> new Object());
139155

140-
String destination = getDestination(roomId);
156+
synchronized (lock) {
157+
Room room = findRoom(roomId);
141158

142-
if (playerSessionMap.size() == 1 && playerSessionMap.get(sessionId) != null) {
143-
roomRepository.removeRoom(roomId);
144-
return RoomExitData.builder().destination(destination).removedRoom(true).build();
145-
}
159+
String destination = getDestination(roomId);
146160

147-
Player removedPlayer = playerSessionMap.remove(sessionId);
148-
if (removedPlayer == null) {
149-
throw new IllegalArgumentException("퇴장 처리 불가 - 404 해당 세션 플레이어는 존재하지않습니다.");
150-
}
161+
Player removePlayer = getRemovePlayer(room, sessionId);
151162

152-
if (room.getHost().getId().equals(removedPlayer.getId())) {
153-
Optional<String> nextHostSessionId = playerSessionMap.keySet().stream().findFirst();
154-
Player nextHost =
155-
playerSessionMap.get(
156-
nextHostSessionId.orElseThrow(
157-
() ->
158-
new IllegalArgumentException(
159-
"방장 교체 불가 - 404 해당 세션 플레이어는 존재하지않습니다.")));
160-
room.updateHost(nextHost);
161-
}
163+
/* 방 삭제 */
164+
if (isLastPlayer(room, sessionId)) {
165+
return removeRoom(room, destination);
166+
}
162167

163-
SystemNoticeResponse systemNoticeResponse =
164-
ofPlayerEvent(removedPlayer, RoomEventType.EXIT);
168+
/* 방장 변경 */
169+
if (room.isHost(removePlayer.getId())) {
170+
changeHost(room, sessionId);
171+
}
165172

166-
PlayerListResponse playerListResponse = toPlayerListResponse(room);
173+
/* 플레이어 삭제 */
174+
removePlayer(room, sessionId, removePlayer);
167175

168-
return new RoomExitData(destination, playerListResponse, systemNoticeResponse, false);
176+
SystemNoticeResponse systemNoticeResponse =
177+
ofPlayerEvent(removePlayer, RoomEventType.EXIT);
178+
179+
PlayerListResponse playerListResponse = toPlayerListResponse(room);
180+
181+
return new RoomExitData(destination, playerListResponse, systemNoticeResponse, false);
182+
}
169183
}
170184

171185
public RoomListResponse getAllRooms() {
@@ -183,11 +197,63 @@ public RoomListResponse getAllRooms() {
183197
return new RoomListResponse(roomResponses);
184198
}
185199

200+
private Player getRemovePlayer(Room room, String sessionId) {
201+
Player removePlayer = room.getPlayerSessionMap().get(sessionId);
202+
if (removePlayer == null) {
203+
room.removeUserId(getCurrentUserId());
204+
throw new IllegalIdentifierException("404 세션 없음 비정상적인 퇴장 요청");
205+
}
206+
return removePlayer;
207+
}
208+
186209
private static String getDestination(Long roomId) {
187210
return "/sub/room/" + roomId;
188211
}
189212

190-
private static Player createPlayer() {
213+
private Player createPlayer() {
191214
return new Player(getCurrentUserId(), getCurrentUserNickname());
192215
}
216+
217+
private Room findRoom(Long roomId) {
218+
return roomRepository
219+
.findRoom(roomId)
220+
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));
221+
}
222+
223+
private boolean isLastPlayer(Room room, String sessionId) {
224+
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
225+
return playerSessionMap.size() == 1 && playerSessionMap.containsKey(sessionId);
226+
}
227+
228+
private RoomExitData removeRoom(Room room, String destination) {
229+
Long roomId = room.getId();
230+
roomRepository.removeRoom(roomId);
231+
roomLocks.remove(roomId);
232+
log.info("{}번 방 삭제", roomId);
233+
return RoomExitData.builder().destination(destination).removedRoom(true).build();
234+
}
235+
236+
private void changeHost(Room room, String hostSessionId) {
237+
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
238+
239+
Optional<String> nextHostSessionId =
240+
playerSessionMap.keySet().stream()
241+
.filter(key -> !key.equals(hostSessionId))
242+
.findFirst();
243+
244+
Player nextHost =
245+
playerSessionMap.get(
246+
nextHostSessionId.orElseThrow(
247+
() ->
248+
new IllegalArgumentException(
249+
"방장 교체 불가 - 404 해당 세션 플레이어는 존재하지않습니다.")));
250+
251+
room.updateHost(nextHost);
252+
log.info("user_id:{} 방장 변경 완료 ", nextHost.getId());
253+
}
254+
255+
private void removePlayer(Room room, String sessionId, Player removePlayer) {
256+
room.removeUserId(removePlayer.getId());
257+
room.removeSessionId(sessionId);
258+
}
193259
}

backend/src/main/java/io/f1/backend/domain/game/model/Player.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ public Player(Long id, String nickname) {
1919
this.id = id;
2020
this.nickname = nickname;
2121
}
22+
23+
public void toggleReady() {
24+
this.isReady = !this.isReady;
25+
}
2226
}

backend/src/main/java/io/f1/backend/domain/game/model/Room.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class Room {
2727

2828
private Map<String, Player> playerSessionMap = new ConcurrentHashMap<>();
2929

30+
private Map<Long, String> userIdSessionMap = new ConcurrentHashMap<>();
31+
3032
private final LocalDateTime createdAt = LocalDateTime.now();
3133

3234
public Room(Long id, RoomSetting roomSetting, GameSetting gameSetting, Player host) {
@@ -36,7 +38,19 @@ public Room(Long id, RoomSetting roomSetting, GameSetting gameSetting, Player ho
3638
this.host = host;
3739
}
3840

41+
public boolean isHost(Long id) {
42+
return this.host.getId().equals(id);
43+
}
44+
3945
public void updateHost(Player nextHost) {
4046
this.host = nextHost;
4147
}
48+
49+
public void removeUserId(Long id) {
50+
this.userIdSessionMap.remove(id);
51+
}
52+
53+
public void removeSessionId(String sessionId) {
54+
this.playerSessionMap.remove(sessionId);
55+
}
4256
}

backend/src/main/java/io/f1/backend/domain/game/websocket/GameSocketController.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ public class GameSocketController {
2020
private final MessageSender messageSender;
2121
private final RoomService roomService;
2222

23-
@MessageMapping("/room/enter/{roomId}")
24-
public void roomEnter(@DestinationVariable Long roomId, Message<?> message) {
23+
@MessageMapping("/room/initializeRoomSocket/{roomId}")
24+
public void initializeRoomSocket(@DestinationVariable Long roomId, Message<?> message) {
2525

2626
String websocketSessionId = getSessionId(message);
2727

28-
RoomInitialData roomInitialData = roomService.enterRoom(roomId, websocketSessionId);
28+
RoomInitialData roomInitialData =
29+
roomService.initializeRoomSocket(roomId, websocketSessionId);
2930
String destination = roomInitialData.destination();
3031

3132
messageSender.send(

0 commit comments

Comments
 (0)