본문 바로가기

항해 99/Spring

Web Game 코드 설계 정리

기술 스택 결정

WebSocket 과 WebRTC

사용 목적

  • WebSocket : 주로 양방향 통신을 위해 주로 사용
  • WebRTC : 브라우저 간의 실시간 통신을 가능하게 하며, 주로 비디오, 오디오, 데이터 스트리밍에 사용

웹 게임에서 실시간 오디오, 비디오 통신 또는 피어-투-피어 데이터 교환을 구현하고자 할 때 WebRTC를 사용하게 되며, 이 경우 시그널링 서버가 필요함

WebRTC를 사용하지 않고 순수하게 게임 로직이나 상태 정보 교환 등을 WebSocket만으로 처리하는 경우, 시그널링 서버는 필요하지 않음

 

WebSocket은 실시간 양방향 통신을 가능하게 하여, 서버와 클라이언트 간에 실시간 메시지 교환이 필요한 웹 기반 게임에 적합하며, 턴제 게임에서 요구되는 기능들을 효과적으로 구현할 수 있음

  • 실시간 통신: WebSocket 연결을 통해 서버와 클라이언트 간에 실시간으로 메시지를 주고받을 수 있습니다. 이는 게임 상태의 실시간 업데이트와 플레이어 간의 인터랙션을 가능하게 합니다.
  • 턴 관리: 서버는 현재 턴이 누구인지 관리할 수 있으며, 이 정보를 모든 클라이언트에 전송하여 게임의 순서를 제어할 수 있습니다.
  • 상태 동기화: 게임 상태(예: 카드 덱의 상태, 플레이어의 손패 등)는 서버에 중앙집중적으로 저장 및 관리될 수 있으며, 변화가 있을 때마다 이를 모든 클라이언트와 실시간으로 동기화할 수 있습니다.
  • 게임 로직 처리: 서버에서 모든 게임 로직을 처리하여 클라이언트는 주로 표시와 사용자 입력 처리에 집중할 수 있습니다. 이는 게임의 규칙을 일관되게 적용하고, 부정행위를 방지하는 데 유리합니다.
  • 에러 및 예외 처리: 서버는 게임 중 발생하는 다양한 상황(예: 플레이어의 비정상적 연결 끊김, 잘못된 게임 동작 등)을 처리하고, 필요한 조치를 취할 수 있습니다.

WebSocket 사용 이유 : 인디언 포커 게임을 웹 기반 게임으로 구현하는데 WebRTC의 기능을 사용하지 않고 WebSocket의 기능만으로도 충분히 구현 가능하다고 판단했기 때문

 

웹 게임 개발 시 고려해야 되는 점

  • 동시성 제어 :
    • 게임의 정확성과 공정성을 유지하기 위해 서버에서는 동시에 여러 요청이 처리되는 상황에서도 일관성 있는 데이터 상태를 보장해야 한다
    • 데이터베이스 트랜잭션 관리와 락(lock) 메커니즘을 사용하여 데이터 무결성을 보장하고, 경쟁 상태(race conditions)를 방지한다
    • 스프링 프레임워크에서 제공하는 @Transactional 어노테이션과 같은 도구를 활용하여 비지니스 로직 단위로 트랜잭션을 관리할 수 있다
    • 서버 내부 로직에서는 멀티스레딩과 동시성 제어 기법을 적절히 활용해야 한다
  • 서버 트래픽 관리 : 
    • 실시간 게임에서는 대량의 동시 연결과 메시지 교환이 발생할 수 있으며, 이로 인해 서버에 높은 트래픽이 발생할 수 있다
    • 서버의 부하를 분산하기 위해 로드 밸런싱 기술을 사용하여 요청을 여러 서버에 균등하게 배분할 수 있다
    • 캐싱, 메시지 큐잉, 데이터 압축 등을 통해 네트워크 트래픽을 최적화하고, 서버의 부하를 감소시킬 수 있다
    • 서버의 확장성(Scalability)을 고려하여, 사용자 수의 증가나 트래픽 변동에 유연하게 대응할 수 있는 인프라를 설계한다

동시성 제어와 서버 트래픽 관리는 게임의 성능과 사용자 경험에 직접적인 영향을 미치기 때문에, 게임 서비스의 설계 및 운영 단계에서 세심한 주의를 기울여야 한다, 이를 위해 체계적인 테스트, 모니터링 및 튜닝 과정이 필요하며, 시스템의 안정성과 확장성을 확보하기 위한 지속적인 노력이 필요함

 

WebSocket 게임 로직 설계

WebSocket pub/sub와 게임 로직에 대한 API 구분

웹소켓 게임 관련 코드에서 퍼블리셔(publisher, pub)와 서브스크라이버(subscriber, sub) 패턴을 활용하여 메시지 교환을 처리한다.

이러한 아키텍쳐에서는 메시지 발행과 구독을 통해 실시간 상호작용을 구현할 수 있는데, 여기서 중요한 것은 API의 명확한 구분보다는 메시지의 흐름과 처리 방식을 효율적으로 관리하는 것이다

 

통합 관리의 장점 :

  • 게임 로직과 웹소켓 통신을 같은 도메인 내에서 관리함으로써 코드의 일관성과 관리 효율성이 높아질 수 있다.
  • pub/sub 관련 로직과 게임 로직이 밀접하게 연관되어 있다면, 한 곳에서 관리하는 것이 효율적일 수 있다

구분의 필요성 :

  • 시스템이 커지고, 다른 종류의 API나 추가적인 기능들이 포함될 경우, 게임 로직과 웹소켓 통신 로직을 분리하여 관리하는 것이 유지보수와 확장성 측면에서 유리할 수 있다
  • API를 분리하면 각 컴포넌트의 책임이 명확해지고, 변경이 필요할 때 다른 부분에 미치는 영향을 최소화 할 수 있다

WebSocket의 pub/sub과 게임 로직을 분리해 관리할 경우 어려움이 될 수 있는 요소

  • 코드의 복잡성 : 기능별로 분리하면 각각의 컴포넌트가 더 단순하고 명확해질 수 있고, 이는 유지보수와 이해하기 쉬운 코드로 이어진다. 복잡한 시스템에서는 이런 분리가 오히려 관리를 용이하게 할 수 있다
  • 팀의 업무 분담 : 팀 내에서 역활과 책임을 분명히 할 수 있다면 관리가 용이해진다, 개발자가 각자의 전문 영역에 집중할 수 있어, 전체적으로 더 효율적인 개발이 가능해진다
  • 시스템의 확장성 : 기능이 분리되어 있으면, 특정 부분에 대한 수정이나 확장이 필요할 때 다른 부분에 미치는 영향을 최소화하며 개발할 수 있다, 이는 장기적으로 시스템의 확장성과 유연성을 높이는 데 도움이 된다
  • 통합 테스트와 디버깅 : 분리된 아케텍처는 컴포넌트 간의 인터페이스를 명확히 정의해야 하므로, 통합 테스트와 디버깅이 복잡해질 수 있으나 이는 API 계약을 잘 정의하고 테스트 자동화를 통해 해결할 수 있다

구조의 분리가 관리를 어렵게 만들 수는 있지만, 장기적인 관점에서 보면 시스템의 안정성, 확장성 및 유지보수성을 높이는 데 기여할 수 있다.

초기에는 설정 및 분리로 인한 오버헤드가 발생할 수 있으나, 잘 정의된 인터페이스와 명확한 책임 분리는 결국 효율적인 관리로 이어절 수 있다

 

웹소켓 pub/sub 시스템 사용 시 게임 로직 동작

게임 시작 시 클라이언트가 특정 주제를 구독하고, 이후 게임 관련 이벤트나 데이터는 별도의 API 호출 없이 웹소켓 연결을 통해 실시간으로 주고 받을 수 있다.

 

  1. 구독(Subscription) : 게임 시작 시, 클라이언트(플레이어)는 웹소켓을 통해 특정 게임방의 주제(예: 게임방 ID)에 구독을 요청한다. 이는 웹소켓을 통해 한 번만 이루어지며, 구독이 성공적으로 완료되면 클라이언트는 해당 주제에 대한 실시간 업데이트를 받을 수 있다
  2. 데이터 전송 : 서버는 게임 상태 변화(예: 게임시작, 플레이어의 행동, 점수 변경 등)가 발생할 때마다 해당 게임방 주제에 메시지를 발행하고 웹소켓을 통해 이 메시지는 실시간으로 구독한 클라이언트에게 전달된다
  3. 게임 로직 처리 : 클라이언트는 웹소켓을 통해 받은 메시지를 기반으로 게임의 상태를 업데이트하고, 필요한 로직을 처리한다. 예를 들어, 서버로부터 "게임 시작" 메시지를 받으면 클라이언트는 게임 UI를 시작 상태로 변경할 수 있다

이 구조에서 클라이언트는 게임 관련 API를 별도로 호출할 필요가 없이, 웹소켓 연결을 통해 게임의 전체 흐름을 관리하고, 상태 변화를 실시간으로 처리할 수 있다. 이 방식은 네트워크 요청의 수를 줄이고, 실시간 상호작용을 효율적으로 구현할 수 있는 장점이 있다.

 

웹 게임에서 배팅 정보와 같은 실시간 데이터를 다룰 때 두 가지 주요 접근 방식

푸시 방식(WebSocket 또는 pub/sub 사용):

  • 실시간 데이터가 변경될 때마다 서버에서 클라이언트로 정보를 자동으로 푸시한다. 클라이언트는 별도의 API 요청을 하지 않고도 실시간으로 업데이트된 정보를 받을 수 있다
  • 웹 게임과 같이 실시간 반응이 중요한 애플리케이션에서 이 방식이 자주 사용 된다

