본문 바로가기

항해 99/Spring

프로젝트 코드 리팩토링

개요

항해 99 기간의 실전 프로젝트의 6주 기간동안 애플리케이션의 정상적인 동작과 유저 테스트를 위해 코드를 짜고 오류가 발생하면 수정하는 데 쫓기다 보니 코드의 가독성이나 불필요한 중복 등의 문제를 신경 쓰지 못했고, 프로젝트 기간 이후에 유저 피드백을 통한 개선과 프로젝트 코드 리팩토링을 진행하기로 했다.

 

파트

웹 게임 프로젝트를 만들었고, 그 중에서 게임 로직 관련 코드를 작성했기 때문에 내가 작성한 코드에 대한 리팩토링을 진행하고 개선 사항을 적용하기로 했다.

 

코드 리팩토링(Code Refactoring)

기존 코드의 기능을 그대로 유지하면서 코드의 구조를 개선하는 과정

 

주요 목적

코드의 가독성, 유지보수성, 확장성을 높이는 것

 

주요 리팩토링 기법

  • 함수 추출(Extract Method): 길고 복잡한 코드를 여러 작은 함수로 분리하여 가독성을 높인다.
  • 변수 이름 변경(Rename Variable): 변수나 함수의 이름을 의미 있게 변경하여 코드의 이해도를 높인다.
  • 중복 코드 제거(Remove Duplicated Code): 여러 곳에 반복되는 코드를 하나의 함수로 통합한다.
  • 조건문 단순화(Simplify Conditional): 복잡한 조건문을 간단하고 명확하게 변경한다.

리팩토링 조건

팀 회의를 통해서 프로젝트 코드 리팩토링 조건을 정하고 해당 조건에 부합하도록 진행하였다.

  1. 코드의 가독성 향상
  2. 중복 코드 제거
  3. 성능 최적화
  4. 빌더 패턴 적용(toEntity 형식)
  5. 컨트롤러 반환 값 ResponseDTO ResponseEntity로 변경

컨트롤러 리팩토링

private void executeGameRequest(GameRequest request) {
    log.info("클라이언트 요청 순서대로 처리");

    Long gameRoomId = request.getGameRoomId();
    log.info("gameRoomId {}", gameRoomId);

    String gameState = request.getGameState();
    log.info("gameState -> {}", gameState);

    switch (gameState) {
        case "START" -> {
            GameDto.StartRoundResponse response = startGameService.startRound(gameRoomId, request.getEmail());
            sendUserGameMessage(response, request.getEmail()); // 유저별 메시지 전송
        }
        case "ACTION" -> {
            ResponseEntity<?> responseEntity = gamePlayService.playerAction(gameRoomId, request.getGameBetting(), request.getGameBetting().getAction());
            String destination = "/topic/gameRoom/" + gameRoomId;

            if (responseEntity.getBody() instanceof ActionDto actionDto) {
                messagingTemplate.convertAndSend(destination, actionDto);
            } else if (responseEntity.getBody() instanceof String message) {
                messagingTemplate.convertAndSend(destination, message);
            }
        }
        case "END" -> {
            GameDto.EndRoundResponse response = endGameService.endRound(gameRoomId, request.getEmail());
            sendUserEndRoundMessage(response, request.getEmail());
        }
        case "GAME_END" -> {
            GameDto.EndGameResponse response = endGameService.endGame(gameRoomId, request.getEmail());
            sendUserEndGameMessage(response, request.getEmail());
        }
        default -> throw new IllegalStateException("Invalid game state: " + gameState);
    }
}

 

변경 사항

  1. ResponseEntity 처리 : gamePlayService.playerAction 메서드의 반환 값을 ResponseEntity<?>로 변경
  2. 바디 추출 및 전송 : 반환된 ResponseEntity의 바디가 ActionDto인 경우와 String인 경우를 각각 처리하여 메시지 전송을 다르게 한다.
    • ActionDto인 경우 : 원래대로 ActionDto 객체를 전송한다.
    • String인 경우 : 메시지 문자열을 전송한다.

 

서비스 코드 리팩토링

GamePlayService

@Tag(name = "게임 플레이 서비스", description = "게임 플레이 서비스 로직")
@Slf4j
@Service
public class GamePlayService {

