안정적인 실시간 양방향 통신 서비스를 제공하기 위해 순수 WebSocket을 활용하여 메세지 파싱과 라우팅, 예외 처리 시스템을 구현하였습니다. 이 과정에서 메세지 프로토콜 정의, 동시성 문제 해결, AOP을 통한 예외처리 등 다양한 어려움을 해결하였습니다.
- CEBONE은 실시간 양방향 통신을 통해 상대 플레이어와 턴을 번갈아 가며 전투를 하는 게임입니다.
- 플레이어는 상점에서 원하는 무기와 장비 등 아이템을 통해 자신의 캐릭터를 강화할 수 있습니다.
- 로비에서 매칭방을 찾아 전투할 플레이어를 선택할 수 있습니다.
- 전투를 완료하면 획득하는 경험치를 통해 레벨 업을 할 수 있으며 이때 전략적으로 새로운 스킬을 습득하여 성장 방향을 결정할 수 있습니다.
- Version : Java 17
- IDE : IntelliJ
- Framework : SpringBoot 3.1.5
- ORM : JPA
- Real Time Networking : WebSocket
- 회원 가입 및 로그인
- 로비 - 방 생성 및 입장
- 상점 - 아이템 구매
- 인벤토리 - 아이템 장착, 해제, 판매
- 상태창 - 자신의 정보 확인, 경험치
- 인게임 - 실시간 통신으로 게임 진행
{
"command": "COMMAND",
"matchId": "long",
"request":{ }
}
@Component
public class WebSocketSessionManager {
private Map<Long, WebSocketSession> webSocketSessionMap = new ConcurrentHashMap<>();
public synchronized void addWebSocketSessionMap(Long key, WebSocketSession socketSession) {
if (!validateSession(socketSession)) {
throw new WebSocketSessionInvalidException();
}
this.webSocketSessionMap.put(key, socketSession);
}
public synchronized void removeWebSocketSessionMap(Long key) {
this.webSocketSessionMap.remove(key);
}
public synchronized void sendMessage(String message, List<Long> keys) throws IOException {
for (Long key : keys) {
WebSocketSession socketSession = this.webSocketSessionMap.get(key);
if (validateSession(socketSession)) {
socketSession.sendMessage(new TextMessage(message));
}
}
}
public synchronized boolean validateSession(WebSocketSession socketSession) {
return socketSession != null && socketSession.isOpen();
}
}
메시지 파싱
/**
* 인게임 플레이 중 발생하는 WebSocket 기반 요청들을 처리하는 핵심 메소드. 메세지 파싱과 라우팅을 수행함.
*
* @param session WebSocketSession
* @param message WebSocketMessageProtocol 형식을 준수한 메세지
* @throws IOException
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws IOException {
Long characterId = null;
try {
characterId = extractCharacterId(session, Long.class);
WebSocketMessageRequest messageRequest = jsonUtil.parseWebSocketMessage(
message.getPayload());
Long matchId = messageRequest.matchId();
JsonObject request = messageRequest.request();
String command = messageRequest.command();
MatchPlayer matchPlayers = matchService.findPlayerByMatchId(matchId);
List<Long> matchPlayersList = matchPlayers.toList();
if (!matchPlayers.isContainsPlayer(characterId)) {
throw new CharacterNotInMatchException(matchId);
}
WebSocketCommand webSocketCommand = findWebSocketCommand(command);
dispatcher(characterId, matchId, request, matchPlayersList, webSocketCommand);
} catch (Exception e) {
webSocketSessionManager.sendMessage(e.getMessage(),
Collections.singletonList(characterId));
}
}
메세지 라우팅
private void dispatcher(Long characterId, Long matchId, JsonObject request,
List<Long> matchPlayers, WebSocketCommand webSocketCommand)
throws Exception {
String responseMessage = new String();
//message routing
switch (webSocketCommand) {
case GREETING -> {
responseMessage = playController.greeting(characterId);
}
case READY -> {
PlayReadyRequest playReadyRequest = jsonUtil.fromJson(request,
PlayReadyRequest.class);
responseMessage = playController.ready(characterId, matchId
, playReadyRequest);
}
case START -> {
responseMessage = playController.start(characterId, matchId);
}
case TURN_GAME -> {
responseMessage = playController.turnGame(characterId, matchId,
jsonUtil.fromJson(request, PlayTurnRequest.class));
}
case END_GAME -> {
responseMessage = playController.endGame(characterId, matchId);
}
case SURRENDER_GAME -> {
responseMessage = playController.surrenderGame(characterId, matchId);
}
case QUIT_GAME -> {
responseMessage = playController.quitGame(characterId, matchId);
}
case EMPTY -> throw new InvalidWebSocketMessageException(matchId);
}
webSocketSessionManager.sendMessage(responseMessage, matchPlayers);
}
@Aspect
@Component
@RequiredArgsConstructor
public class WebSocketExceptionAspect {
private final JsonUtil jsonUtil;
@Around("execution(* com.project.game.play.controller.PlayController.*(..))")
public Object handleExceptionAndLog(ProceedingJoinPoint joinPoint) {
try {
return joinPoint.proceed();
} catch (WebSocketException we) {
ErrorResponse response = new ErrorResponse(we);
return jsonUtil.toJson(response);
} catch (Throwable e) {
ErrorResponse response = new ErrorResponse(e);
return jsonUtil.toJson(response);
}
}
}