풀 방식(REST API 사용):

  • 클라이언트가 주기적으로 서버에 요청을 보내어 최신 정보를 조회한다. 상대방의 배팅 정보와 같은 데이터가 필요할 때마다 클라이언트는 해당 정보를 가져오는 API를 호출한다

웹 게임에서는 대부분 푸시 방식을 사용하여 실시간으로 상대방의 행동을 반영하는 것이 일반적임, 그러나 아래와 같은 경우에는 API를 통해 조회하는 방식을 고려할 수 있다.

  • 실시간 반응이 필수적이지 않은 경우: 데이터가 자주 변하지 않거나, 실시간 반응이 게임 플레이에 결정적인 영향을 미치지 않는 경우
  • 서버 리소스 관리: 모든 정보를 실시간으로 푸시하는 것이 서버 리소스에 부담이 되는 경우, 요청 기반으로 조회하는 방식이 리소스를 더 효율적으로 관리할 수 있음

대부분의 상황에서는 사용자 경험을 최적화하고 게임의 동적인 요소를 유지하기 위해 실시간 푸시 방식을 사용하는 것이 좋지만 게임의 특성, 서버 부하, 개발 및 유지보수의 복잡성 등을 고려하여 적절한 방식을 선택해야 한다

WebSocket 기반 WebGame 코드 설계 정리

Controller

@MessageMapping("/gameRoom/{gameRoomId}/{gameState}")
public void handleGameState(@DestinationVariable Long gameRoomId, @DestinationVariable String gameState,
                                         @Payload ChatMessage chatMessage, @Payload(required = false) UserChoices userChoices) {

    switch (gameState) {
        case "START" -> {
            StartRoundResponse response = startGameService.startRound(gameRoomId);
            sendUserGameMessage(response); // 유저별 메시지 전송
        }
        case "ACTION", "END", "GAME_END", "USER_CHOICE" -> {
            Object response = switch (gameState) {
                case "ACTION" ->
                        gamePlayService.playerAction(gameRoomId, chatMessage.getSender(), chatMessage.getContent());
                case "END" -> endGameService.endRound(gameRoomId);
                case "GAME_END" -> endGameService.endGame(gameRoomId);
                case "USER_CHOICE" -> gameSessionService.processUserChoices(userChoices);
                default -> throw new IllegalStateException("Unexpected value: " + gameState);
            };
            // 공통 메시지 전송
            String destination = "/topic/gameRoom/" + gameRoomId;
            messagingTemplate.convertAndSend(destination, response);
        }
        default -> throw new IllegalStateException("Invalid game state: " + gameState);
    }
}

 

handleGameState 메서드에서 입력된 gameState 값에 따라 다른 로직을 수행하도록 하는 방식의 이점

  • 간결성
    • 여러 API 엔드포인트 대신 단일 엔드포인트에서 여러 액션을 처리할 수 있어서, 컨트롤러의 구조가 간결해진다
    • 관련된 모든 게임 액션을 하나의 메서드에서 처리할 수 있으므로, 유사한 로직이 여러 곳에 분산되어 중복되는 것을 방지할 수 있다
  • 유지보수성
    • 게임 로직이 변경되거나 새로운 액션이 추가되더라도, 해당 메서드 내에서 처리 로직을 수정하거나 추가하기만 하면 되므로 유지보수가 용이해진다
    • 액션 종류가 많아지더라도 관련 로직을 한 곳에 모아둠으로써, 코드의 가독성과 관리성이 향상된다
  • 확장성
    • 새로운 게임 액션을 추가하려 할 때 기존의 메서드와 엔드포인트를 재활용할 수 있으므로, 확장성이 좋다
    • switch 문이나 다른 조건문을 사용하여 액션을 처리함으로써, 새로운 액션을 손쉽게 통합할 수 있다

handleGameState과 같은 방식은 API의 수를 줄이고 로직을 중앙집중화하여 관리의 복잡성을 줄이는 데 도움이 된다.

이는 특히 다양한 게임 액션이 있고 이러한 액션이 유사한 처리 패턴을 가질 때 매우 유용하다

 

Enum vs Entity

Enum 클래스와 Entity 클래스를 사용하는 경우의 성능 차이는 사용 사례에 따라 달라질 수 있다

  • Enum 클래스 사용
    • Enum은 컴파일 타임에 정의되며, 런타임에 추가적인 메모리 할당이 필요하지 않는다, 이로 인해 메모리 사용량이 상대적으로 낮고, 접근 속도가 빠르다
    • Enum은 상수 값을 정의하는 데 사용되므로, 변경되지 않는 값(예: 카드의 숫자나 덱 번호)을 관리하는 데 적합하다
    • 로직이 단순하고 빠르게 실행될 때 유리하며, Enum 값은 JVM에 의해 효율적으로 관리된다
  • Entity 클래스 사용
    • Entity 클래스는 데이터베이스 테이블과 연결되어 동적인 데이터 관리가 가능하다, 데이터 변경이나 추가가 발생할 경우 유리하다
    • 데이터베이스 연산(예: CRUD)이 필요한 경우, Entity 클래스를 사용하는 것이 적합하며, 캐싱, 트랜잭션 관리, 관계형 데이터 처리 등 ORM(Object-Relational Mapping)의 이점을 활용할 수 있다
    • Entity는 데이터베이스와의 입출력 작업을 포함하므로 Enum에 비해 상대적으로 처리 시간이 길어질 수 있다
  • 성능 측면에서의 선택
    • 만약 카드 덱의 데이터가 게임 플레이 중 변경되지 않고, 빠른 접근이 중요한 경우 Enum을 사용하는 것이 더 효율적일 수 있다
    • 반면, 카드 덱에 관한 정보가 동적으로 변경되거나, 복잡한 데이터 관계를 관리해야 하는 경우 Entity 클래스를 사용하는 것이 적절할 수 있다
  • 결론
    • 성능 측면에서 Enum이 Entity보다 메모리와 처리 시간 면에서 효율적일 수 있지만 실제로 가장 적합한 선택은 애플리케이션의 요구사항과 데이터의 특성에 따라 달라진다.
    • 데이터의 정적/ 동적 특성, 변경 빈도, 복잡성 등을 고려하여 적절한 방법을 선택해야 한다

handleGameState 로직 흐름

  • StartRound
    • 유저정보 확인: 게임 시작 전 각 플레이어의 정보(닉네임, 포인트)를 검증한다
    • 자동으로 playerAction을 호출하여, 플레이어 행동(채팅, 베팅 등)을 처리한다
  • playerAction
    • 유저 간의 채팅을 통해 정보를 교환하고, 일정 시간(예:1분) 후에 베팅 프로세스를 시작한다
    • 모든 플레이어의 베팅이 완료되면 endRound를 호출한다
  • endRound
    • 유저의 카드와 베팅을 비교하여 라운드의 승패를 결정하고, 포인트 정보를 업데이트한다
    • 라운드 카운트를 관리하여, 모든 라운드가 완료되었는지 판단하고, 3라운드가 끝나면 endGame을 호출한다
  • endGame
    • 게임의 최종 결과를 처리하고, 유저에게 게임 재시작 여부를 선택하도록 한다
    • 모든 유저가 다시 게임하기를 선택하면 startRound를 호출하여 새 게임을 시작한다

이 로직은 게임의 전체 흐름을 잘 반영하고 있으며, 각 단계가 명확히 구분되어 있어 게임 상태 관리를 용이하게 한다

또한, 게임의 각 상태 전환을 로직으로 명확하게 표현하여, 게임의 흐름을 이해하기 쉽고 구현하기 명확해진다.

 

 

일반적으로 게임 상태를 관리하는 로직에서는 다음과 같은 흐름으로 처리하는 것이 자연스러울 수 있다

  1. startRound 호출:
    • 라운드를 시작하는 과정에서 필요한 초기화 작업을 수행한다
    • 플레이어에게 카드를 할당하고 초기 베팅 상태를 설정하는 등의 작업
  2. 라운드 진행 상태로 전환:
    • startRound 로직이 완료된 후, 게임의 상태를 '라운드 진행 중'으로 변경하여, 플레이어들이 액션(예: 베팅)을 수행할 수 있는 상태로 만든다
  3. playerAction:
    • 플레이어의 행동을 처리한다, 이는 보통 플레이어의 입력(예: 채팅을 통한 유추, 베팅 선택 등)에 의해 트리거되며, 사용자의 입력에 따라 동적으로 호출된다

즉, playerAction은 일반적으로 플레이어의 입력이나 액션에 의해 호출되므로, startRound에서 직접 playerAction을 호출하기보다는 startRound가 완료된 후 플레이어가 취할 수 있는 행동을 기다리는 상태가 된다

playerAction은 플레이어가 실제로 어떤 액션(예:베팅)을 할 때마다 호출되어야 한다.

 

이러한 흐름은 게임 로직이 사용자의 입력에 따라 반응하며 상태가 전환되는 이벤트 기반 모델을 따른다. 따라서 handleGameState에서는 상태에 따라 적절한 서비스 메서드를 호출하고, 실제 플레이어의 행동은 이벤트(예: 웹소켓 메시지)에 의해 처리된다.

 

호출에 대한 구현

클라이언트 단에서 사용자의 행동에 따라 서버에 요청을 보내는 것이 일반적인 방식

클라이언트(예: 웹 브라우저, 모바일 앱)는 사용자 인터페이스를 통해 플레이어의 행동을 캡처하고, 이를 서버에 요청으로 전송한다.