    private final GameValidator gameValidator;
    private final GameTurnService gameTurnService;
    private final GameRepository gameRepository;
    private final MeterRegistry registry;
    private final Timer totalGamePlayTimer;
    @PersistenceContext
    private EntityManager em;

    public GamePlayService(GameValidator gameValidator, GameTurnService gameTurnService,
                           GameRepository gameRepository, MeterRegistry registry) {
        this.gameValidator = gameValidator;
        this.gameTurnService = gameTurnService;
        this.gameRepository = gameRepository;
        this.registry = registry;
        this.totalGamePlayTimer = registry.timer("totalGamePlay.time");
    }

    @Transactional
    public ResponseEntity<?> playerAction(Long gameRoomId, GameBetting gameBetting, String action) {
        return totalGamePlayTimer.record(() -> {
            log.info("Action received: gameRoomId={}, nickname={}, action={}", gameRoomId, gameBetting.getNickname(), action);
            GameRoom gameRoom = em.find(GameRoom.class, gameRoomId, LockModeType.PESSIMISTIC_WRITE);
            Game game = gameRoom.getCurrentGame();
            User user = gameValidator.findUserByNickname(gameBetting.getNickname());
            Turn turn = gameTurnService.getTurn(game.getId());

            if (!turn.getCurrentPlayer().equals(user.getNickname())) {
                log.warn("It's not the turn of the user: {}", gameBetting.getNickname());
                return ResponseEntity.status(HttpStatus.FORBIDDEN)
                        .body("당신의 턴이 아닙니다, 선턴 유저의 행동이 끝날 때까지 기다려 주세요.");
            }

            log.info("Performing {} action for user {}", action, gameBetting.getNickname());
            Betting betting = Betting.valueOf(action.toUpperCase());
            return switch (betting) {
                case CHECK -> performCheckAction(game, user, turn);
                case RAISE -> performRaiseAction(game, user, turn, gameBetting.getPoint());
                case DIE -> performDieAction(game, user);
            };
        });
    }

    @Transactional
    public ResponseEntity<ActionDto> performCheckAction(Game game, User user, Turn turn) {
        Timer.Sample checkTimer = Timer.start(registry);
        log.info("Check action: currentPlayer={}, user={}, currentPot={}, betAmount={}",
                user.getNickname(), user.getEmail(), game.getPot(), game.getBetAmount());

        if (game.isCheckStatus() || game.isRaiseStatus()) {
            return gameEnd(user, game);
        }

        user.decreasePoints(game.getBetAmount());
        game.updatePot(game.getBetAmount());
        game.updateCheck();
        turn.nextTurn();
        log.info("First turn check completed, moving to next turn {}", turn.getCurrentPlayer());

        checkTimer.stop(registry.timer("playCheck.time"));
        return createActionResponse(GameState.ACTION, Betting.CHECK, game, turn.getCurrentPlayer(), user);
    }

    @Transactional
    public ResponseEntity<ActionDto> performRaiseAction(Game game, User user, Turn turn, int raiseAmount) {
        Timer.Sample raiseTimer = Timer.start(registry);
        int userPoints = user.getPoints();
        log.info("Raise action initiated by user: {}, currentPoints={}", user.getNickname(), userPoints);

        if (userPoints <= 0) {
            log.info("User has insufficient points to raise");
            return createActionResponse(GameState.END, Betting.RAISE, game, user.getNickname(), user);
        }

        log.info("Raise amount entered: {}", raiseAmount);

        user.decreasePoints(game.getBetAmount() + raiseAmount);
        game.updatePot(game.getBetAmount() + raiseAmount);
        game.setBetAmount(raiseAmount);
        game.updateRaise();
        turn.nextTurn();
        log.info("Raise action completed: newPot={}, newBetAmount={}", game.getPot(), game.getBetAmount());

        raiseTimer.stop(registry.timer("playRaise.time"));
        return createActionResponse(GameState.ACTION, Betting.RAISE, game, turn.getCurrentPlayer(), user);
    }

