Skip to content

Latest commit

 

History

History
189 lines (162 loc) · 7.19 KB

README.md

File metadata and controls

189 lines (162 loc) · 7.19 KB

🕹️실시간 양방향 턴제 RPG

진화전투_로고

  • 다운로드: GitHub Release
  • 디자인: Figma
  • ERD: ERD

프로젝트 소개

안정적인 실시간 양방향 통신 서비스를 제공하기 위해 순수 WebSocket을 활용하여 메세지 파싱과 라우팅, 예외 처리 시스템을 구현하였습니다. 이 과정에서 메세지 프로토콜 정의, 동시성 문제 해결, AOP을 통한 예외처리 등 다양한 어려움을 해결하였습니다.

  • CEBONE은 실시간 양방향 통신을 통해 상대 플레이어와 턴을 번갈아 가며 전투를 하는 게임입니다.
  • 플레이어는 상점에서 원하는 무기와 장비 등 아이템을 통해 자신의 캐릭터를 강화할 수 있습니다.
  • 로비에서 매칭방을 찾아 전투할 플레이어를 선택할 수 있습니다.
  • 전투를 완료하면 획득하는 경험치를 통해 레벨 업을 할 수 있으며 이때 전략적으로 새로운 스킬을 습득하여 성장 방향을 결정할 수 있습니다.

💻 개발 환경

  • Version : Java 17
  • IDE : IntelliJ
  • Framework : SpringBoot 3.1.5
  • ORM : JPA
  • Real Time Networking : WebSocket

주요 기능

  • 회원 가입 및 로그인
  • 로비 - 방 생성 및 입장
  • 상점 - 아이템 구매
  • 인벤토리 - 아이템 장착, 해제, 판매
  • 상태창 - 자신의 정보 확인, 경험치
  • 인게임 - 실시간 통신으로 게임 진행

🧩 ERD

CEBONE_ERD

인게임 시연

CEBONE

실시간 양방향 통신 시스템

📝 WebSocket 메세지 프로토콜

{
    "command": "COMMAND",
    "matchId": "long",
    "request":{ }
}

📝 WebSocket 세션 관리 - 동시성 문제 해결

@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 메세지 파싱 및 라우팅

메시지 파싱

/**
     * 인게임 플레이 중 발생하는 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);
    }

AOP 기반 WebSocket 예외 처리

@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);
        }
    }
}