이 과정에서 웹소켓, HTTP 요청 등 다양한 방식이 사용될 수 있다.

  • 클라이언트 단에서의 행동 처리
    • 사용자 인터페이스: 플레이어는 게임 상태에 맞는 액션(예: 베팅, 카드 선택 등)을 사용자 인터페이스를 통해 수행한다
    • 요청 전송: 사용자의 액션이 발생하면, 클라이언트는 해당 액션 정보를 서버의 적절한 엔드포인트로 전송한다
      • 예를 들어 WebSocket을 사용하는 경우, playerAction 메시지를 서버에 보낼 수 있다
    • 서버 응답 처리: 서버로부터의 응답을 클라이언트가 받아 사용자 인터페이스를 업데이트하거나 게임 상태를 변경합니다
  • 서버 단에서의 처리
    • 요청 처리: 서버는 클라이언트로부터 받은 요청을 처리하기 위해 적절한 컨트롤러 및 서비스 로직을 실행합니다.
      • 예를 들어 handleGameState 메서드 내에서 게임 상태를 따라 startRound, playerAction endRound 등의 메서드 호출할 수 있다
    • 게임 상태 관리: 서버는 게임의 현재 상태를 관리하고, 플레이어의 액션에 따라 게임 흐름을 조정합니다

이러한 구조는 서버와 클라이언트 간의 명확한 역할 분담을 통해, 각각의 책임을 효과적으로 수행할 수 있게 해준다

클라이언트는 사용자와의 상호작용을 처리하고, 서버는 게임 로직과 데이터 관리를 담당한다

 

게임 로직 내에서 채팅

일반적으로 1분 채팅 시간이 끝나고 playerAction을 호출하는 과정은 클라이언트 단에서 처리하는 것이 좋다

이는 서버와 클라이언트 간의 상호작용이 필요한 부분이며, 사용자의 인터랙션이나 시간에 기반한 액션이 클라이언트에서 발생하기 때문이다.

 

  • 클라이언트 단에서의 처리
    • 타이머 처리: 클라이언트 단에서는 게임의 각 라운드가 시작될 때 1분 타이머를 설정할 수 있으며, 이 타이머는 채팅 시간을 제어하는 데 사용된다.
    • 시간 만료 처리: 타이머가 만료되면, 클라이언트는 사용자에게 채팅 시간이 끝났음을 알리고, 서버에 playerAction을 시작하도록 요청을 보낼 수 있다
  • 백엔드 클라이언트 간의 상호작용
    • 클라이언트는 타이머가 만료되었을 때 서버에 알리기 위해, 예를 들어 WebSocket 메시지를 사용할 수 있다
    • 서버는 이 메시지를 받고, playerAction 상태로 게임을 전환하여 플레이어가 다음 행동을 취할 수 있도록 한다

 

Entity 추가 고려 사항

GameRoom 엔티티에 필요한 컬럼을 추가해서 사용하는 것과 별도의 Game 엔티티를 만들어 사용하는 것 사이에서 다음과 같은 요소들을 고려해햐 한다

  • 도메인 모델의 복잡성과 응집도
    • GameRoom 엔티티 확장: GameRoom이 상태, 참여자, 플레이어 카드, 베팅 금액 등 게임 진행에 필요한 모든 정보를 자연스럽게 포함할 수 있다면, GameRoom에 필요한 컬럼을 추가하여 확장하는 것이 좋다
    • 별도의 Game 엔티티 생성: 만약 게임 진행과 관련된 로직이 GameRoom의 기본 책임(예: 플레이어 관리, 방 설정)과 명확하게 구분되어야 하고, 게임 로직이 더 복잡하거나 독립적인 경우, Game을 별도의 엔티티로 분리하여 관리하는 것이 낫다
  • 데이터 모델의 확장성
    • 확장성: 시스템이 발전하면서 게임에 더 많은 기능이 추가될 가능성을 고려해야 한다, 미래에 다양한 종류의 게임이나 룰이 GameRoom에 통합될 가능성이 있다면, Game을 별도의 엔티티로 만들어서 확장성을 높이는 것이 현명할 수 있다
  • 데이터 관리와 접근
    • 데이터 관리: GameRoom과 Game의 데이터를 어떻게 관리하고 접근할지도 중요하다, 각 엔티티의 생명주기, 관리의 복잡성, 데이터 접근 패턴 등을 고려해야 한다
    • 트랜잭션 관리: 게임의 라이프사이클과 GameRoom의 라이프사이클이 긴밀하게 연결되어 있으면 하나의 엔티티로 관리하는 것이 트랜잭션 관리 측면에서 이점이 있을 수도 있다
  • 결론
    • GameRoom 엔티티만으로 게임 로직을 충분히 처리할 수 있고, 그 관리가 비교적 단순하다면 GameRoom 내에 필요한 컬럼을 추가하는 것이 좋다
    • 게임 로직이 복잡하거나 미래의 확장성, 유지보수의 편의성을 고려해야 한다면, Game과 GameRoom을 분리하여 각각의 생명주기를 명확히 하는 것이 바람직하다

 

Entity 분리해서 관리하는 것의 이점

  • 확장성
    • 룰의 다양화: 서로 다른 룰이나 게임 메커니즘이 GameRoom에 적용될 수 있으므로, Game 엔티티에서 이를 관리하면 게임 별 로직을 더 효과적으로 구현할 수 있다
    • 게임 로직의 독립성: 특수 룰이 추가되면 게임 로직이 복잡해질 수 있으므로, 이를 독립된 Game 엔티티에서 관리하는 것이 로직을 명확히 분리하고 유지보수하기 좋다
  • 유지보수성
    • 코드의 관리: 게임 규칙이 변화하거나 다양해짐에 따라, 관련 로직을 별도의 엔티티로 분리함으로써 코드의 변경이 다른 부분에 미치는 영향을 최소화할 수 있다
    • 테스트 용이성: Game과 GameRoom을 분리하면, 각각에 대한 테스트를 독립적으로 진행할 수 있어, 더 효과적인 단위 테스트와 디버깅이 가능하다
  • 설계의 명확성
    • 역할과 책임의 분명한 분리: GameRoom은 플레이어 관리, 세션 관리 등의 방 관리 측면을, Game은 실제 게임의 진행, 규칙 관리 등을 담당함으로써, 각각의 역할이 더 명확해짐
  • 결론
    • 새로운 특수 룰을 추가하고 게임 로직이 복잡해질 경우, Game과 GameRoom을 분리하여 관리하는 것이 시스템의 확장성, 유지보수성 및 설계의 명확성 측면에서 유리하다
    • 이러한 구조는 앞으로 시스템을 확장하거나 변경할 때 유연성을 제공하며, 각 구성요소의 관리를 용이하게 한다

 

Game Entity에서 User 엔티티와 연관 관계

GameRoom에서 참가한 유저 데이터를 관리하는 경우, Game 엔티티에서 반드시 유저 데이터(User 엔티티)에 대한 직접적인 연관관계를 유지할 필요는 없으나 이는 설계와 애플리케이션의 요구사항에 따라 달라질 수 있다.

 

  • 연관관계가 필요하지 않을 때
    • 중복 관리 회피: GameRoom이 이미 유저 정보를 관리하고 있다면, Game 엔티티에서 유저에 대한 추가적인 연관관계를 정의할 필요가 없다. 이렇게 하면 데이터 중복 관리를 피할 수 있다.
    • 책임 분리: GameRoom은 참가한 유저와 방 설정 관련 정보를, Game은 게임 진행과 관련된 상태와 로직을 관리한다. 이러한 책임 분리를 통해 각 엔티티의 역할을 명확히 할 수 있다.
  • 연관관계가 필요할 때
    • 게임 로직의 요구사항: 만약 Game 엔티티 내에서 플레이어 별로 특정 게임 로직을 처리해야 한다면, Game에서 직접 유저 정보에 접근할 수 있어야 한다. 예를 들어, 게임별 플레이어의 상태나 점수를 관리해야 할 경우 Game 내에 유저에 대한 연관관계가 필요할 수 있다

Game에서 User 데이터에 대한 연관관계의 필요성은 게임의 비즈니스 로직, 데이터 관리의 효율성, 그리고 시스템 설계의 전반적인 명확성에 기반해 결정되어야 한다. 가능한 한 책임과 관심사를 분리하여 각 엔티티가 자신의 주된 역할에 집중할 수 있도록 하는 것이 중요하다.

 

게임 주요 기능 클라이언트 / 서비스 단 처리 여부

채팅 처리:

  • 클라이언트 단 처리의 장점
    • 리얼타임 인터랙션: 클라이언트 단에서 채팅을 처리하면 WebSocket 같은 기술을 이용해 실시간으로 통신할 수 있어 사용자 경험이 개선된다
    • 서버 부하 감소: 모든 채팅 메세지가 서버를 거치지 않으므로, 서버 부하가 줄어들고, 대규모 사용자가 접속하는 환경에서 효율적일 수 있다
  • 클라이언트 단 처리의 단점
    • 메시지 영속성: 메시지를 서버에 저장하지 않으면, 새로운 클라이언트 세션에서 이전 대화를 복원할 수 없다
    • 보안 및 검증: 클라이언트 단에서만 채팅을 관리하면 메시지 내용에 대한 검증이나 필터링이 어려울 수 있다

베팅 처리:

  • 서버 단 처리의 장점
    • 보안: 서버에서 베팅을 처리하면 보안이 강화되고, 부정 행위를 방지할 수 있다
    • 일관성: 게임 상태와 베팅 로직이 중앙에서 관리되므로 데이터의 일관성을 보장할 수 있다
    • 트랜잭션 관리: 서버에서 트랜잭션을 관리함으로써 데이터 무결성을 유지할 수 있다
  • 서버 단 처리의 단점
    • 응답 시간: 모든 행동이 서버를 통해야 하므로 네트워크 지연이 사용자 경험에 영향을 줄 수 있다