    @Transactional
    public ResponseEntity<ActionDto> performDieAction(Game game, User user) {
        Timer.Sample dieTimer = Timer.start(registry);
        User playerOne = game.getPlayerOne();
        User playerTwo = game.getPlayerTwo();
        User winner = user.equals(playerOne) ? playerTwo : playerOne;
        log.info("Die action by user: {}, winner: {}", user.getNickname(), winner.getNickname());

        int pot = game.getPot();
        if (winner.equals(playerOne)) {
            game.addPlayerOneRoundPoints(pot);
        } else {
            game.addPlayerTwoRoundPoints(pot);
        }

        game.setFoldedUser(user);
        game.setBetAmount(0);

        log.info("Die action completed, game ended. Winner: {}", winner.getNickname());

        dieTimer.stop(registry.timer("playDie.time"));
        return createActionResponse(GameState.END, Betting.DIE, game, winner.getNickname(), user);
    }

    @Transactional
    public ResponseEntity<ActionDto> gameEnd(User user, Game game) {
        log.info("User points before action: {}, currentBet={}", user.getPoints(), game.getBetAmount());

        int betPoint = user.getPoints() > 0 ? game.getBetAmount() : 0;
        user.decreasePoints(betPoint);
        game.updatePot(betPoint);

        log.info("Check completed, game state updated: newPot={}, newUserPoints={}", game.getPot(), user.getPoints());

        gameRepository.save(game);

        return createActionResponse(GameState.END, Betting.CHECK, game, user.getNickname(), user);
    }

    private ResponseEntity<ActionDto> createActionResponse(GameState nextState, Betting actionType, Game game, String currentPlayer, User user) {
        ActionDto actionDto = new ActionDto(
                GameState.ACTION,
                nextState,
                actionType,
                game.getBetAmount(),
                game.getPot(),
                currentPlayer,
                user.getNickname(),
                user.getPoints()
        );
        return ResponseEntity.ok(actionDto);
    }
}

 

주요 변경 사항

  1. 반환 형식 ResponseEntity로 변경
  2. Builder 타입 변경 : toEntity 형식
  3. 로그 메시지와 주요 로직을 메서드로 분리하여 가독성 향상
  4. ActionDto 빌더 패턴이 중복되는 부분을 공통 메서드로 추출해 중복 코드 제거 및 재사용성 향상

예외 처리 변경

throwException responseEntity

if (!turn.getCurrentPlayer().equals(user.getNickname())) {
    log.warn("It's not the turn of the user: {}", gameBetting.getNickname());
    return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body("당신의 턴이 아닙니다, 선턴 유저의 행동이 끝날 때까지 기다려 주세요.");
}

 

변경 이점

  • HTTP 상태 코드와 메시지 반환: HTTP 상태 코드를 명시적으로 설정할 수 있음
  • 더 나은 오류 메시지 전달: 예외를 던지는 경우 클라이언트가 직접적으로 예외 메시지를 받지 못할 수 있지만, ResponseEntity 사용 시 클라이언트에 명확한 오류 메시지 전달 가능
  • 예외 처리의 일관성 유지: ResponseEntity 사용 시 각 컨트롤러 메서드 내에서 명확하고 일관된 방식으로 예외를 처리할 수 있다.
  • 비즈니스 로직 분리: 비즈니스 로직과 예외 처리를 명확히 분리할 수 있다. 예외를 던지는 대신 적절한 HTTP 응답을 반환함으로써 코드의 가독성과 유지보수성을 높일 수 있다.

 

StartGameService

@Tag(name = "라운드 시작 서비스", description = "게임(라운드) 시작 서비스 로직")
@Slf4j(topic = "게임 시작 서비스 레이어")
@Service
public class StartGameService {

    /* 생성자를 통한 필드 주입 */
    private final GameTurnService gameTurnService;
    private final Timer totalRoundStartTimer;
    private final Timer performRoundStartTimer;
    @PersistenceContext
    private EntityManager em;

    public StartGameService(GameTurnService gameTurnService, MeterRegistry registry) {
        this.gameTurnService = gameTurnService;
        this.totalRoundStartTimer = registry.timer("totalRoundStart.time");
        this.performRoundStartTimer = registry.timer("performRoundStart.time");
    }