결론:

  • 채팅: 채팅 기능은 대부분 실시간 통신이 중요한 부분이므로, 클라이언트 단에서 처리하는 것이 일반적이다. 필요한 경우 서버를 통해 메시지를 브로드캐스팅하고, 필터링 및 검증 로직을 적용할 수도 있다
  • 베팅: 베팅은 게임의 핵심적인 금융 거래와 관련된 부분이기 때문에 서버에서 처리하는 것이 좋다. 서버 단에서 베팅 로직을 처리하면 보안, 데이터 무결성, 그리고 사용자 간의 동기화를 보장할 수 있다.

따라서 playerAction 메서드는 채팅 관련 로직은 클라이언트에서 처리하고, 서버에서는 베팅 로직만을 담당하도록 설계하는것이 바람직하다

 

코드 리팩토링

서비스 클래스 분리 방식

  1. 게임 서비스 클래스 분리 : 라운드 시작 / 게임 로직(베팅) / 라운드 종료 / 게임 종료
  2. 서비스 로직을 다른 클래스로 분리해서 서비스 파일에 임포트해서 사용

서비스 클래스 분리

  • 장점
    • 단일 책임 원칙(Single Responsibility Principle): 각 서비스 클래스가 하나의 기능에만 집중하므로 유지보수와 확장성이 용이해진다
    • 분리된 관심사(Separation of Concerns): 기능별로 클래스를 분리함으로써 코드의 가독성이 향상되고, 각 부분을 독립적으로 테스트할 수 있다
  • 단점
    • 클래스 수 증가: 기능을 세분화하면 클래스의 수가 늘어나, 프로젝트의 복잡성이 증가할 수 있다
    • 상호 의존성 증가: 서로 다른 서비스 간에 데이터를 교환하거나 상태를 관리해야 할 경우, 의존성이 증가할 수 있다

서비스 로직을 다른 클래스로 분리해서 서비스 파일에 임포트

  • 장점
    • 재사용성 증가: 공통 로직을 별도의 클래스로 분리하면 다른 서비스에서도 재사용할 수 있다
    • 모듈화: 기능별로 로직을 모듈화하면 시스템의 각 부분을 독립적으로 개발하고 관리할 수 있다
  • 단점
    • 결합도 문제: 다른 클래스를 임포트해서 사용하면 결합도가 증가할 수 있으며, 이는 유지보수성을 저해할 수 있다
    • 네임스페이스 관리: 여러 클래스를 임포트하는 경우, 네임스페이스가 복잡해질 수 있고, 이로 인해 이름 충돌이 발생할 수 있다

종합적 판단

두 방법을 종합적으로 평가할 때, 프로젝트의 특성과 요구 사항을 고려해야 한다.

만약 시스템이 복잡하고 확장성을 고려해야 한다면, 게임 서비스 클래스 분리 방식이 좋을 수 있다. 이 방식은 기능별로 명확하게 구분되어 있어 확장 및 유지보수가 용이하다

반면, 특정 로직이 여러 서비스에서 공통적으로 사용된다면, 서비스 로직을 다른 클래스로 분리하는 방식이 더 적합할 수 있다. 이는 재사용성을 높이고 중복 코드를 줄일 수 있다.

 

결론적으로 두 방식을 적절히 조합하여 사용하는 것이 좋을 수 있다. 예를 들어, 공통 로직은 재사용 가능한 모듈로 분리하고, 주요 게임 로직은 기능별로 클래스를 분리하여 관리하는 식으로 구현할 수 있다.

이렇게 하면 시스템의 유연성과 확장성을 동시에 확보할 수 있다.

 

코드 리팩토링 순서

  1. 엔티티 클래스를 먼저 리팩토링하는 경우
  2. 서비스 클래스를 먼저 리팩토링하는 경우

엔티티 클래스 먼저 리팩토링하는 경우의 장점

  • 데이터 모델 정의: 엔티티 클래스는 애플리케이션의 데이터 모델을 정의한다. 데이터 모델을 먼저 명확하게 하는 것은 애플리케이션의 기본 구조를 결정하는 데 도움이 된다
  • 안정적인 기반 제공: 엔티티가 확장되면 서비스 로직이 이를 바탕으로 구축된다. 이는 후속 리팩토링 작업에서 안정적인 기반을 제공한다

서비스 클래스 먼저 리팩토링하는 경우의 장점

  • 비즈니스 로직 이해: 때로는 서비스 클래스의 리팩토링을 통해 비즈니스 로직을 더 잘 이해하고, 이를 바탕으로 엔티티 모델을 더 적절하게 수정할 수 있다
  • 진화하는 설계: 애플리케이션의 기능과 요구 사항이 변함에 따라 서비스 로직을 먼저 리팩토링하면 데이터 모델의 변경 필요성을 보다 명확히 파악할 수 있다

종합적 판단

  • 순서의 중요성: 일반적으로 엔티티 클래스를 먼저 리팩토링하는 것이 바람직함, 이는 데이터 모델이 애플리케이션의 핵심이고, 이를 바탕으로 서비스 로직이 구현되기 때문이다.
  • 상황에 따른 유연성: 그러나 실제 리팩토링 시에는 현재 코드베이스의 상태, 개발 팀의 경험, 프로젝트 요구 사항 등을 고려하여 순서를 유연하게 결정해야 한다. 어떤 경우에는 서비스 로직을 통해 데이터 모델의 필요성이나 변경점을 파악하고 이를 바탕으로 엔티티를 수정하는 것이 더 효율적일 수 있다

따라서 두 부분의 리팩토링을 진행할 때는 프로젝트의 전체적인 구조와 목표를 고려하여 결정하는 것이 중요하다

가능하다면 두 영역 사이의 의존성을 최소화하고, 리팩토링을 진행하면서 지속적으로 테스트하며 진행하는 것이 좋다

 

리팩토링 시 접근 방식

  1. 메서드 분리: 큰 메서드를 더 작고 관리하기 쉬운 메서드로 분리하여 각 메서드가 하나의 기능만 수행하도록 한다
  2. 서비스 레이어와 도메인 레이어의 책임 명확화: 비즈니스 로직과 데이터 액세스 로직을 명확히 분리하여 유지보수성과 테스트 용이성을 높인다
  3. 예외 처리의 일관성 확보: 적절한 예외 처리를 통해 시스템의 안정성을 강화한다
  4. 중복 코드 제거: 유사한 로직이 반복되는 경우, 공통 메서드를 만들어 중복을 제거한다
  5. 의존성 명확화: 의존성 주입을 사용하여 필요한 의존성을 명확하게 정의하고, 필드 주입 대신 생성자 주입을 사용하여 불변성을 보장한다

위 방식으로 코드를 리팩토링하면 각 메서드의 목적이 더 명확해지고, 유지보수 및 테스트가 용이해진다. 또한, 비즈니스 로직과 데이터 액세스 로직이 분리되어 각각의 변경이 다른 부분에 미치는 영향을 최소화할 수 있다

 

데이터 반환에 대한 고려

  • DTO(Data Transfer Object): 클라이언트와 서버 간의 통신에서 사용하는 객체로, 메서드의 반환값으로 사용하여 클라이언트에 전달되는 데이터의 구조를 명확하게 정의할 수 있다
  • DAO(Data Access Object): 데이터베이스의 데이터에 접근하기 위한 객체로, 주로 데이터베이스 CRUD 작업에 사용된다

컨트롤러에서 서비스를 호출할 때는, 처리 결과를 클라이언트에 전달해야 하는 경우 DTO를 사용하여 데이터를 반환하는 것이 일반적임

이는 클라이언트에 보내는 데이터 형식과 내용을 명확하게 정의하고, 필요한 정보만 전달하여 데이터 전송의 효율성을 높이며, 클라이언트와 서버 간의 계약을 명확하게 할 수 있다

 

결론

  • 각 서비스 호출 후에는 처리 결과를 DTO를 통해 반환해주는 것이 좋다, 이렇게 함으로써 프론트엔드와 백엔드 간의 명확한 계약(Contract)이 형성되고, 데이터 형식이 표준화된다
  • DAO는 데이터 액세스 로직 내부에서 사용되며, 일반적으로 서비스 계층에서 직접 클라이언트로 반환되지 않는다

 

MessageMapping에 DTO를 통한 데이터 반환 필요성

@MessageMapping을 사용하는 컨텍스트에서 DTO(Data Transfer Object)를 통해 데이터를 반환하는 것은 클라이언트와 서버 간의 통신에서 여러 가지 이점을 제공한다.

주요 필요성과 이점은 다음과 같다:

 

1. 데이터 캡슐화 및 구조화

DTO는 특정 작업에 필요한 데이터를 캡슐화하여 전달한다, 이를 통해 필요한 데이터만 구조화하여 클라이언트에 전송할 수 있으며, 클라이언트는 받은 데이터를 가공없이 바로 사용할 수 있다

 

2. 안정성과 일관성

DTO를 사용하면 API의 반환 값이 일관되고 예측 가능해진다. 클라이언트는 항상 동일한 형식의 데이터 구조를 기대할 수 있으며, 이는 프론트엔드 개발 시 안정성과 일관성을 보장한다

 

3. 타입 안정성

특히 타입이 엄격한 언어르 사용하는 경우, DTO는 타입 안정성을 보장한다, 클라이언트와 서버 간에 주고받는 데이터에 대해 컴파일 타임에 타입 체크를 수행하여 런타임 오류의 가능성을 줄일 수 있다

 

4. 네트워크 효율성

DTO를 통해 데이터를 전달하면 필요한 정보만을 선택적으로 포함시킬 수 있어 네트워크를 통한 데이터 전송량을 최적화할 수 있다, 이는 특히 대용량 데이터를 다루는 애플리케이션에서 중요하다

 

5. 보안

DTO를 사용하면 엔티티의 모든 정보를 클라이언트에 노출시키지 않고, 필요한 정보만을 제공할 수있다. 이는 민감한 정보를 숨기고 데이터 노출을 최소화하여 애플리케이션의 보안성을 강화한다

 

결론

WebSocket 통신을 포함한 모든 클라이언트-서버 통신에서 DTO를 사용하여 데이터를 반환하는 것은 안정성, 일관성, 효율성, 보안 등을 포함한 다양한 이점을 제공한다. 따라서 통신 프로토콜과 관계없이 DTO를 사용하는 것은 좋은 개발 관행이다

 

서비스 로직 반환 DTO에 상태 값 전달

서비스 로직에서 상태 값을 반환 DTO에 포함시키는 것은 매우 합리적인 접근 방법으로 클라이언트에게 현재 게임의 상태에 대한 명확한 정보를 제공하고, 클라이언트가 해당 상태에 맞게 UI를 업데이트하거나 다음 행동을 결정하는 데 도움을 줄 수 있다

 

장점

  • 명확한 커뮤니케이션: 서비스 호출 결과로 gameState를 포함함으로써 클라이언트와 서버 간의 통신이 더 명확해진다, 클라이언트는 현재 게임의 상태를 정확히 알 수 있으며, 이에 따라 적절한 로직을 실행할 수 있다
  • 상태 관리 용이성: 클라이언트 측에서 게임 상태를 관리하는 복잡성이 감소한다, 서버에서 상태 정보를 전달받음으로써 클라이언트는 상태 관리 로직을 최소화할 수 있다
  • 일관된 상태 전환: 서버 측에서 gameState를 관리하고 전달함으로써, 클라이언트와 서버 간의 게임 상태가 일관되게 유지된다, 이는 상태 불일치로 인한 버그를 줄이는 데 도움이 된다

 

카드 관련 로직

roundStart 시에 카드 목록에서 이전 라운드에 사용된 카드를 제외하고 사용하는 접근 방식은 다음 라운드에 새롭고 고유한 카드 세트를 제공함으로써 게임의 공정성과 다양성을 보장하는데 효과적임

 

이 접근 방식이 제대로 작동하기 위해서는 몇 가지 요소를 고려해야 한다

  1. 카드 목록 관리
    • 게임의 각 라운드 시작 시, 전체 카드 목록에서 이미 사용된 카드를 제외하여 사용 가능한 카드 목록을 생성해야 한다
    • 이를 통해 각 라운드마다 사용자에게 새로운 카드 조합을 제공할 수 있으며, 이전 라운드의 카드 사용이 다음 라운드에 영향을 미치지 않도록 한다
  2. 카드 재사용 로직
    • 전체 게임을 통틀어 모든 카드가 한 번씩 사용되었다면, 사용된 카드 목록을 초기화하거나 새로운 게임 룰을 적용해야 할 수도 있다, 이는 게임의 규칙과 디자인에 따라 달라진다
    • 카드 게임의 종류에 따라 사용된 카드 목록을 어떻게 관리할지 결정해야 한다, 예를 들어 포커나 블랙잭과 같은 게임은 한 세트의 카드로 여러 라운드를 진행할 수 있으며, 카드가 다 소진되면 새로운 세트를 사용할 수 있더
  3. 성능 고려
    • 카드 목록을 처리하는 로직은 게임의 성능에 영향을 줄 수 있으므로 효율적으로 구현되어야 한다
    • 대규모의 카드 목록과 빈번한 업데이트가 있는 경우, 성능 저하를 피하기 위해 최적화된 자료 구조와 알고리즘을 사용해야 한다
  4. 상태 일관성 유지
    • 멀티플레이어 게임의 경우, 서버 측에서 게임 상태를 중앙 집중적으로 관리하고 모든 플레이어에게 일관된 상태를 보장해야 한다
    • 동시성 문제가 발생하지 않도록 주의해야 하며, 여러 사용자가 동시에 게임에 액세스할 때 상태가 올바르게 동기화되어야 한다

위 요소들을 고려하여 roundStart 시에 전체 카드 목록에서 이전 라운드에 사용된 카드를 제외하는 방식은 잘 작동할 것임(게임의 규칙과 특성에 맞게 카드를 관리하고 사용하는 것이 중요)

 

여러 클래스에 같은 필드를 주입하는 문제 해결 방법

  1. 상속 사용
  2. 컴포지션 사용

상속 사용

공통 필드와 메서드를 가진 기본 클래스를 만들어 이를 상속받는 방식

public abstract class BaseService {
    protected final GameRoomRepository gameRoomRepository;
    protected final UserRepository userRepository;

    protected BaseService(GameRoomRepository gameRoomRepository, UserRepository userRepository) {
        this.gameRoomRepository = gameRoomRepository;
        this.userRepository = userRepository;
    }
}

public class StartGameService extends BaseService {
    public StartGameService(GameRoomRepository gameRoomRepository, UserRepository userRepository) {
        super(gameRoomRepository, userRepository);
    }

    // StartGameService의 메서드 구현
}

 

컴포지션 사용

공통 필드를 가진 별도의 구성 클래스를 만들고, 이를 사용하는 클래스에서 이를 포함시키는 방식

public class RepositoryHolder {
    public final GameRoomRepository gameRoomRepository;
    public final UserRepository userRepository;

    public RepositoryHolder(GameRoomRepository gameRoomRepository, UserRepository userRepository) {
        this.gameRoomRepository = gameRoomRepository;
        this.userRepository = userRepository;
    }
}

public class StartGameService {
    private final RepositoryHolder repositoryHolder;

    public StartGameService(RepositoryHolder repositoryHolder) {
        this.repositoryHolder = repositoryHolder;
    }

    // StartGameService의 메서드 구현
}

 

고려 사항

  • 상속: 상속을 사용할 때는 "is-a" 관계가 성립해야 한다. 즉, 하위 클래스가 상위 클래스의 "하위 유형"이어야 한다, 상속은 계층 구조가 복잡해질 수 있으므로 신중하게 사용해야 한다
  • 컴포지션: 컴포지션은 더 유연한 코드 재사용 방식을 제공하며, "has-a" 관계를 만든다, 이 방식은 서비스가 다양한 저장소에 의존하는 경우 중복을 줄이는 좋은 방법이다

결론

코드 중복을 줄이는 방법은 상황에 따라 다를 수 있다, 상속과 컴포지션 둘 다 유효한 접근 방법이지만, 설계의 목적과 유지보수성, 확장성을 고려하여 적절한 방법을 선택해야 한다.

일반적으로는 컴포지션을 사용하는 것이 더 선호되며, 이는 클래스 간의 느슨한 결합을 유지하고 변경에 더 유연하게 대응할 수 있도록 해준다

 

컴포지션 클래스 패키지 위치

컴포지션을 사용하는 클래스, 특히 공통 의존성을 관리하는 클래스의 경우, 이를 어디에 위치시킬지는 그 용도와 프로젝트의 구조에 따라 달라질 수 있다

 

일반적으로는 다음과 같은 고려사항을 바탕으로 결정할 수 있다

  1. repository 패키지
    • 이 패키지는 데이터 액세스 레이어와 관련된 클래스를 보관하는 곳임
    • 컴포지션 클래스가 주로 데이터 저장소나 리포지토리와 직접적인 상호작용을 하는 경우, repository 패키지에 두는 것이 적절할 수 있다
    • 그러나 이러한 클래스가 리포지토리에만 국한되지 않고 다른 서비스나 컴포넌트와의 상호작용도 포함한다면, repository 패키지는 이상적인 위치가 아닐 수 있다
  2. util 패키지
    • 프로젝트 전반에 걸쳐 재사용 가능한 유틸리티나 헬퍼 함수를 포함하는 곳
    • 컴포지션 클래스가 범용적이고 다양한 컨텍스트에서 재사용될 수 있는 기능을 제공한다면, util 패키지가 적합할 수 있다
  3. service 패키지
    • 서비스 레이어의 클래스를 보관하는 service 패키지 내에 컴포지션 클래스를 두는 것도 고려할 수 있다
    • 이 클래스가 주로 서비스 레이어의 로직을 지원하는 역할을 한다면, service 패키지 내에 위치시키는 것이 자연스럽다
  4. config 패키지
    • 프로젝트의 구성 또는 설정과 관련된 클래스를 포함하는 config 패키지를 고려할 수도 있다
    • 컴포지션 클래스가 애플리케이션의 설정이나 초기화 과정에서 사용되며, 여러 서비스나 컴포넌트에서 공통적으로 의존하는 객체를 관리한다면 config 패키지가 적합할 수 있다

결론

컴포지션 클래스를 배치할 위치는 해당 클래스의 역할, 사용 범위, 프로젝트의 전체 구조를 고려하여 결정해야 한다

가장 중요한 것은 패키지 구조가 프로젝트의 명확성과 유지보수성을 높이는 방향으로 설계되어야 한다는 점으로, 컴포지션 클래스가 어느 범위까지 영향을 미치는지, 어떤 계층 또는 부분과 가장 밀접하게 연관되어 있는지를 기준으로 적절한 패키지를 선택하는 것이 좋다

 

DTO 클래스 내부에 여러 DTO 클래스 사용할지 DTO 클래스를 분리할지?