    @Transactional
    public StartRoundResponse startRound(Long gameRoomId, String email) {
        return totalRoundStartTimer.record(() -> {
            log.info("게임룸 ID로 라운드 시작: {}", gameRoomId);
            GameRoom gameRoom = em.find(GameRoom.class, gameRoomId, LockModeType.PESSIMISTIC_WRITE);
            gameRoom.updateGameState(GameState.START);

            Game game = gameRoom.getCurrentGame();
            performRoundStartTimer.record(() -> performRoundStart(game, email));
            Card card = email.equals(game.getPlayerOne().getEmail()) ? game.getPlayerTwoCard() : game.getPlayerOneCard();
            Turn turn = gameTurnService.getTurn(game.getId());

            int myPoint = email.equals(game.getPlayerOne().getEmail()) ? game.getPlayerOne().getPoints() : game.getPlayerTwo().getPoints();
            int otherPoint = email.equals(game.getPlayerOne().getEmail()) ? game.getPlayerTwo().getPoints() : game.getPlayerOne().getPoints();

            return new StartRoundResponse("ACTION", game.getRound(), game.getPlayerOne(), game.getPlayerTwo(), card, turn, game.getBetAmount(), game.getPot(), myPoint, otherPoint);
        });
    }

    @Transactional
    public synchronized void performRoundStart(Game game, String email) {
        /* 라운드 수 저장, 라운드 베팅 금액 설정, 플레이어에게 카드 지급, 플레이어 턴 설정*/
        log.info("게임 ID로 라운드 시작 작업 수행 중: {}", game.getId());

        if (!game.isRoundStarted()){
            game.incrementRound();
            game.updateRoundStarted();
            log.info("라운드가 {}로 증가됨.", game.getRound());
        }

        initializeBetting(game);
        if (game.getRound() > 1) {
            List<Card> availableCards = prepareAvailableCards(game);
            Collections.shuffle(availableCards);
            assignRandomCardsToPlayers(game, availableCards, email);
        }

        if (game.getRound() == 1) {
            initializeTurnForGame(game);
        }
    }

    private int calculateInitialBet(User playerOne, User playerTwo) {
        int minPoints = Math.min(playerOne.getPoints(), playerTwo.getPoints());
        int betAmount = Math.max((int) Math.round(minPoints * 0.05), 1);
        return Math.min(betAmount, 2000);
    }

    private List<Card> prepareAvailableCards(Game game) {
        /* 사용한 카드 목록과 전체 카드 목록을 가져옴
         * 전체 카드 목록에서 사용한 카드 목록을 제외하고 남은 카드 목록을 반환한다*/
        Set<Card> usedCards = game.getUsedCards();
        Set<Card> allCards = EnumSet.allOf(Card.class); // 성능 개선 여지 있음
        allCards.removeAll(usedCards);
        return new ArrayList<>(allCards);
    }

    private void assignRandomCardsToPlayers(Game game, List<Card> availableCards, String email) {
        if (email.equals(game.getPlayerOne().getEmail())) {
            game.setPlayerTwoCard(availableCards.get(1));
            game.addUsedCard(availableCards.get(1));
        } else {
            game.setPlayerOneCard(availableCards.get(0));
            game.addUsedCard(availableCards.get(0));
        }
        log.info("플레이어에게 카드 할당됨: {} - {}, {} - {}",
                game.getPlayerOne().getNickname(), game.getPlayerOneCard(), game.getPlayerTwo().getNickname(), game.getPlayerTwoCard());
    }

    private void initializeTurnForGame(Game game) {
        List<User> players = Arrays.asList(game.getPlayerOne(), game.getPlayerTwo());
        gameTurnService.setTurn(game.getId(), new Turn(players));
    }

    private void initializeBetting(Game game) {
        if (game.getPot() == 0) {
            int betAmount = calculateInitialBet(game.getPlayerOne(), game.getPlayerTwo());
            game.getPlayerOne().decreasePoints(betAmount);
            game.getPlayerTwo().decreasePoints(betAmount);
            game.setBetAmount(0);
            game.updatePot(betAmount * 2);
            log.info("초기 배팅금액 {}로 설정됨.", betAmount);
        }
    }
}

 

주요 변경 사항

  1. 로그 메시지 및 중복 코드 제거: 불필요한 로그 메시지 제거 및 중복 코드를 최소화하여 가독성 향상
  2. 메서드 추출: initializeBetting 메서드를 별도로 추출하여 초기 베팅 금액 설정 로직을 분리
  3. 불필요한 조건문 제거: if (game.getRound() > 1) 조건문을 performRoundStart 메서드 내부로 이동하여 가독성 향상
  4. 로컬 변수 및 메서드 사용: 사용되지 않는 변수와 메서드 제거, 필요한 경우 로컬 변수로 대체하여 코드 명확성 높임
  5. 사용자 카드 할당 로직 간소화: assignRandomCardsToPlayers 메서드 내부의 카드 할당 로직 간소화
  6. 로깅 메시지 통합: 카드 할당 시 로깅 메시지를 통합하여 한 번의 로깅으로 모든 정보를 출력하도록 수정

 

EndGameService

@Tag(name = "게임/라운드 종료 서비스", description = "게임/라운드 종료 서비스 로직")
@Slf4j
@Service
public class EndGameService {

    private final GameTurnService gameTurnService;
    private final ValidateRoomRepository validateRoomRepository;
    private final UserRepository userRepository;
    private final Timer totalRoundEndTimer;
    private final Timer totalGameEndTimer;
    private final GameValidator gameValidator;

    @PersistenceContext
    private EntityManager em;

    public EndGameService(GameTurnService gameTurnService, ValidateRoomRepository validateRoomRepository, UserRepository userRepository,
                          MeterRegistry registry,GameValidator gameValidator) {
        this.gameTurnService = gameTurnService;
        this.validateRoomRepository = validateRoomRepository;
        this.userRepository = userRepository;
        this.totalRoundEndTimer = registry.timer("totalRoundEnd.time");
        this.totalGameEndTimer = registry.timer("totalGameEnd.time");
        this.gameValidator = gameValidator;
    }

    /* 라운드 종료 로직*/
    @Transactional
    public EndRoundResponse endRound(Long gameRoomId, String email) {
        return totalRoundEndTimer.record(() -> {
            log.info("Ending round for gameRoomId={}", gameRoomId);

            GameRoom gameRoom = em.find(GameRoom.class, gameRoomId, LockModeType.PESSIMISTIC_WRITE);
            Game game = gameRoom.getCurrentGame();

            /* 라운드 승자 패자 결정
            승자에게 라운드 포인트 할당
            라운드 포인트 값 가져오기*/
            GameResult gameResult = determineGameResult(game);
            Card myCard = email.equals(game.getPlayerOne().getEmail()) ? game.getPlayerOneCard() : game.getPlayerTwoCard();
            log.info("myCard : {}", myCard);

            if (!game.isRoundEnded()) {
                assignRoundPointsToWinner(game, gameResult);
                initializeTurnForGame(game, gameResult);
                game.updateRoundEnded();
            } else {
                game.resetRound();
                log.info("Round reset for gameRoomId={}", gameRoomId);
            }

            log.info("Round result determined: winnerId={}, loserId={}", gameResult.getWinner().getNickname(), gameResult.getLoser().getNickname());
            int roundPot = game.getPot();
            /* 게임 상태 결정 : 다음 라운드 시작 상태 반환 or 게임 종료 상태 반환*/
            String nextState = determineGameState(game);
            log.info("Round ended for gameRoomId={}, newState={}", gameRoomId, nextState);

            return new EndRoundResponse("END", nextState, game.getRound(), gameResult.getWinner(), gameResult.getLoser(),
                    roundPot, myCard, gameResult.getWinner().getPoints(), gameResult.getLoser().getPoints());
        });
    }