DTO 클래스를 설계할 때 여러 DTO를 하나의 클래스 내에 중첩해서 넣는 것과 각 DTO를 별도의 파일로 분리하는 것 사이에는 장단점이 있다

선택은 주로 프로젝트의 규모, 코드의 가독성, 재사용성, 유지보수의 용이성에 따라 달라진다

 

여러 DTO를 하나의 클래스 내에 넣는 경우

  • 장점
    • 관련성: 서로 관련된 DTO가 있다면, 하나의 파일 내에 묶어 관리함으로써 관련성을 명확하게 표현할 수 있다
    • 접근 용이성: 하나의 파일에서 여러 관련 DTO를 정의하면, 해당 DTO들을 사용하는 개발자가 여러 파일을 오가며 찾지 않아도 된다
  • 단점
    • 파일 크기: 하나의 파일에 너무 많은 DTO를 정의하게 되면 파일 크기가 커져서 관리가 어려워질 수 있다
    • 가독성: 파일 내에 많은 클래스가 있으면 가독성이 떨어질 수 있다

각 DTO를 별도의 파일로 분리하는 경우

  • 장점
    • 재사용성과 모듈성: 각 DTO가 별도의 파일로 분리되어 있으면 재사용성과 모듈성이 향상된다
    • 유지보수: 각 DTO가 별도의 파일에 있으면 유지보수가 용이하며, 변경 사항이 다른 DTO에 미치는 영향을 최소화할 수 있다
    • 파일 크기 관리: 파일 하나당 하나의 DTO를 두면 파일 크기를 작게 유지할 수 있어 관리가 용이하다
  • 단점
    • 파일 수: 많은 수의 DTO가 있을 경우, 그만큼 많은 파일이 생성되어 프로젝트의 구조가 복잡해질 수 있다

 

비즈니스 로직에 대한 검증 클래스

비즈니스 로직에 대한 검증을 수행하는 클래스를 만들고, 이를 서비스 로직에서 호출해서 사용하려는 경우, 다음 사항들을 고려해야 한다

  1. 클래스의 책임과 명확성: 검증 클래스는 비즈니스 로직과 관련된 유효성 검사를 명확하고 효과적으로 수행해야 한다, 이는 SOLID 원칙 중 하나인 단일 책임 원칙(SRP)을 따르는 것이 좋은데, 검증 클래스는 오로지 비즈니스 규칙에 대한 검증만을 담당해야 한다
  2. 의존성 주입 사용: 검증 클래스는 스프링의 의존성 주입(Dependency Injection) 메커니즘을 사용하여 서비스 로직에 주입되어야 한다, 이를 위해 @Service 또는 @Component 애너테이션을 사용하여 스프링 컨테이너에 의해 관리될 수 있도록 해야 한다
  3. 재사용성과 확장성: 검증 로직은 재사용 가능하고 확장성을 고려하여 설계되어야 한다, 이는 미래의 비즈니스 요구 사항 변경에 유연하게 대응할 수 있게 해준다
  4. 예외 처리: 검증 과정에서 발견된 오류는 적절한 예외 처리를 통해 관리되어야 한다, 이를 통해 서비스 로직에서 검증 실패 시 적절한 사용자 피드백을 제공할 수 있다

 

게임 상태 초기화

게임 상태를 초기화를 고려할 때 다음 사항을 확인하고 필요에 따라 수정해야 한다

  1. 게임 상태의 안정성: 게임의 모든 관련 상태가 초기화되어야 한다. 예를 들어, 특정 게임 로직에 따라 추가적인 상태 정보가 있을 경우(예: 특별한 규칙, 시간 제한, 게임 모드 등), 해당 정보도 초기화 과정에 포함해야 한다
  2. 연관된 엔티티의 초기화: Game 엔티티 외에도 게임 진행에 사용된 다른 엔티티나 컴포넌트가 있다면, 이들의 상태도 적절히 초기화되어야 한다
  3. 플레이어 상태 초기화: 각 플레이어의 상태(예: 임시 점수, 상태 표시기, 플레이어가 게임에서 사용한 리소스 등)도 리셋해야 할 수 있다
  4. 게임의 논리적 일관성: 초기화 로직이 게임의 논리적 일관성을 유지하는지 확인해야 한다. 예를 들어, resetGame 메서드가 호출되는 시점과 상황이 게임의 전체 흐름과 일치하는지 확인해야 한다

게임의 전체적인 로직과 요구 사항을 잘 이해하고 그에 맞춰 필요한 부분을 추가하거나 수정하는 것이 중요하다

 

게임 종료 로직 처리

로직 설계

  1. endRound 로직에서 라운드가 3일 때 endGame을 호출하도록 메서드를 구성하고 클라이언트에 endRound의 정보를 반환하고 추가적으로 endGame의 정보를 반환하도록 처리
  2. endRound 로직에서 라운드가 3일 때 endRound의 반환 값으로 endGame을 호출할 수 있도록 상태 값 추가하고 gameController를 통해 endGame을 호출하도록 처리

endRound 메서드 내에서 endGame을 직접 호출하는 것은 일반적으로 좋지 않은 설계이다. 이는 두 메서드 간의 역할이 명확하게 분리되어 있지 않고, endRound가 자신의 책임을 넘어서는 작업을 수행하기 때문이다, endRound는 한 라운드의 종료 로직을 처리하는 데 초점을 맞추고, endGame은 게임 자체의 종료 로직을 처리해야 한다

 

라운드가 3일 때 게임을 종료해야 한다는 로직을 처리하는 적절한 방법

  1. endRound에서 게임 상태 변경: endRound 메서드에서 라운드 수를 확인하고, 라운드가 3일 때 EndRoundResponse의 gameState 값을 게임 종료 상태(예: "END" 또는 "GAME_OVER" 등)로 설정한다. 이렇게 하여 클라이언트 측에서 게임의 상태를 인식하고 적절한 조치를 취할 수 있게 한다
  2. 컨트롤러에서 endGame 호출: 클라이언트 또는 게임 컨트롤러에서 endRound의 응답을 확인한 후, 게임 상태가 종료 상태라면 endGame 메서드를 호출한다. 이렇게 하여 라운드 종료와 게임 종료 로직 사이의 책임을 명확히 분리하고, 각각의 메서드가 자신의 역할에만 집중하도록 한다

이 방식으로 endRound와 endGame 각각의 책임과 역할을 명확히 하여, 코드의 유지보수성과 가독성을 향상시킬 수 있으며, 게임의 라이프사이클을 좀 더 명확하게 관리하게 한다

 

서비스 내에서 다른 서비스 메서드 호출

서비스 계층 내에서 다른 서비스 메서드를 호출하는 것은 일반적인 패턴이다, 이렇게 하면 관련된 비즈니스 로직이 한 곳에 집중되어 코드의 응집성이 높아지고, 유지보수와 테스트가 용이해진다

 

이점

  • 코드 재사용: 공통 비즈니스 로직을 서비스 계층에서 처리함으로써 코드 중복을 줄이고 재사용성을 높일 수 있다
  • 로직 분리: 다양한 컨트롤러에서 같은 비즈니스 로직을 필요로 할 때 서비스 메서드를 호출하기만 하면 되므로, 로직이 중복되는 것을 방지하고 수정이 필요할 때 한 곳에서 관리할 수 있다
  • 테스트 용이성: 비즈니스 로직이 서비스 계층에 명확하게 구현되어 있으면 단위 테스트를 작성하기가 더 쉬워진다

이러한 설계를 할 때는 서비스 계층에서 다른 서비스나 컴포넌트와의 의존성이 적절히 관리되고 있는지 주의 깊게 고려해야 한다. 필요한 경우 의존성 주입을 통해 다른 서비스나 컴포넌트를 활용할 수 있다

 

서비스 로직에서 클라이언트의 특정 행동을 요구하는 경우 처리 방법

서비스 로직에서 직접적으로 페이지 전환을 수행하는 API 호출을 하는 것은 일반적으로 권장되지 않는다. 서비스 계층은 비즈니스 로직을 처리하는 역할에 집중해야 하며, UI 로직이나 페이지 전환과 같은 프론트엔드의 책임을 수행해서는 안 된다. 서비스 계층과 프론트엔드(또는 클라이언트) 계층 사이의 관심사는 명확하게 분리되어야 한다

 

처리 방식

  1. 서비스 계층에서 상태 반환: 서비스 계층은 사용자의 액션 또는 시스템의 상태에 기반하여 특정 상태 정보나 결과만을 반환하고, 실제 페이지 전환 로직은 클라이언트 측(프론트엔드)에서 처리한다
  2. 클라이언트 측에서 처리: 클라이언트(예: 웹 프론트엔드, 모바일 앱)는 서비스로부터 받은 응답을 기반으로 적절한 페이지로 이동하거나 사용자 인터페이스를 업데이트하는 로직을 실행한다
  3. 이벤트 기반 처리: 어떤 시스템에서는 서버 측에서 이벤트를 발생시키고, 클라이언트 측에서는 이 이벤트를 구독하여 적절한 액션(예: 페이지 전환)을 수행하는 방식으로 처리한다. WebSocket이나 Server-Sent Events (SSE) 등이 이러한 상호작용을 지원한다

예를 들어, 서버 측에서는 게임 상태나 유저 선택에 따른 결과만을 전달하고, 클라이언트 측에서는 이 정보를 바탕으로 사용자에게 로비 페이지나 게임 방 페이지로의 전환을 제안하거나 자동으로 수행할 수 있다

이점

  • 관심사의 분리: 서버는 비즈니스 로직과 상태 관리에 집중하고, 클라이언트는 사용자 인터페이스와 사용자 경험에 집중한다
  • 재사용성과 유지보수성: 서버에서 전달하는 상태값을 다양한 클라이언트가 재사용할 수 있고, 클라이언트별 특화 로직은 각각에서 관리되므로 유지보수와 확장이 용이하다
  • 강력한 디커플링: 서버와 클라이언트 간의 결합도가 낮아져, 각각 독립적으로 개발하고 테스트할 수 있다

 

메서드가 void 타입인 경우 클라이언트에 필요한 데이터 반환 방법

  1. 비동기 이벤트 또는 메시지 사용: 웹소켓, SSE(Server-Sent Events), 또는 다른 비동기 메시징 시스템을 사용하여 클라이언트에 상태 변경을 알릴 수 있다. 이 방법을 통해 서버는 상태 변경을 클라이언트에 푸시할 수 있고, 클라이언트는 이 정보를 기반으로 적절한 행동을 취할 수 있다
  2. 상태 확인 API 제공: 클라이언트가 주기적으로 또는 특정 이벤트 발생 시 서버에 요청을 보내 상태를 확인할 수 있는 API를 제공할 수 있다. 이 API는 현재 게임 룸의 상태, 사용자 선택 결과 등 필요한 정보를 클라이언트에 반환한다
  3. 서비스 메서드의 반환 값 사용: 메서드가 void가 아니라 특정 결과나 상태를 나타내는 객체를 반환하도록 변경할 수 있다. 예를 들어, 사용자의 선택에 다른 결과를 포함한느 객체를 반환하여 클라이언트가 이를 활용할 수 있게 한다

 

서버에서 상태 값만 반환하고 클라이언트에서 이를 기반으로 연관된 API를 호출하는 방식

클라이언트와 서버 간의 통신을 간소화하고 클라이언트의 역할을 강화하는 전략으로 이 방식은 특히 클라이언트가 더 동적으로 서버와 상호작용해야 하는 경우 유용할 수 있다

 

장점

  • 유연성: 클라이언트는 서버로부터 상태 정보를 받고, 이를 기반으로 필요한 액션을 결정하며 다양한 시나리오에 빠르게 대응할 수 있다
  • 분리된 관심사: 서버는 상태 정보를 제공하는 역할에 집중하고, 클라이언트는 사용자 인터페이스와 사용자 경험을 관리하는 역할을 수행한다
  • 확장성: 클라이언트에서 다양한 액션을 결정하고 처리할 수 있으므로, 새로운 기능을 추가하거나 변경할 때 서버의 로직을 수정하지 않아도 된다

단점

  • 복잡성 증가: 클라이언트가 더 많은 로직을 처리해야 하므로, 클라이언트의 복잡성이 증가할 수 있다
  • 종속성 관리: 클라이언트가 서버의 상태 코드에 따라 다르게 동작해야 하므로, 서버와 클라이언트 간의 종속성을 잘 관리해야 한다
  • 네트워크 오버헤드: 클라이언트가 추가적인 API 호출을 수행해야 하므로, 네트워크 오버헤드가 증가할 수 있다

이러한 접근 방식을 사용할 때는 클라이언트와 서버 간의 명확한 계약(예: API 스펙, 상태 코드 정의)을 설정하고 유지하는 것이 중요하다. 이를 통해 클라이언트가 서버의 응답을 올바르게 이해하고 적절하게 반응할 수 있도록 해야 한다

 

따라서, 이 방식이 애플리케이션의 구조와 목표에 적합하다고 판단되면 채택할 수 있다. 그러나 클라이언트의 로직과 서버의 응답 사이의 의존성을 잘 관리하고, 시스템의 전체적인 복잡성과 확장성을 고려하는 것이 중요하다

 

유저의 선택 처리 방식

  1. 유저의 선택을 한 번에 처리하는 경우:
    • 컨트롤러에서 한 번의 요청으로 두 유저의 선택을 모두 받을 수 있다. 예를 들어, GameSessionService에 processUserChoices 메서드를 호출할 때, 두 유저의 선택을 포함하는 DTO 객체를 전달한다
    • 이 경우, 클라이언트는 두 유저의 선택을 모아서 서버에 전송해야 하며, 서버는 이 정보를 바탕으로 처리를 진행한다
  2. 유저의 선택을 따로 받고 서비스 단에서 합쳐서 처리하는 경우:
    • 컨트롤러는 각 유저의 선택을 개별적으로 받을 수 있고, 서비스 계층에서 이러한 개별 선택을 합쳐서 로직을 수행할 수 있다
    • 이 경우, 서버는 유저의 선택을 추적하고 상태를 관리해야 한다. 예를 들어, 게임방 상태를 관리하는 컴포넌트에서 각 유저의 선택을 기록하고 모든 유저의 선택이 완료됐을 때 처리를 진행할 수 있다

일반적으로, 실시간 상호작용이 필요한 게임에서는 유저의 선택을 즉각적으로 처리해야 하므로, 각 유저의 선택이 서버에 도달하면 즉시 이를 처리하는 방식을 선호할 수 있다. 그러나 모든 유저의 선택을 모아서 처리해야 하는 로직이 있을 경우, 상태 관리 로직을 통해 유저의 선택을 적절히 조율하고 합쳐서 처리하는 방식이 필요할 수 있다

따라서 구현 방식은 애플리케이션의 요구 사항과 특정 상황에 따라 결정되어야 하며, 어떤 방식이든 각 유저의 선택을 효과적으로 관리하고 처리할 수 있는 로직을 구현하는 것이 중요하다

 

턴 정보 관리 방식

  1. 턴 관리자 클래스 도입:
    • 별도의 TurnManager 클래스를 만들어 모든 턴 관련 로직을 이 클래스에 캡슐화한다. 이 클래스는 현재 턴, 다음 턴, 턴 순서 변경 등을 관리할 수 있다
    • TurnManager는 User 객체의 리스트나 큐를 사용하여 턴 순서를 관리하며, 필요에 따라 턴을 업데이트하고 조회하는 메서드를 제공한다
  2. 상태 패턴 사용:
    • 게임의 상태(예: 턴 상태)를 관리하는 데 상태 패턴을 사용합니다. 각 플레이어의 턴을 상태로 나타내고, 게임의 상태를 전환함으로써 턴을 관리할 수 있다
    • 이 방식은 게임의 다양한 상태(예: 시작, 진행, 종료)를 명확하게 관리할 수 있으며, 각 상태에서 허용되는 행동을 정의할 수 있다
  3. 이벤트 기반 턴 관리:
    • 게임의 턴을 이벤트로 처리하고, 턴이 변경될 때마다 이벤트를 발생시켜 턴 관련 처리를 한다
    • 이벤트 기반 접근 방식은 턴의 변화를 유연하게 처리할 수 있으며, 다양한 게임 로직에 쉽게 통합할 수 있다
  4. 플레이어 큐 사용:
    • 참여하는 모든 플레이어를 큐에 넣고, 큐에서 플레이어를 순차적으로 꺼내 턴을 처리한다. 턴이 끝나면 플레이어를 큐의 뒤로 다시 넣어 순환 구조를 만든다
    • 이 방식은 플레이어의 순서가 일정하고 반복되는 게임에서 특히 유용하다

확장성 측면에서는 게임의 요구사항에 따라 턴 관리 방식을 선택해야 한다. 예를 들어, 플레이어 수가 변동적이거나 게임의 규칙이 복잡한 경우, 턴 관리 로직을 유연하고 확장 가능하게 설계하는 것이 중요하다. 위의 방식들을 적절히 조합하거나 변형하여 사용할 수도 있다

 

베팅 로직 개선

베팅 과정에서 플레이어 간의 상호작용이 있고, 특히 RAISE와 같은 액션이 베팅 순서에 영향을 미치는 경우, 턴 관리는 다음 플레이어로 단순히 넘어가는 것보다 더 동적이고 상호적인 방식으로 처리되어야 한다. 이런 상황을 관리하기 위해, 턴의 순환과 플레이어 간의 상호작용을 효과적으로 처리할 수 있는 로직이 필요하다

  1. 턴 큐 사용:
    • 플레이어를 순서대로 큐에 넣는다. 턴이 끝날 때마다 큐에서 플레이어를 꺼내어 해당 플레이어의 행동을 처리하고, 행동이 끝나면 큐의 끝에 다시 추가한다
    • RAISE와 같은 액션이 발생하면, 해당 플레이어의 액션을 처리한 후 다시 선턴 플레이어에게 턴이 돌아가도록 큐를 조정한다
  2. 턴 상태 관리:
    • 각 플레이어의 상태(예: 대기 중, 액션 진행 중, 액션 완료)를 관리한다
    • 플레이어가 RAISE를 하면, 다른 플레이어에게도 반응할 기회를 주어야 한다. 이 때, 선턴 플레이어부터 순서대로 다시 액션을 취할 수 있도록 한다
  3. 턴 로직 상세화:
    • RAISE 액션이 발생했을 때, 모든 플레이어가 그에 대해 반응할 때까지 턴을 계속 순환시킨다
    • 각 플레이어가 CHECK 또는 RAISE를 한 후, 모든 플레이어가 동의하거나 모든 포인트를 베팅할 때까지 턴을 순환시킨다
  4. 이벤트 기반 턴 관리:
    • 플레이어의 각 액션을 이벤트로 처리하고, RAISE 같은 액션이 발생했을 때, 이에 대한 적절한 반응을 유도하기 위한 이벤트를 발생시킨다
    • 이를 통해 게임 로직이 더 동적이고 상호작용적으로 진행될 수 있다