    /* 게임 종료 로직*/
    @Transactional
    public EndGameResponse endGame(Long gameRoomId, String email) {
        return totalGameEndTimer.record(() -> {
            log.info("Ending game for gameRoomId={}", gameRoomId);
            GameRoom gameRoom = gameValidator.validateAndRetrieveGameRoom(gameRoomId);
            User user = userRepository.findByEmail(email).orElseThrow(() -> new RestApiException(ErrorCode.NOT_FOUND_USER.getMessage()));
            ValidateRoom validateRoom = validateRoomRepository.findByGameRoomAndParticipants(gameRoom, user.getNickname()).orElseThrow(() -> new RestApiException(ErrorCode.NOT_FOUND_GAME_USER.getMessage()));
            validateRoom.resetReady();
            Game game = gameRoom.getCurrentGame();

            /* 게임 결과 처리 및 게임 정보 초기화*/
            GameResult gameResult = processGameResults(game, email);
            gameRoom.updateGameState(GameState.READY);

            log.info("Game ended for gameRoomId={}, winnerId={}, loserId={}, winnerPot={}, loserPot={}",
                    gameRoomId, gameResult.getWinner().getNickname(), gameResult.getLoser().getNickname(), gameResult.getWinnerPot(), gameResult.getLoserPot());

            /* 유저 선택 상태 반환 */
            return new EndGameResponse("GAME_END", "READY", gameResult.getWinner(), gameResult.getLoser(),
                    gameResult.getWinnerPot(), gameResult.getLoserPot());
        });
    }

    /* 검증 메서드 필드*/
    /* 라운드 승자, 패자 선정 메서드 */
    @Transactional
    public GameResult determineGameResult(Game game) {
        User playerOne = game.getPlayerOne();
        User playerTwo = game.getPlayerTwo();

        if (game.getFoldedUser() != null) {
            User winner = game.getFoldedUser().equals(playerOne) ? playerTwo : playerOne;
            return new GameResult(winner, game.getFoldedUser());
        }

        return getGameResult(game, playerOne, playerTwo);
    }

    @Transactional
    public GameResult getGameResult(Game game, User playerOne, User playerTwo) {
        Card playerOneCard = game.getPlayerOneCard();
        Card playerTwoCard = game.getPlayerTwoCard();

        log.info("{} Card : {}", playerOne.getNickname(), game.getPlayerOneCard());
        log.info("{} Card : {}", playerTwo.getNickname(), game.getPlayerTwoCard());

        /* 카드 숫자가 같으면 1번 덱의 카드를 가진 플레이어가 승리*/
        if (playerOneCard.getNumber() != playerTwoCard.getNumber()) {
            return playerOneCard.getNumber() > playerTwoCard.getNumber() ? new GameResult(playerOne, playerTwo) : new GameResult(playerTwo, playerOne);
        }

        return playerOneCard.getDeckNumber() == 1 ? new GameResult(playerOne, playerTwo) : new GameResult(playerTwo, playerOne);
    }

    /* 라운드 포인트 승자에게 할당하는 메서드*/
    @Transactional
    public void assignRoundPointsToWinner(Game game, GameResult gameResult) {
        User winner = gameResult.getWinner();
        int pointsToAdd = game.getPot();
        winner.updatePoint(pointsToAdd);

        if (winner.equals(game.getPlayerOne())) {
            game.addPlayerOneRoundPoints(pointsToAdd);
        } else {
            game.addPlayerTwoRoundPoints(pointsToAdd);
        }

        log.info("Points assigned: winnerId={}, pointsAdded={}", winner.getNickname(), pointsToAdd);
    }

    /* 게임 내 라운드가 모두 종료되었는지 확인하는 메서드 */
    /* 수정 필요 - 유저 포인트가 0이 있을 때 하는 방법 */
    private String determineGameState(Game game) {
        /* 한 게임의 라운드는 현재 3라운드 까지임
         * 라운드 정보를 확인해 3 라운드일 경우 게임 종료 상태를 반환
         * 라운드 정보가 3보다 적은 경우 다음 라운드 시작을 위한 상태 반환
         * game.getRound >= 3 비교 과정을 게임 시작 시 유저의 입력 값을 통해
         * maxRound 필드 등을 만들어서 비교하는 등의 개선도 가능
         * 게임에 참가 중인 유저의 포인트를 확인해 0이 있을 경우 게임 종료 상태 반환*/
        /* 3라운드 종료 시*/
        if (game.getRound() >= 3) {
            return "GAME_END";
        }

        /* 플레이어의 포인트가 없을 때*/
        if (game.getPlayerOne().getPoints() <= 0 || game.getPlayerTwo().getPoints() <= 0) {
            return "GAME_END";
        }

        /* 정상 실행 상태 */
        return "START";
    }

    /* 게임 결과 처리 메서드*/
    @Transactional
    public GameResult processGameResults(Game game, String email) {
        int playerOneTotalPoints = game.getPlayerOneRoundPoints();
        int playerTwoTotalPoints = game.getPlayerTwoRoundPoints();

        log.info("playerOneTotalPoint : {}", playerOneTotalPoints);
        log.info("playerTwoTotalPoint : {}", playerTwoTotalPoints);

        /* 게임 승자와 패자를 정하고 각각의 정보 업데이트*/
        User gameWinner = playerOneTotalPoints > playerTwoTotalPoints ? game.getPlayerOne() : game.getPlayerTwo();
        User gameLoser = gameWinner.equals(game.getPlayerOne()) ? game.getPlayerTwo() : game.getPlayerOne();

        if (email.equals(gameWinner.getEmail())){
            gameWinner.incrementWins();
        }

        if (email.equals(gameLoser.getEmail())){
            gameLoser.incrementLosses();
        }

        /* 승자와 패자의 총 획득 포인트*/
        int winnerTotalPoints = gameWinner.equals(game.getPlayerOne()) ? playerOneTotalPoints : playerTwoTotalPoints;
        int loserTotalPoints = gameLoser.equals(game.getPlayerOne()) ? playerOneTotalPoints : playerTwoTotalPoints;

        log.info("winnerTotalPoints : {}", winnerTotalPoints);
        log.info("loserTotalPoints : {}", loserTotalPoints);

        /* 게임 데이터 초기화*/
        gameTurnService.removeTurn(game.getId());
        game.resetGame();

        return new GameResult(gameWinner, gameLoser, winnerTotalPoints, loserTotalPoints);
    }

    /* 1라운드 이후 턴 설정 메서드 */
    @Transactional
    public void initializeTurnForGame(Game game, GameResult gameResult) {
        List<User> players = List.of(gameResult.getWinner(), gameResult.getLoser());
        gameTurnService.removeTurn(game.getId());
        Turn turn = new Turn(players);
        gameTurnService.setTurn(game.getId(), turn);
    }

}

 

주요 변경 사항

  1. 중복 코드 제거: 동일한 로직을 갖는 코드를 메서드로 분리하여 중복을 최소화
  2. 로깅 간소화: 중복되고 과도한 로깅을 제거하고 필요한 로깅만 남김
  3. 메서드 추출: 각 메서드의 책임을 명확하게 하기 위해 메서드를 추출하고 가독성 높임
  4. 불필요한 트랜잭션 제거: 불필요한 트랜잭션 어노테이션 제거해 성능 최적화

 

ReadyService

@Service
public class ReadyService {

    private final GameRoomRepository gameRoomRepository;
    private final ValidateRoomRepository validateRoomRepository;
    private final UserRepository userRepository;
    private final MeterRegistry registry;
    private final Timer totalGameReadyTimer;
    private final GameValidator gameValidator;

    public ReadyService(GameRoomRepository gameRoomRepository, ValidateRoomRepository validateRoomRepository,
                        UserRepository userRepository, MeterRegistry registry, GameValidator gameValidator) {
        this.gameRoomRepository = gameRoomRepository;
        this.validateRoomRepository = validateRoomRepository;
        this.userRepository = userRepository;
        this.registry = registry;
        this.totalGameReadyTimer = registry.timer("totalReady.time");
        this.gameValidator = gameValidator;
    }

    @Transactional
    public GameStatus gameReady(Long gameRoomId, Principal principal) {
        return totalGameReadyTimer.record(() -> {
            User user = getUser(principal);
            GameRoom gameRoom = getGameRoom(gameRoomId);
            ValidateRoom validateRoom = getValidateRoom(gameRoom, user);

            checkUserPoints(user);
            updateReadyState(validateRoom);

            List<ValidateRoom> validateRooms = getReadyValidateRooms(gameRoom);

            return determineGameStatus(gameRoom, validateRoom, validateRooms, user.getNickname());
        });
    }

    private User getUser(Principal principal) {
        return userRepository.findByEmail(principal.getName())
                .orElseThrow(() -> new RestApiException(ErrorCode.NOT_FOUND_GAME_USER.getMessage()));
    }