이러한 턴 관리 방식은 게임의 베팅 로직이 더 복잡하고 동적인 상호작용을 포함할 때 효과적이다. 각 플레이어가 서로의 베팅에 반응하고 전략을 수정할 수 있는 기회를 제공하여, 게임의 전략적 깊이와 상호작용성을 향상시킬 수 있다

 

턴 정보를 명확하게 추적하고, 서비스 간에 효율적으로 공유할 수 있는 구조

방법

  1. 게임 상태 객체 사용:
    • 게임의 현재 상태(턴 정보 포함)를 저장하는 별도의 객체(예: GameState)를 사용한다. 이 객체는 현재 턴이 누구인지, 각 플레이어의 상태(예: 현재 베팅액, 잔여 포인트 등) 등을 포함한다
    • StartRoundService에서는 GameState를 초기화하고, GamePlayService는 이 객체를 참조하여 해당 턴에서 필요한 작업을 수행한다
  2. 데이터베이스에 턴 정보 저장:
    • 턴 정보를 데이터베이스에 저장하고, 각 서비스에서는 데이터베이스를 조회하여 현재 턴 상태를 확인한다. 이 방법은 서버 재시작 시에도 게임 상태를 유지할 수 있는 장점이 있다
    • Game 엔티티 내에 현재 턴을 가리키는 필드(예: currentTurnUserId)를 추가하고, 턴이 변경될 때마다 이 필드를 업데이트한다
  3. 세션 또는 캐시 사용:
    • 메모리 내 세션 또는 캐시(예: Redis, In-Memory Database)를 사용하여 턴 정보와 같은 게임 상태를 저장하고 관리한다. 이는 빠른 접근 속도를 제공하며, 분산 시스템에서도 유용하게 사용할 수 있다
    • 각 서비스에서는 세션 또는 캐시에서 현재 게임의 상태를 조회하여 처리를 진행한다
  4. 이벤트 기반 통신:
    • 이벤트 기반 아키텍처를 사용하여, 턴이 변경될 때 이벤트를 발생시키고, 관련된 서비스가 이 이벤트를 구독하여 필요한 로직을 수행한다
    • 예를 들어, StartRoundService에서 턴 시작 이벤트를 발행하고, GamePlayService에서 이를 구독하여 턴에 따른 로직을 실행한다

턴 정보를 관리하는 방식을 결정할 때는 시스템의 요구 사항, 확장성, 유지보수성 등을 고려하여 선택해야 한다, 서비스 간의 의존성을 최소화하고, 시스템 전체의 복잡성을 관리하기 쉬운 수준에서 설계하는 것이 중요하다

 

턴 세션 관리 방식

세션 관리 방식은 웹 애플리케이션에서 사용자별로 데이터를 유지하고 관리하는 데 주로 사용된다. 이 방법을 게임의 턴 관리에 적용하려면, 각 사용자의 게임 상태, 특히 턴 정보를 세션에 저장하고, 게임 동안 이 정보를 유지하여 필요할 때마다 접근할 수 있도록 해야 한다

 

  1. 세션 객체 초기화:
    • 게임이 시작할 때, 또는 사용자가 게임에 접속할 때 세션 객체를 초기화하고 필요한 게임 정보(예: 사용자 ID, 턴 상태 등)를 저장한다
  2. 턴 정보 저장:
    • 게임의 각 라운드나 턴이 시작할 때, 현재 턴의 사용자 정보나 턴 순서를 세션에 저장한다
    • 예를 들어, StartRoundService에서 라운드를 시작하며 누가 선턴인지 정보를 세션에 저장할 수 있다
  3. 세션에서 정보 읽기:
    • GamePlayService와 같은 다른 서비스에서는 세션에서 이 정보를 읽어 현재 게임 상태를 파악하고, 해당 턴에서 수행해야 할 작업을 결정한다
  4. 세션 유지:
    • 사용자가 게임에 연결된 동안 세션을 유지한다. 사용자가 게임을 떠나거나 세션 타임아웃이 발생하면 세션은 만료된다

예제

// 세션에 턴 정보 저장
public void startRound(HttpServletRequest request) {
    HttpSession session = request.getSession(true);
    Turn turnInfo = new Turn(/* 게임 참가자 정보 */);
    session.setAttribute("turnInfo", turnInfo);
}

// 세션에서 턴 정보 읽기
public void playAction(HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    if (session != null) {
        Turn turnInfo = (Turn) session.getAttribute("turnInfo");
        // turnInfo를 사용하여 게임 로직 수행
    }
}
import java.util.List;

public class TurnManager {
    private List<String> players; // 플레이어 목록, 여기서는 플레이어를 예시로 문자열(ID 등)을 사용
    private int currentPlayerIndex = 0; // 현재 플레이어의 인덱스

    public TurnManager(List<String> players) {
        this.players = players;
        this.currentPlayerIndex = 0; // 게임 시작 시 첫 플레이어부터 시작
    }

    public void nextTurn() {
        // 다음 플레이어로 순서를 넘깁니다. 순환 구조로 관리
        currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
    }

    public String getCurrentPlayer() {
        return players.get(currentPlayerIndex);
    }
}

 

 

게임 상태 저장소 사용 방식

게임 상태 저장소를 사용하여 Turn 클래스를 관리하기 위해, 게임 상태를 저장하고 관리하는 중앙 저장소 또는 서비스가 필요하다. 이 저장소는 데이터베이스, 인메모리 데이터 스토어, 또는 다른 영속성 메커니즘을 사용하여 게임 상태를 저장할 수 있다

 

1. 게임 상태 관리 서비스 구현

게임의 모든 상태 정보를 관리할 수 있는 서비스 클래스를 구현, 이 클래스는 Turn 객체의 상태를 포함하여 게임의 전ㅊ ㅔ상태를 관리한다

예제

import java.util.HashMap;
import java.util.Map;

public class GameStatusService {
    private final Map<Long, Turn> gameTurns = new HashMap<>();

    public void initializeGame(Long gameId, List<User> players) {
        Turn turn = new Turn(players);
        gameTurns.put(gameId, turn);
    }

    public void nextTurn(Long gameId) {
        Turn turn = gameTurns.get(gameId);
        if (turn != null) {
            turn.NextTurn();
        }
    }

    public User getCurrentPlayer(Long gameId) {
        Turn turn = gameTurns.get(gameId);
        if (turn != null) {
            return turn.getCurrentPlayer();
        }
        return null;
    }
}

 

2. 게임 상태 저장 및 복원

게임 상태를 영속화하는 메커니즘이 필요하다. 예를 들어, 게임 상태를 데이터베이스에 저장하고, 게임 시작 시 또는 특정 이벤트 발생 시 해당 상태를 복원할 수 있어야 한다

 

3. 서비스 통합

게임의 로직을 처리하는 부분에서 GameStatusService를 사용하여 현재 턴 상태를 관리하고 업데이트한다. 예를 들어, 게임의 각 행동에서 nextTurn 메서드를 호출하여 턴을 진행시키고, getCurrentPlayer 메서드를 사용하여 현재 플레이어를 가져온다

 

4. 상태 동기화 및 일관성

여러 서버 인스턴스나 스케일 아웃 환경에서 게임 상태의 일관성을 유지하려면, 분산된 상태 관리 솔루션(예: Redis, Hazelcast 등)을 사용하여 게임 상태 정보를 관리해야 할 수 있다.

이렇게 게임 상태 저장소를 사용하면, 게임의 턴 및 다른 상태 정보를 중앙에서 관리하고, 게임 전체에서 일관된 상태 정보에 접근하여 사용할 수 있다

 

턴 관리를 스타트 라운드 내에서 수행하는 이유

  1. 책임의 명확성: 라운드 시작과 턴 초기화는 게임 진행의 첫 단계로 서로 긴밀히 관련되어 있다. 이 두 작업을 같은 서비스 내에서 처리함으로써 로직의 연관성을 명확히 하고, 코드의 가독성과 관리성을 향상시킬 수 있다.
  2. 상태 일관성: 라운드 시작 시 턴 상태를 초기화하고, 해당 상태를 게임의 다른 부분에서 참조할 수 있도록 만든다. 이는 상태 관리의 일관성을 보장하며, 다른 서비스에서 상태를 변경할 때 발생할 수 있는 문제를 방지한다.
  3. 확장성과 유지보수성: 라운드와 턴 관리 로직을 함께 두면, 게임의 규칙이 변경되거나 추가 기능이 필요할 때, 관련 로직을 쉽게 찾아 수정할 수 있다

예시

public class StartRoundService {
    private GameStateService gameStateService; // 게임 상태 관리 서비스

    public StartRoundService(GameStateService gameStateService) {
        this.gameStateService = gameStateService;
    }

    public void startRound(Long gameId) {
        Game game = gameStateService.getGame(gameId);
        if (game == null) {
            // 게임 초기화 로직
            List<User> players = ...; // 플레이어 목록 초기화
            gameStateService.startGame(gameId, players);
        }

        game.initializeRound(); // 라운드 및 턴 초기화
        gameStateService.nextTurn(gameId); // 첫 턴 설정
    }
}

 

이 방식으로, StartRoundService는 라운드를 시작하는 동시에 턴 관리의 초기 단계를 수행하며, GameStateService는 게임의 전체 상태를 관리하는 중앙 서비스로 기능하다

요약하면, 스타트 라운드 서비스 로직에서 턴 관리를 수행하는 것은 게임의 라이프사이클과 흐름상 논리적이며, 관련 로직을 한 곳에서 관리할 수 있어 여러 면에서 효율적이다

 

 

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

CI / CD  (0) 2024.05.11
ORM  (0) 2024.05.03
프로젝트 코드 분석  (0) 2024.04.23
Reflection API  (0) 2024.04.23
POJO  (0) 2024.04.22