    private GameRoom getGameRoom(Long gameRoomId) {
        return gameRoomRepository.findById(gameRoomId)
                .orElseThrow(() -> new RestApiException(ErrorCode.NOT_FOUND_GAME_ROOM.getMessage()));
    }

    private ValidateRoom getValidateRoom(GameRoom gameRoom, User user) {
        return validateRoomRepository.findByGameRoomAndParticipants(gameRoom, user.getNickname())
                .orElseThrow(() -> new RestApiException(ErrorCode.GAME_ROOM_NOW_FULL.getMessage()));
    }

    private void checkUserPoints(User user) {
        if (!hasSufficientPoints(user)) {
            throw new RestApiException(INSUFFICIENT_POINTS.getMessage());
        }
    }

    private boolean hasSufficientPoints(User user) {
        return user.getPoints() > 0;
    }

    private void updateReadyState(ValidateRoom validateRoom) {
        validateRoom.revert(validateRoom.isReady());
    }

    private List<ValidateRoom> getReadyValidateRooms(GameRoom gameRoom) {
        Timer.Sample getValidateRoomTimer = Timer.start(registry);
        List<ValidateRoom> validateRooms = validateRoomRepository.findAllByGameRoomAndReadyTrue(gameRoom);
        getValidateRoomTimer.stop(registry.timer("readyValidate.time"));
        return validateRooms;
    }

    private GameStatus determineGameStatus(GameRoom gameRoom, ValidateRoom validateRoom, List<ValidateRoom> validateRooms, String nickname) {
        if (validateRooms.size() == 2) {
            gameValidator.gameValidate(gameRoom);
            shuffleFirstCards(gameRoom.getCurrentGame());
            return new GameStatus(gameRoom.getRoomId(), nickname, GameState.ALL_READY);
        }

        if (validateRooms.size() == 1) {
            return new GameStatus(gameRoom.getRoomId(), nickname, validateRoom.isReady() ? GameState.READY : GameState.UNREADY);
        }

        return new GameStatus(gameRoom.getRoomId(), nickname, GameState.NO_ONE_READY);
    }

    private void shuffleFirstCards(Game game) {
        /* 카드를 섞은 후 플레이어에게 각각 한장 씩 제공
         * 플레이어에게 제공한 카드는 사용한 카드목록에 포함되어 다음 라운드에서는 사용되지 않는다*/
        List<Card> availableCards = prepareAvailableCards(game);
        Collections.shuffle(availableCards);

        game.setPlayerOneCard(availableCards.get(0));
        game.setPlayerTwoCard(availableCards.get(1));

        game.addUsedCard(availableCards.get(0));
        game.addUsedCard(availableCards.get(1));
    }

    private List<Card> prepareAvailableCards(Game game) {
        /* 사용한 카드 목록과 전체 카드 목록을 가져옴
         * 전체 카드 목록에서 사용한 카드 목록을 제외하고 남은 카드 목록을 반환한다*/
        Set<Card> usedCards = game.getUsedCards();
        Set<Card> allCards = EnumSet.allOf(Card.class);
        allCards.removeAll(usedCards);
        return new ArrayList<>(allCards);
    }
}

 

주요 변경 사항

  1. 메서드 추출 및 분리: 가독성과 유지보수성을 위해 주요 로직을 별도의 메서드로 분리
  2. 불필요한 조건문 제거 및 간소화: 필요하지 않은 조건문을 제거하고 로직을 간소화
  3. 로깅 간소화: 중복되고 과도한 로깅을 제거하고 필요한 로깅만 남김
  4. 유틸리티 메서드 분리: 유틸리티 메서드를 분리하여 재사용성을 높임(getUser, getGameRoom, getValidateRoom 등)
  5. 변수 이름 개선: 변수 이름을 더 명확하게 변경하여 코드의 이해도를 높임

 

추후 목표

동시성 제어를 위한 Redis SortedSet 적용 이후 정상적인 동작 여부 확인 및 성능 개선을 위한 추가 리팩토링

'항해 99 > Spring' 카테고리의 다른 글

Spring - Image Resize  (5) 2024.09.03
CI/CD 2  (0) 2024.05.13
CI / CD  (0) 2024.05.11
ORM  (0) 2024.05.03
Web Game 코드 설계 정리  (0) 2024.04.23