본문 바로가기

항해 99/Spring

WebSocket - 실제 코드 분석

웹 게임 프로젝트 관련 코드 분석 - with GPT4

분석 프로젝트 :

https://github.com/namoldak/Backend/tree/dev/src/main/java/com/example/namoldak

 

Config

WebSocketConfig

package com.example.namoldak.config;

import com.example.namoldak.util.webSocket.SignalHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.*;

// 기능 : 웹소켓 사용에 필요한 설정
@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocket
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer, WebSocketConfigurer {

    // 웹 소켓 연결을 위한 엔드포인트 설정 및 stomp sub/pub 엔드포인트 설정
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // stomp 접속 주소 url => /ws/chat
        registry.addEndpoint("/ws-stomp")      // 연결될 Endpoint
                .setAllowedOriginPatterns("*")  // CORS 설정
                .withSockJS()                   // SockJS 설정
                .setHeartbeatTime(1000);        // 연결상태 확인 주기
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/sub");  // 구독한 것에 대한 경로
        config.setApplicationDestinationPrefixes("/pub");   //
    }

    // 웹 소켓 버퍼 사이즈 증축
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit(160 * 64 * 1024); // default : 64 * 1024
        registration.setSendTimeLimit(100 * 10000); // default : 10 * 10000
        registration.setSendBufferSizeLimit(3* 512 * 1024); // default : 512 * 1024
    }

    @Bean
    public WebSocketHandler signalHandler() {
        return new SignalHandler();
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(signalHandler(), "/signal")
                .setAllowedOrigins("*")
                .setAllowedOrigins("http://localhost:3000", "https://namoldak.com")
                .withSockJS(); // allow all origins
    }
}
  • Spring 프레임워크를 사용하여 웹소켓 기반의 통신을 구성하기 위한 설정 클래스
    • 웹소켓 통신을 위한 라우팅, 보안, 세션 관리 등을 구성하는 중요한 역할 수행
  • 클래스 선언 및 어노테이션 사용 : @Configuration, @EnableWebSocketMessageBroker, @EnableWebSocket, @Slf4j 어노테이션을 사용해 웹소켓 설정 및 로깅 기능을 활성화하고, Spring의 구성요소로 정의
  • STOMP 엔드포인트 설정 : registerStompEndpoints( ) 메서드는 클라이언트가 웹소켓 서버에 연결할 수 있는 주소('/ws-stomp')를 설정하고 CORS 설정과 SockJS  지원을 통해 다양한 브라우저와의 호환성을 제공함
  • 메시지 브로커 구성 : configureMessageBroker( ) 메서드는 클라이언트가 메시지를 구독할 수 있는 경로('/sub')와 메시지를 발행할 수 있는 경로('/pub')를 설정하여 메시지 브로커를 구성함
  • 웹소켓 트랜스포트 설정 : configureWebSocketTransport( ) 메서드는 메시지 크기, 전송 시간 제한, 버퍼 사이즈 등 웹소켓 연결의 세부 트랜스포트 설정을 조정함
  • 핸들러 정의 및 매핑 : signalHandler( ) 빈을 정의하여 웹소켓 핸들러를 구현하고, registerWebSocketHandler( ) 메서드에서 이 핸들러를 경로('/signal')에 매핑함, CORS 정책과 SockJS 설정도 이 부분에서 관리됨

WebSecurityConfig

package com.example.namoldak.config;

import com.example.namoldak.util.jwt.JwtAuthFilter;
import com.example.namoldak.util.security.SecurityExceptionFilter;
import com.example.namoldak.util.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Collections;

// 기능 : Spring Security 사용에 필요한 설정
@Configuration
@Slf4j
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
    private final JwtUtil jwtUtil;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        try {
            // CSRF 설정
            http.csrf().disable();

            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

            http.httpBasic().disable()
                    .authorizeRequests()
                    .antMatchers("/auth/**").permitAll()
                    .antMatchers(HttpMethod.GET, "/posts/myPost").authenticated()
                    .antMatchers(HttpMethod.GET, "/**").permitAll()
                    .antMatchers("/ws-stomp").permitAll()
                    .antMatchers("/signal/**").permitAll()
                    .antMatchers("/signal").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .addFilterBefore(new JwtAuthFilter(jwtUtil),
                            UsernamePasswordAuthenticationFilter.class)
                    .addFilterBefore(new SecurityExceptionFilter(), JwtAuthFilter.class);
            http.cors();
        } catch (Exception e) {
            log.info(e.getMessage());
        }
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {

        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("http://localhost:3000");
        config.addAllowedOrigin("https://namoldak.com.s3.ap-northeast-2.amazonaws.com");
        config.addAllowedOrigin("https://namoldak.com");
        config.addAllowedOrigin("https://d3j37rx7mer6cg.cloudfront.net");
        // addExposedHeader : 프론트에서 헤더의 해당 값에 접근할 수 있게 허락해주는 옵션
        config.addExposedHeader(JwtUtil.ACCESS_TOKEN);
        config.addExposedHeader(JwtUtil.REFRESH_TOKEN);
        config.addExposedHeader(JwtUtil.KAKAO_TOKEN);
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowedOriginPatterns(Collections.singletonList("*"));
        config.setAllowCredentials(true);
        config.validateAllowCredentials();
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return source;
    }
}
  • Spring Security를 활용한 보안 설정을 정의하는 클래스
    • 웹 애플리케이션의 보안 관련 주요 설정을 담당하며, 사용자 인증 및 권한 부여, CORS 정책, HTTP 요청 보안 등의 기능을 구성하는 데 중요한 역할을 수행
  • 보안 구성 어노테이션 사용 : @EnableWebSecurity와 @EnableGlobalMethodSecurity(securedEnabled = true) 어노테이션을 사용하여 웹 보안과 메서드 수준의 보안을 활성화함
  • 비밀번호 인코더 빈 설정 : passwordEncoder( ) 메서드를 통해 BCrypt 알고리즘을 사용하는 PasswordEncoder의 인스턴스를 생성하고 빈으로 등록함
  • 보안 필터 체인 정의 : securityFilterChain(HttpSecurity http) 메서드에서는 다음과 같은 보안 관련 설정을 수행함
    • CSRF(Cross-Site Request Forgery) 공격 방지 기능을 비활성화함
    • 세션 관리 전력을 STATELESS로 설정하여 세션을 사용하지 않고 각 요청을 독립적으로 처리함
    • HTTP Basic 인증을 비활성화하고, 특정 경로에 대한 접근 권한을 설정함
    • JWT(JSON Web Token) 인증 필터를 UsernamePasswordAuthenticationFilter 전에 추가하여 인증 과정을 커스텀함
  • CORS(Cross-Origin Resource Sharing) 설정 : corsConfigurationSource( ) 메서드에서는 CORS 정책을 정의하여, 서로 다른 도메인 간의 리소스 공유를 허용하는 설정을 구성
  • JWT 관련 필터 추가 : JwtAuthFilter와 SecurityExceptionFilter를 사용하여 JWT를 통한 인증 과정을 처리하고, 보안 관련 예외를 처리함

RedisConfig

package com.example.namoldak.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

// 기능 : Redis 사용에 필요한 설정
@EnableRedisRepositories
@Configuration
public class RedisConfig {

    // Redis 연결
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory();
        return lettuceConnectionFactory;
    }

    // Redis 데이터 처리에 사용하는 템플릿
    @Bean
    public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 직렬화
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}
  • Redis 데이터베이스 연결 및 작업을 위한 설정을 제공
    • Redis와의 통신을 위한 기본적인 연결 설정과 데이터 직렬화 방식을 정의하며, Spring 애플리케이션에서 Redis를 사용하기 위한 기반을 마련
  • @EnableRedisRepositories 어노테이션 :  Spring Data Redis의 리포지토리 지원을 활성화함, 이를 통해 Redis 연산을 수행하는 데 필요한 리포지토리 액세스를 자동으로 구성
  • RedisConnectionFactory 빈 설정 : redisConnectionFactory( ) 메서드는 Redis 서버에 연결을 관리하는 RedisConnectionFactory 인스턴스를 생성함, LettuceConnectionFactory를 사용하여 연결 팩토리를 구현하며, Lettuce는 비동기 이벤트 기반의 Redis 클라이언트 라이브러리임
  • RedisTemplate 빈 설정 : redisTemplate( ) 메서드는 Redis 데이터 작업을 위한 RedisTemplate 객체를 생성하고 구성함, 이 템플릿을 통해 Redis 데이터베이스와의 상호 작용이 이루어짐
    • setConnectionFactory( ) : Redis 연결을 관리하기 위해 RedisConnectionFactory를 RedisTemplate에 설정함
    • setKeySerializer( )와 setValueSerializer( ) : 데이터 저장 및 조회 시 사용할 키와 값의 직렬화 방식을 설정함, StringRedisSerializer를 사용하여 문자열 데이터를 처리함

 

  •  

Util

webSocket/SignalHandler

package com.example.namoldak.util.webSocket;

import com.example.namoldak.repository.SessionRepository;
import com.example.namoldak.dto.RequestDto.WebSocketMessage;
import com.example.namoldak.dto.ResponseDto.WebSocketResponseMessage;
import com.example.namoldak.service.GameRoomService;
import com.example.namoldak.util.GlobalResponse.CustomException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import javax.transaction.Transactional;
import java.io.IOException;
import java.util.*;

import static com.example.namoldak.util.GlobalResponse.code.StatusCode.SESSION_ROOM_NOT_FOUND;

// 기능 : WebRTC를 위한 시그널링 서버 부분으로 요청타입에 따라 분기 처리
@Slf4j
@Component
public class SignalHandler extends TextWebSocketHandler {

    @Autowired
    private GameRoomService gameRoomService;
    private final SessionRepository sessionRepository = SessionRepository.getInstance();  // 세션 데이터 저장소
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final String MSG_TYPE_JOIN_ROOM = "join_room";
    private static final String MSG_TYPE_OFFER = "offer";
    private static final String MSG_TYPE_ANSWER = "answer";
    private static final String MSG_TYPE_CANDIDATE = "candidate";

    @Override
    public void afterConnectionEstablished(final WebSocketSession session) {
        // 웹소켓이 연결되면 실행되는 메소드
    }

    // 시그널링 처리 메소드
    @Override
    protected void handleTextMessage(final WebSocketSession session, final TextMessage textMessage) {

        try {
            WebSocketMessage message = objectMapper.readValue(textMessage.getPayload(), WebSocketMessage.class);
            String userName = message.getSender();
            Long roomId = message.getRoomId();

            switch (message.getType()) {
                // 처음 입장
                case MSG_TYPE_JOIN_ROOM:

                    if (sessionRepository.hasRoom(roomId)) {
                        // 해당 챗룸이 존재하면
                        // 세션 저장 1) : 게임방 안의 session List에 새로운 Client session정보를 저장
                        sessionRepository.addClient(roomId, session);
                    } else {
                        // 해당 챗룸이 존재하지 않으면
                        // 세션 저장 1) : 새로운 게임방 정보와 새로운 Client session정보를 저장
                        sessionRepository.addClientInNewRoom(roomId, session);
                    }

                    // 세션 저장 2) : 이 세션이 어느 방에 들어가 있는지 저장
                    sessionRepository.saveRoomIdToSession(session, roomId);

                    // 세션 저장 3) : 방 안에 닉네임들 저장
                    sessionRepository.addNicknameInRoom(session.getId(), message.getNickname());

                    Map<String, WebSocketSession> joinClientList = sessionRepository.getClientList(roomId);

                    // 방안 참가자 중 자신을 제외한 나머지 사람들의 Session ID를 List로 저장
                    List<String> exportSessionList = new ArrayList<>();
                    for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
                        if (entry.getValue() != session) {
                            exportSessionList.add(entry.getKey());
                        }
                    }

                    // 방안 참가자들 닉네임 List
                    Map<String, String> exportNicknameList = new HashMap<>();
                    for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
                        if (entry.getValue() != session) {
                            exportNicknameList.put(entry.getKey(), sessionRepository.getNicknameInRoom(entry.getKey()));
                        }
                    }

                    // 접속한 본인에게 방안 참가자들 정보를 전송
                    sendMessage(session,
                            new WebSocketResponseMessage().builder()
                                    .type("all_users")
                                    .sender(userName)
                                    .data(message.getData())
                                    .allUsers(exportSessionList)
                                    .allUsersNickNames(exportNicknameList)
                                    .candidate(message.getCandidate())
                                    .sdp(message.getSdp())
                                    .build());

                    break;

                case MSG_TYPE_OFFER:
                case MSG_TYPE_ANSWER:
                case MSG_TYPE_CANDIDATE:

                    if (sessionRepository.hasRoom(roomId)) {
                        Map<String, WebSocketSession> oacClientList = sessionRepository.getClientList(roomId);

                        if (oacClientList.containsKey(message.getReceiver())) {
                            WebSocketSession ws = oacClientList.get(message.getReceiver());
                            sendMessage(ws,
                                    new WebSocketResponseMessage().builder()
                                            .type(message.getType())
                                            .sender(session.getId())            // 보낸사람 session Id
                                            .senderNickName(message.getNickname())
                                            .receiver(message.getReceiver())    // 받을사람 session Id
                                            .data(message.getData())
                                            .offer(message.getOffer())
                                            .answer(message.getAnswer())
                                            .candidate(message.getCandidate())
                                            .sdp(message.getSdp())
                                            .build());
                        }
                    } else {
                        throw new CustomException(SESSION_ROOM_NOT_FOUND);
                    }
                    break;

                default:
                    log.info("======================================== DEFAULT");
                    log.info("============== 들어온 타입 : " + message.getType());
            }
        } catch (JsonProcessingException e) {
            log.info("=================== SignalHandler Json처리 에러 : {} ", e.getMessage());
        }
    }

    // 웹소켓 연결이 끊어지면 실행되는 메소드
    @Override
    @Transactional
    public void afterConnectionClosed(final WebSocketSession session, final CloseStatus status) {
        String nickname = sessionRepository.getNicknameInRoom(session.getId());
        // 끊어진 세션이 어느방에 있었는지 조회
        Long roomId = sessionRepository.getRoomId(session);

        // 1) 게임방에서 나가는 멤버 정보 정리 / 방장이 나가면 방장도 바꿈
        gameRoomService.exitGameRoomAboutSession(nickname, roomId);

        // 2) 방 참가자들 세션 정보들 사이에서 삭제
        sessionRepository.deleteClient(roomId, session);

        // 3) 별도 해당 참가자 세션 정보도 삭제
        sessionRepository.deleteRoomIdToSession(session);

        // 4) 별도 해당 닉네임 리스트에서도 삭제
        sessionRepository.deleteNicknameInRoom(session.getId());
        // 본인 제외 모두에게 전달
        for(Map.Entry<String, WebSocketSession> oneClient : sessionRepository.getClientList(roomId).entrySet()){
            sendMessage(oneClient.getValue(),
                    new WebSocketResponseMessage().builder()
                            .type("leave")
                            .sender(session.getId())
                            .receiver(oneClient.getKey())
                            .build());
        }
    }

    // 메세지 발송
    private void sendMessage(final WebSocketSession session, final WebSocketResponseMessage message) {
        try {
            String json = objectMapper.writeValueAsString(message);
            session.sendMessage(new TextMessage(json));
        } catch (IOException e) {
            log.info("============== 발생한 에러 메세지: {}", e.getMessage());
        }
    }
}
  • SignalHandler 클래스는 Spring에서 제공하는 TextWebSocketHandler를 상속하여 구현한 웹소켓 핸들러로, WebRTC 시그널링 서버의 역할을 수행
    • WebRTC 기반 통신을 구현하는 데 핵심적인 역할을 하며, 웹소켓을 통한 실시간 통신 환경에서의 세션 관리, 메시지 라우팅, 그리고 상태 관리 등을 책임짐
  • 웹소켓 연결 관리 : afterConnectionEstablished와 afterConnectionClosed 메서드를 통해 웹소켓 연결이 시작되거나 종료될 때 필요한 로직을 실행
  • 메시지 처리 : handleTextMessage 메서드는 클라이언트로부터 전달받은 텍스트 메시지를 처리함, 이 메서드는 WebRTC 시그널링 과정에서의 다양한 메시지 타입(예: join_room, offer, answer, candidate)을 처리하여 상황에 맞는 동작을 수행
  • 세션 및 방 관리 : 클라이언트의 세션 정보와 방 정보를 관리함, 예를 들어, 클라이언트가 방에 입장할 때('MSG_TYPE_JOIN_ROOM'), 세션 저정소에 클라이언트의 세션을 추가하고, 방 정보를 업데이트함
  • 통신 로직 구현 : WebRTC 연결을 위한 시그널링 데이터(예:SDP, ICE candidate)를 교환하는 과정에서 필요한 메시지를 생성하고, 해당 메시지를 클라이언트에게 전송
  • 예외 처리 : 메시지 처리 중 발생할 수 있는 예외를 로그로 기록하고, 적절한 예외 처리를 수행

converter/GameStartSetConverter

package com.example.namoldak.util.converter;

import com.example.namoldak.util.GlobalResponse.CustomException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;

import java.util.Map;

import static com.example.namoldak.util.GlobalResponse.code.StatusCode.JSON_PROCESS_FAILED;

// 기능 : 객체 전환
@Component
public class GameStartSetConverter {

    private ObjectMapper objectMapper = new ObjectMapper();
    //GameStartSet Map <-> String

    // DB에서 String 저장한 멤버와 keyword의 매칭을 Map으로 전환
    public Map<String, String> getMapFromStr(String keywordToMember) {
        try {
            Map<String, String> map = objectMapper.readValue(keywordToMember, new TypeReference<Map<String, String>>() {});
            return map;
        } catch (JsonProcessingException e) {
            throw new CustomException(JSON_PROCESS_FAILED);
        }
    }

    // DB에 String으로 저장하기 위해 멤버와 keyword의 매칭을 String으로 전환
    public String getStrFromMap(Map<String, String> keywordToMember) {
        try {
            String str = objectMapper.writeValueAsString(keywordToMember);
            return str;
        } catch (JsonProcessingException e) {
            throw new CustomException(JSON_PROCESS_FAILED);
        }
    }
}
  • 맵(Map) 데이터와 JSON 형태의 문자열 간 변환을 처리하는 유틸리티 컴포넌트
    • JSON 문자열과 맵 데이터 간 변환을 쉽게 하기 위한 목적으로 사용되며, 주로 데이터베이스에 복잡한 구조의 데이터를 저장하거나 읽을 때 유용함
    • 예외 처리를 통해 변환 과정에서 발생할 수 있는 오류를 관리하며, 오류 발생 시 적절한 예외 메시지와 함께 처리함
  • 맵 데이터를 JSON 문자열로 변환 : getStrFromMap 메서드는 맵 객체를 입력받아 JSON 형태의 문자열로 변환, 이 과정에서 ObjectMapper의 writeValueAsString 메서드를 사용하여 변환을 수행하며, 변환 중 예외가 발생할 경우 CustomException을 발생시킴
  • JSON 문자열을 맵 데이터로 변환 : getMapFromStr 메서드는 JSON 형태의 문자열을 입력받아 맵 객체로 변환, 이 때 ObjectMapper의 readValue 메서드를 사용하며, TypeReference를 통해 반환될 맵의 키와 값이 모두 문자열(String)임을 지정함, 변환 중 예외가 발생할 경우 CustomException을 발생 시킴

 

Domain

GameMessage

package com.example.namoldak.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

// 기능 : 채팅에 적용되는 관리자 메세지용 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class GameMessage<T> {

    private String roomId;
    private String senderId;
    private String sender;
    private String nickname;
    private T content;
    private GameMessage.MessageType type;

    public enum MessageType {
        JOIN, OWNER, ENTER, RULE, READY, START, KEYWORD, SPOTLIGHT,
        FAIL, SKIP, SUCCESS, WINNER, ENDGAME, FORCEDENDGAME,
        LEAVE, NEWOWNER, END, REWARD, STUPID
    }
}
  • 채팅 기능에서 사용되는 메시지를 표현하는 데 사용되는 데이터 전송 객체(DTO)
    • 게임 상황에 따른 다양한 메시지 유형을 관리하며, 채팅이나 게임 상태 변화에 따른 정보 전달에 사용됨, 제네릭을 활용하여 메시지 내용을 유연하게 처리할 수 있어, 다양한 타입의 데이터를 메시지로 전송할 수 있음
  • 필드
    • roomId : 메시지가 속한 방의 식별자
    • senderId : 메시지를 보낸 사용자의 식별자
    • sender : 메시지를 보낸 사용자의 이름 또는 아이디
    • nickname : 메시지를 보낸 사용자의 닉네임
    • content : 메시지의 내용으로, 제네릭 타입 'T'를 사용하여 다양한 타입의 내용을 지원함
    • type : 메시지의 종류를 나타내는 MessageType 열거형
  • 메서드
    • 클래스에는 생성자를 포함한 메서드는 Lombok 어노테이션을 통해 자동으로 생성 됨
  • MessageType 열거형
    • MessageType은 메시지 유형을 나타내며, 여러 가지 게임 상황에 맞는 상태 값들(JOIN, READY, START, END, LEAVE)을 포함, 이를 통해 메시지의 목적과 상황을 구분할 수 있음

GameRoom

package com.example.namoldak.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;

// 기능 : 게임룸 Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
public class GameRoom extends Timestamped{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gameRoomId;

    @Column(nullable = false)
    private String gameRoomName;
    @Column
    private String gameRoomPassword;

    @Column(nullable = false)
    private String owner;

    @Column(nullable = false)
    private boolean status;

    public void setOwner(String owner){
        this.owner = owner;
    }

    public void setStatus(boolean status) {
        this.status = status;
    }
}
  • 게임룸에 대한 엔티티(Entity)를 정의하며, 데이터베이스 테이블과 매핑됨
    • 게임룸의 기본적인 정보를 저장하고 관리하는 데 사용되며, 데이터베이스와의 상호작용을 위한 JPA 엔티티로서의 역할을 수행
  • 필드 정의
    • gameRoomId : 게임룸의 고유 식별자로, 자동으로 생성되는 값, @GeneratedValue 어노테이션을 사용해 ID가자동으로 생성되도록 설정
    • gameRoomName : 게임룸의 이름으로, nullable = false 속성을 통해 반드시 값을 가지도록 설정
    • gameRoomPassword : 게임룸의 비밀번호로, 비공개 방을 만들 때 사용할 수 있음
    • owner : 게임룸의 소유자(방장)를 나타내며, 이 필드 역시 반드시 값을 가지도록 설정
    • status : 게임룸의 상태(예: 활성화/비활성화)를 나타내며, nullable = false 속성을 통해 반드시 값이 설정되어야 함
  • Lombok 어노테이션 사용
  • 메소드 정의
    • setOwner : 게임룸의 소유자를 설정하는 메소드
    • setStatus : 게임룸의 상태를 설정하는 메소드

GameRoomAttendee

package com.example.namoldak.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;

// 기능 : 게임룸과 유저를 연결하는 중간 Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
public class GameRoomAttendee extends Timestamped{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gameRoomMemberId;

    @JoinColumn(name="gameroomid")
    @OneToOne(fetch = FetchType.LAZY)
    private GameRoom gameRoom;

    @JoinColumn(name="memberid")
    @OneToOne(fetch = FetchType.LAZY)
    private Member member;

    @Column(nullable = false)
    private String memberNickname;

    public GameRoomAttendee(GameRoom gameRoom, Member member){
        this.gameRoom       = gameRoom;
        this.member         = member;
        this.memberNickname = member.getNickname();
    }
}
  • 게임룸과 유저 간의 관계를 나타내는 중간 엔티티로서, 게임룸에 참여하는 각 유저의 정보를 매핑하는 데 사용됨
    • 게임룸과 유저 사이의 관계를 나타내며, 각 게임룸에 참여하는 유저들의 정보를 관리하는 중요한 역할을 함
  • 필드 정의
    • gameRoomMemberId : 참가자의 고유 식별자로, 데이터베이스에서 자동으로 생성되는 값
    • gameRoom : 게임룸 엔티티에 대한 참조로, @OneToOne 관계를 통해 게임룸 객체와 연결됨
    • member : 유저(멤버) 엔티티에 대한 참조로, @OneToOne 관계를 통해 유저 객체와 연결됨
    • memberNickname : 참가하는 멤버의 닉네임을 저장합니다. 이 필드는 nullable = false 속성을 가지므로 반드시 값을 설정해야 함
  • JPA 관계 매핑 : @JoinColumn(name = "gameroomid")과 @JoinColumn(name = "memberid") 어노테이션을 통해 외래 키 관계를 명시적으로 정의
  • 생성자 : GameRoomAttendee(GameRoom gameRoom, Member member)
    • 주요 정보를 받아 객체를 초기화하는 생성자로 게임룸 객체와 멤버 객체를 인자로 받아, 해당 엔티티 인스턴스의 필드를 설정
  • Lombok 어노테이션 사용

GameStartSet

package com.example.namoldak.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;

// 기능 : 게임에 필요한 세트 설정
@Getter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GameStartSet{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gameSetId;

    @Column(nullable = false)
    private Long roomId;

    @Column(nullable = false)
    private String category;

    @Column
    private String keywordToMember;     // JSON화

    @Column(nullable = false)
    private Integer round;

    @Column(nullable = false)
    private Integer spotNum;

    @Column
    private String winner;

    @Column
    private Long gameStartTime;

    public void setSpotNum(Integer num) {
        this.spotNum = num;
    }

    public void setRound(Integer round) {
        this.round = round;
    }

    public void setWinner(String winner) {
        this.winner = winner;
    }
}
  • 게임 시작 시 필요한 설정 정보를 담는 엔티티로 게임의 세션 설정을 데이터베이스에 저장하고 관리하기 위한 구조를 제공함
    • 게임 설정에 필요한 다양한 정보를 관리하는 중요한 역할을 수행하며, 게임 로직을 실행하는 데 필요한 기본 데이터를 제공
  • 필드 정의
    • gameSetId : 게임 세트의 고유 식별자로, 데이터베이스에서 자동으로 생성되는 값
    • roomId : 게임이 진행되는 방의 식별자
    • category : 게임의 카테고리를 나타내며, 이는 게임의 종류나 분류를 구분하는 데 사용됨
    • keywordToMember : 게임에 사용되는 키워드와 멤버 간의 매칭 정보를 JSON 문자열 형태로 저장함
    • round : 게임의 라운드 수를 나타냄
    • spotNum : 게임에서 중요한 위치나 역할을 나타내는 번호임
    • winner : 게임의 승자를 나타냄
    • gameStartTime : 게임의 시작 시간을 나타냄
  • 메서드 정의
    • setSpotNum, setRound, setWinner : 각각 게임의 중요 번호, 라운드 수, 승자를 설정하는 메서드임
  • JPA 설정
  • Lombok 어노테이션 사용

Member

package com.example.namoldak.domain;

import com.example.namoldak.dto.RequestDto.SignupRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;

// 기능 : 유저 정보 Entity
@Entity
@Table(name = "MEMBER")
@Getter
@NoArgsConstructor
public class Member extends Timestamped{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false, unique = true)
    private String nickname;

    @Column(nullable = false)
    private String password;

    @Column
    private Long kakaoId;

    @Column
    private Long winNum = 0L;

    @Column
    private Long loseNum = 0L;

    @Column
    private Long totalGameNum = 0L;

    @Column
    private Long enterGameNum = 0L;

    @Column
    private Long soloExitNum = 0L;

    @Column
    private Long makeRoomNum = 0L;

    @Column
    private Long playTime = 0L;

    public Member(String email, String nickname, String password) {
        this.email    = email;
        this.nickname = nickname;
        this.password = password;
    }

    public Member(String email, String password, Long kakaoId, String kakaoNickname){
        this.email     = email;
        this.password  = password;
        this.kakaoId   = kakaoId;
        this.nickname  = kakaoNickname;
    }

    public void update(SignupRequestDto signupRequestDto) {
        this.nickname = signupRequestDto.getNickname();
    }

    public void updateWinNum(Long num) {
        this.winNum += num;
    }

    public void updateLoseNum(Long num) {
        this.loseNum += num;
    }

    public void updateTotalGame(Long num) {
        this.totalGameNum += num;
    }

    public void updateSoloExit(Long num) {
        this.soloExitNum += num;
    }

    public void updateMakeRoom(Long num) {
        this.makeRoomNum += num;
    }

    public void updateEnterGame(Long num) {
        this.enterGameNum += num;
    }
    public void updatePlayTime(Long num) {
        this.playTime += num;
    }
}
  • 유저 정보를 관리하는 엔티티로서, 사용자의 기본 정보 및 게임 관련 통계 데이터를 저장함
    • 유저의 인증 정보와 게임 내 활동을 추적하는 데 필요한 데이터를 포함하며, 애플리케이션 내에서 유저의 중심적인 역할을 수행함
  • 필드 정의
    • id : 유저의 고유 식별자로, 데이터베이스에서 자동으로 생성되는 값
    • email, nickname, password : 유저의 이메일, 닉네임, 비밀번호를 저장합니다. 이메일과 닉네임은 고유해야 하므로 unique = true 설정이 추가됨
    • kakaoId : 카카오 로그인을 통해 얻은 유저 식별자
    • winNum, loseNum, totalGameNum, enterGameNum, soloExitNum, makeRoomNum, playTime : 유저의 게임 승리 횟수, 패배 횟수, 총 게임 수, 참가한 게임 수, 독자적으로 게임을 나간 횟수, 방을 만든 횟수, 총 플레이 시간 등의 게임 관련 통계 데이터를 저장함
  • 메서드 정의
    • 생성자를 통해 유저 정보를 초기화
    • update... 메서드들을 통해 유저의 게임 통계 데이터를 업데이트함
  • JPA 설정

 

domainModel

GameCommand

package com.example.namoldak.domainModel;

import com.example.namoldak.domain.*;
import com.example.namoldak.repository.GameRoomAttendeeRepository;
import com.example.namoldak.repository.GameRoomRepository;
import com.example.namoldak.repository.GameStartSetRepository;
import com.example.namoldak.repository.RewardReposiroty;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

// 기능 : 게임 도메인 관련 DB CUD 관리
@Service
@RequiredArgsConstructor
public class GameCommand {

    private final GameRoomRepository gameRoomRepository;
    private final GameRoomAttendeeRepository gameRoomAttendeeRepository;
    private final GameStartSetRepository gameStartSetRepository;
    private final RewardReposiroty rewardReposiroty;

    //////////////TODO GameRoom 관련
    // 게임방 저장하기
    public void saveGameRoom(GameRoom gameRoom) {
        gameRoomRepository.save(gameRoom);
    }

    // 게임방 삭제하기
    public void deleteGameRoom(GameRoom gameRoom) {
        gameRoomRepository.delete(gameRoom);
    }

    //////////////TODO GameRoomAttendee 관련
    // 참가자 저장
    public void saveGameRoomAttendee(GameRoomAttendee gameRoomAttendee) {
        gameRoomAttendeeRepository.save(gameRoomAttendee);
    }

    // 참가자 삭제
    public void deleteGameRoomAttendee(GameRoomAttendee gameRoomAttendee) {
        gameRoomAttendeeRepository.delete(gameRoomAttendee);
    }

    //////////////TODO GameStartSet 관련
    // GameStartSet 저장하기
    public void saveGameStartSet(GameStartSet gameStartSet) {
        gameStartSetRepository.save(gameStartSet);
    }

    // GameStartSet 객체로 DB에서 삭제하기
    public void deleteGameStartSetByRoomId(Long roomId) {
        gameStartSetRepository.deleteByRoomId(roomId);
    }

    //////////////TODO Reward 관련
    // 리워드 저장하기
    public void saveReward(Reward reward) {
        rewardReposiroty.save(reward);
    }

}
  • 게임 도메인에 대한 데이터베이스의 생성(Create), 갱신(Update), 삭제(Delete) 작업을 관리함
    • @Service 어노테이션을 사용하여 정의되어 있으며, 의존성 주입을 통해 필요한 레포지토리 객체들을 주입받음
    • 이러한 구조는 각 엔티티에 대한 데이터베이스 작업을 명확하게 분리하고, 도메인 로직을 캡슐화하여 애플리케이션의 유지보수성과 확장성을 향상시킴
  • 필드 정의
    • gameRoomRepository : 게임방 데이터를 관리하는 레포지토리
    • gameRoomAttendeeRepository : 게임방 참가자 데이터를 관리하는 레포지토리
    • gameStartSetRepository : 게임 설정 데이터를 관리하는 레포지토리
    • rewardReposiroty : 게임의 보상 데이터를 관리하는 레포지토리
  • 기능 제공
    • saveGameRoom : 게임방 엔티티를 저장
    • deleteGameRoom : 게임방 엔티티를 삭제
    • saveGameRoomAttendee : 게임방 참가자 정보를 저장
    • deleteGameRoomAttendee : 게임방 참가자 정보를 삭제
    • saveGameStartSet : 게임 설정 정보를 저장
    • deleteGameStartSetByRoomId : 특정 게임방의 게임 설정 정보를 삭제
    • saveReward : 게임의 보상 정보를 저장

GameQuery

package com.example.namoldak.domainModel;

import com.example.namoldak.domain.*;
import com.example.namoldak.repository.*;
import com.example.namoldak.util.GlobalResponse.CustomException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.List;
import static com.example.namoldak.util.GlobalResponse.code.StatusCode.*;
import static com.example.namoldak.util.GlobalResponse.code.StatusCode.NOT_EXIST_ROOMS;

// 기능 : 게임 도메인 관련 DB Read 관리
@Service
@RequiredArgsConstructor
public class GameQuery {

    private final GameStartSetRepository gameStartSetRepository;
    private final KeywordRepository keywordRepository;
    private final GameRoomAttendeeRepository gameRoomAttendeeRepository;
    private final GameRoomRepository gameRoomRepository;
    private final RewardReposiroty rewardReposiroty;

    //////////////TODO GameRoom 관련
    // 게임룸 Id로 객체 찾아오기
    public GameRoom findGameRoomByRoomId(Long roomId) {
        return gameRoomRepository.findByGameRoomId(roomId).orElseThrow(
                ()-> new CustomException(NOT_EXIST_ROOMS)
        );
    }
    public GameRoom findGameRoomByRoomIdLock(Long roomId) {
        return gameRoomRepository.findByGameRoomId2(roomId).orElseThrow(
                ()-> new CustomException(NOT_EXIST_ROOMS)
        );
    }

    // 페이징 처리해서 모든 게임방 갖고 오기
    public Page<GameRoom> findGameRoomByPageable(Pageable pageable) {
        Page<GameRoom> gameRoomPage = gameRoomRepository.findAll(pageable);
        return gameRoomPage;
    }

    // 특정 키워드로 검색해서 게임방 가져오기
    public Page<GameRoom> findGameRoomByContainingKeyword(Pageable pageable, String keyword) {
        Page<GameRoom> gameRoomPage = gameRoomRepository.findByGameRoomNameContaining(pageable, keyword);
        return gameRoomPage;
    }

    // 게임방 리스트 형식으로 갖고 오기
    public List<GameRoom> findAllGameRoomList() {
        List<GameRoom> gameRoomList = gameRoomRepository.findAll();
        return gameRoomList;
    }

    //////////////TODO GameRoomAttendee 관련
    // 멤버 객체로 참가자 정보 조회
    public GameRoomAttendee findAttendeeByMember(Member member) {
        GameRoomAttendee gameRoomAttendee = gameRoomAttendeeRepository.findByMember(member).orElseThrow(
                ()-> new CustomException(NOT_FOUND_ATTENDEE)
        );
        return gameRoomAttendee;
    }
    // 게임룸 객체로 참가자 찾아오기
    public List<GameRoomAttendee> findAttendeeByGameRoom(GameRoom gameRoom) {
        List<GameRoomAttendee> gameRoomAttendeeList = gameRoomAttendeeRepository.findByGameRoom(gameRoom);
        return gameRoomAttendeeList;
    }

    public List<GameRoomAttendee> findAttendeeByRoomId(Long roomId) {
        return gameRoomAttendeeRepository.findByGameRoom_GameRoomId(roomId);
    }

    // 멤버 Id로 참가자 객체 가져오기
    public GameRoomAttendee findAttendeeByMemberId(Long memberId) {
        return gameRoomAttendeeRepository.findByMember_Id(memberId).orElseThrow(
                ()-> new CustomException(NOT_FOUND_ATTENDEE)
        );
    }

    //////////////TODO GameStartSet 관련
    // RoomId로 GameStartSet 객체 찾아오기
    public GameStartSet findGameStartSetByRoomId(Long roomId) {
        return gameStartSetRepository.findByRoomId(roomId).orElseThrow(
                ()-> new CustomException(GAME_SET_NOT_FOUND)
        );
    }

    //////////////TODO 댓글 관련
    // 키워드 랜덤으로 4개 가지고 오기
    public List<Keyword> findTop4KeywordByCategory(String category) {
        List<Keyword> keywordList = keywordRepository.findTop4ByCategory(category);
        return keywordList;
    }

    // 키워드 랜덤으로 3개 가지고 오기
    public List<Keyword> findTop3KeywordByCategory(String category) {
        List<Keyword> keywordList = keywordRepository.findTop3ByCategory(category);
        return keywordList;
    }

    //////////////TODO Reward 관련
    public List<Reward> findAllReward(Member member) {
        List<Reward> rewardList = rewardReposiroty.findByMember(member);
        return rewardList;
    }
}
  • 게임 도메인 관련 데이터를 조회(Read)하는 데 사용되는 서비스로, 데이터베이스에서 정보를 읽어오는 기능을 제공함
  • 게임방 정보 조회
    • findGameRoomByRoomId : 특정 ID를 가진 게임방을 찾아 반환함
    • findGameRoomByRoomIdLock : 특정 ID를 가진 게임방을 찾되, 데이터베이스 락을 적용하여 동시성을 관리할 때 사용할 수 있음
    • findGameRoomByPageable : 페이지 처리를 적용하여 게임방 목록을 조회함
    • findGameRoomByContainingKeyword : 특정 키워드를 포함하는 게임방 이름으로 게임방을 조회함
  • 게임방 참가자 정보 조회
    • findAttendeeByMember : 특정 멤버 객체를 기반으로 게임방 참가자 정보를 조회함
    • findAttendeeByGameRoom : 특정 게임방 객체를 기반으로 해당 게임방의 참가자 목록을 조회함
    • findAttendeeByRoomId : 특정 게임방 ID를 기반으로 참가자 목록을 조회함
    • findAttendeeByMemberId : 특정 멤버 ID로 게임방 참가자 객체를 조회함
  • 게임 설정 정보 조회
    • findGameStartSetByRoomId : 특정 게임방 ID로 게임 설정 정보(GameStartSet)를 조회함
  • 키워드 정보 조회
    • findTop4KeywordByCategory : 특정 카테고리에 대해 랜덤으로 4개의 키워드를 조회함
    • findTop3KeywordByCategory : 특정 카테고리에 대해 랜덤으로 3개의 키워드를 조회함
  • 보상 정보 조회
    • findAllReward : 특정 멤버에 대한 모든 보상 정보를 조회함

Dto

RequestDto/GameDto

package com.example.namoldak.dto.RequestDto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

// 기능 : 게임 정답 Dto
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class GameDto {
    private String answer;
    private String nickname;
}
  • 게임에서 사용자가 제출한 정답과 닉네임을 전달하기 위한 데이터 전송 객체
    • 클라이언트로부터 게임과 관련된 요청을 처리할 때 사용되며, 요청 데이터를 담아 애플리케이션 내부로 전달하는 역할을 수행함
    • 이 구조는 게임 로직 처리에 필요한 사용자 데이터를 명확하고 효율적으로 전달하기 위해 설계 됨
  • 필드 정의
    • answer : 사용자가 게임에서 입력한 정답 문자열을 저장함
    • nickname : 사용자의 닉네임을 저장함
  • Lombok 어노테이션 사용

RequestDto/GameRoomRequestDto

package com.example.namoldak.dto.RequestDto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

// 기능 : 게임룸 생성시 사용하는 Dto
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class GameRoomRequestDto {
    private String gameRoomName;
    private String gameRoomPassword;
}
  • 게임룸 생성 시 클라이언트로부터 받는 데이터를 담는 역할을 하는 데이터 전송 객체(DTO)임, 이 클래스는 게임룸 생성에 필요한 정보를 구조화하여 관리함
    • 사용자의 요청을 애플리케이션 서버에서 처리할 때 필요한 데이터를 캡슐화하여 전달하는데 사용되며, 이를 통해 게임룸 생성 과정에서 요구되는 정보를 효율적으로 관리하고 처리할 수 있음
  • 필드 정의
    • gameRoomName : 생성할 게임룸의 이름을 나타냄
      gameRoomPassword :
      게임룸에 접근하기 위해 필요한 비밀번호를 나타내며, 이는 옵셔널하게 사용될 수 있어 비공개 방을 생성할 때 필요함
  • Lombok 어노테이션 사용

RequestDto/WebSocketMessage

package com.example.namoldak.dto.RequestDto;

import lombok.Getter;

// 기능 : 프론트에서 받는 시그널링용 Message
@Getter
public class WebSocketMessage {
    private String sender;
    private String type;
    private String data;
    private Long roomId;
    private String nickname;
    private String receiver;
    private Object offer;
    private Object answer;
    private Object candidate;
    private Object sdp;

}
  • 프론트엔드로부터 받는 시그널링 정보를 처리하기 위한 DTO(Data Transfer Object)임, 이 클래스는 웹소켓 통신 중에 교환되는 메시지의 구조를 정의하며, WebRTC 연결 설정과 관련된 다양한 데이터를 포함함
    • 웹소켓을 통한 실시간 통신에서 중요한 역할을 수행하며, WebRTC 기반의 어플리케이션에서 피어 간의 연결 설정과 데이터 교환을 가능하게 하는 데 사용됨
  • 필드 정의
    • sender : 메시지를 보낸 사용자의 식별자
    • type : 메시지의 유형을 나타내며, 예를 들어 offer, answer, candidate 등이 있음
    • data : 메시지와 관련된 추가 데이터를 나타냄
    • roomId : 메시지가 속한 방의 식별자
    • nickname : 메시지를 보낸 사용자의 닉네임
    • receiver : 메시지를 받을 대상의 식별
    • offer : WebRTC 연결에서 사용하는 SDP(Session Description Protocol) 초기 제안을 나타냄
    • answer : offer에 대한 응답으로, WebRTC 연결에서 사용하는 SDP 정보
    • candidate : ICE(Interactive Connectivity Establishment) 후보자 정보를 나타냄
    • sdp : 세션 기술 프로토콜 관련 정보

ResponseDto/GameRoomResponseDto

package com.example.namoldak.dto.ResponseDto;

import lombok.Builder;
import lombok.Getter;
import java.util.List;

// 기능 : 게임룸 정보 Response Dto
@Builder
@Getter
public class GameRoomResponseDto {
    private Long id;
    private String roomName;
    private String roomPassword;
    private String owner;
    private boolean status;
    private int memberCnt;
    private List<MemberResponseDto> member;
}
  • 게임룸 정보를 클라이언트에 응답할 대 사용되는 데이터 전송 객체(DTO)임, 이 클래스는 게임룸의 상세 정보를 구조화하여 클라이언트에 전달하는 역할을 수행함
    • 게임룸 관련 요청에 대한 응답 데이터를 캡슐화하여, 클라이언트에 필요한 게임룸의 상태 및 참가 멤버 정보를 효과적으로 전달함
    • 프론트엔드에서 게임룸의 상태를 사용자에게 표시하거나, 게임룸 관련 정보를 처리하는 데 사용될 수 있ㅇ므
  • 필드 정의
    • id : 게임룸의 고유 식별자
    • roomName : 게임룸의 이름
    • roomPassword : 게임룸의 비밀번호로, 비공개 방인 경우 사용
    • owner : 게임룸의 소유자(방장)입니다
    • status : 게임룸의 현재 상태(예: 활성화/비활성화)를 나타냄
    • memberCnt : 게임룸에 참가한 멤버의 수
    • member : 게임룸에 참가한 멤버들의 정보 목록으로, MemberResponseDto의 리스트 형태임
  • Lombok 어노테이션 사용

ResponseDto/GameRoomResponseListDto

package com.example.namoldak.dto.ResponseDto;

import lombok.Getter;
import java.util.List;

// 기능 : 게임룸 List 응답 Dto
@Getter
public class GameRoomResponseListDto {
    private int totalPage;
    List<GameRoomResponseDto> gameRoomResponseDtoList;

    public GameRoomResponseListDto(int totalPage, List<GameRoomResponseDto> gameRoomResponseDtoList) {
        this.totalPage               = totalPage;
        this.gameRoomResponseDtoList = gameRoomResponseDtoList;
    }
}
  • 클라이언트에 전달되는 게임룸 목록 정보를 담는 응답 DOT(Data Transfer Object)임, 이 클래스는 게임룸 리스트와 관련된 데이터를 구조화하여 전달하는 역할을 수행함
    • 게임룸 정보를 페이지화하여 관리하고, 이를 클라이언트에게 효율적으로 전달하기 위한 목적으로 사용됨, 클라이언트는 게임룸의 리스트와 전체 페이지 수를 알 수 있으며, 사용자 인터페이스에서 이 정보를 바탕으로 사용자에게 게임룸 데이터를 표시할 수 있음
  • 필드 정의
    • totalPage : 전체 페이지 수를 나타냄, 페이징 처리된 응답에서 사용자에게 전체 데이터의 페이지 수를 알려주는 데 사용
    • gameRoomResponseDtoList : GameRoomResponseDto 객체의 리스트로, 각 게임룸의 상세 정보를 포함한다
  • 생성자
    • GameRoomResponseListDto(int totalPage, List<GameRoomResponseDto> gameRoomResponseDtoList) : 전체 페이지 수와 게임룸 상세 정보 리스트를 매개변수로 받아 객체를 초기화함

ResponseDto/WebSocketResponseMessage

package com.example.namoldak.dto.ResponseDto;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;

// 기능 : 프론트에 응답하는 시그널링용 Message
@Builder
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
@NoArgsConstructor
@AllArgsConstructor
public class WebSocketResponseMessage {
    private String sender;
    private String senderNickName;
    private String type;
    private String data;
    private Long roomId;
    private List<String> allUsers;
    private Map<String, String> allUsersNickNames;
    private String receiver;
    private Object offer;
    private Object answer;
    private Object candidate;
    private Object sdp;
}
  • 웹소켓 통신을 통해 프론트엔드에 응답하는 시그널링 메시지를 정의하는 DTO(Data Transfer Object)임, 이 클래스는 WebRTC 통신을 시그널링 정보와 그외 필요한 데이터를 전달하는 데 사용됨
  • 필드 정의
    • sender : 메시지를 보낸 사용자의 식별자
    • senderNickName : 메시지를 보낸 사용자의 닉네임
    • type : 메시지의 유형(예: offer, answer, candidate)임
    • data : 메시지와 관련된 추가 데이터
    • roomId : 메시지가 속한 방의 식별자
    • allUsers : 방에 있는 모든 사용자의 식별자 리스트
    • allUsersNickNames : 방에 있는 모든 사용자의 닉네임과 식별자를 매핑한 정보
    • receiver : 메시지를 받을 대상의 식별자
    • offer, answer, candidate, sdp: WebRTC 연결에 사용되는 각종 시그널링 데이터
  • Lombok 어노테이션 사용
  • JsonInclude 어노테이션 사용
    • @JsonInclude(JsonInclude.Include.NON_NULL): JSON으로 직렬화할 때 null 값이 아닌 필드만 포함시키도록 설정함, 이를 통해 불필요한 null 값의 전송을 방지하고, 데이터 전송을 최적화함

ResponseDto/VictoryDto

package com.example.namoldak.dto.ResponseDto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;

// 기능 : 위너와 루저 정보를 전달하는 Dto
@Getter
@NoArgsConstructor
public class VictoryDto {
    List<String> winner = new ArrayList<>();
    List<String> loser = new ArrayList<>();

    public void setWinner(String winner){
        this.winner.add(winner);
    }
    public void setLoser(String loser){
        this.loser.add(loser);
  • 게임 결과에서 승리자(winner)와 패배자(loser)의 정보를 전달하기 위한 데이터 전송 객체(DTO)
    • 게임 종료 시 승리자와 패배자 목록을 쉽게 관리하고 전달하기 위해 사용됨, 이 DTO를 통해 게임 결과 관련 정보를 클라이언트나 다른 서비스 컴포넌트에 효과적으로 전달할 수 있음
  • 필드 정의
    • winner : 승리자 목록을 나타내는 String 타입의 리스트입니다. 여러 승리자를 관리할 수 있도록 ArrayList로 초기화되어 있습니다.
    • loser : 패배자 목록을 나타내는 String 타입의 리스트입니다. 여러 패배자를 관리할 수 있도록 ArrayList로 초기화되어 있습니다.
  • 메서드 정의
    • setWinner(String winner) : 승리자 리스트에 새로운 승리자를 추가합니다. 파라미터로 받은 승리자의 이름을 리스트에 추가하는 기능을 수행합니다.
    • setLoser(String loser) : 패배자 리스트에 새로운 패배자를 추가합니다. 파라미터로 받은 패배자의 이름을 리스트에 추가하는 기능을 수행합니다.
  • Lombok 어노테이션 사용

 

Repository

GameRoomAttendeeRepository

package com.example.namoldak.repository;

import com.example.namoldak.domain.GameRoom;
import com.example.namoldak.domain.GameRoomAttendee;
import com.example.namoldak.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;

// 기능 : 게임룸에 들어간 유저 정보 레포
public interface GameRoomAttendeeRepository extends JpaRepository<GameRoomAttendee, Long> {
    List<GameRoomAttendee> findByGameRoom(GameRoom gameRoom); // 게임룸 객체로 안에 있는 멤버들 전부 조회
    Optional<GameRoomAttendee> findByMember(Member member); // 멤버 객체로 참가자 정보 조회
    Optional<GameRoomAttendee> findByMember_Id(Long memberId); // 멤버 ID로 참가자 정보 조회
    List<GameRoomAttendee> findByGameRoom_GameRoomId(Long gameRoomId); // 게임룸 ID로 안에 있는 멤버 전부 조회
    @Transactional
    void deleteAllByMember(Member member);  // 멤버 객체로 삭제
    boolean existsByMember(Member member);  // 멤버 존재 여부 확인

}
  • 위 인터페이스는 GameRoomAttendee 엔티티에 대한 데이터 접근을 위한 JPA 레포지토리
    • JpaRepository 인터페이스를 상속받아 기본적인 CRUD 작업을 위한 메서드뿐만 아니라, 필요한 커스텀 쿼리 메서드를 추가로 제공함
    • 이를 통해 게임룸과 관련된 참가자 데이터에 대한 관리가 용이해짐
  • 게임룸 참가자 조회
    • findByGameRoom(GameRoom gameRoom) : 특정 게임룸에 참여하고 있는 모든 멤버를 조회
    • findByMember(Member member) : 특정 멤버에 해당하는 게임룸 참가자 정보를 조회
    • findByMember_Id(Long memberId) : 멤버 ID를 기준으로 게임룸 참가자 정보를 조회
    • findByGameRoom_GameRoomId(Long gameRoomId) : 게임룸 ID를 통해 해당 게임룸의 모든 참가자 정보를 조회
  • 참가자 정보 삭제
    • deleteAllByMember(Member member) : 특정 멤버와 관련된 게임룸 참가자 정보를 삭제함, @Transactional 어노테이션을 사용하여 데이터의 일관성을 유지
  • 참가자 존재 여부 확인
    • existsByMember(Member member) : 특정 멤버가 게임룸 참가자로 존재하는지 여부를 확인

GameRoomRepository

package com.example.namoldak.repository;

import com.example.namoldak.domain.GameRoom;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import javax.persistence.LockModeType;
import java.util.Optional;

// 기능 : 게임룸 레포
public interface GameRoomRepository extends JpaRepository<GameRoom, Long> {
    Page<GameRoom> findAll(Pageable pageable); // 게임룸 전체 조회 페이징처리
    Page<GameRoom> findByGameRoomNameContaining(Pageable pageable, String keyword); // 게임룸 페이징 처리 + 검색 기능
    Optional<GameRoom> findByGameRoomId(Long gameRoomId); // 게임룸 단건 조회
    @Lock(LockModeType.PESSIMISTIC_WRITE)   // 동시 접속에 대한 충돌 방지용 비관적 락
    @Query("select b from GameRoom b where b.gameRoomId = :gameRoomId")
    Optional<GameRoom> findByGameRoomId2(Long gameRoomId); // 게임룸 단건 조회

}
  • GameRoom 엔티티에 대한 데이터 접근을 위한 JPA 레포지토리, 이 인터페이스는 게임룸 데이터에 대한 기본적인 CRUD 작업 및 특정 기능을 위한 메서드를 제공
    • 게임룸 데이터에 대한 접근 및 관리가 용이해지며, 페이징 처리, 검색 기능, 동시성 제어 등의 추가적인 요구 사항을 충족할 수 있음
  • 기본 조회 기능
    • findAll(Pageable pageable) : 게임룸 전체를 페이지 처리하여 조회함, 이 메소드는 대규모 데이터셋을 효과적으로 처리하기 위해 페이징을 사용함
    • findByGameRoomNameContaining(Pageable pageable, String keyword) : 게임룸 이름에 특정 키워드가 포함되어 있는지 검색하고, 결과를 페이지 처리하여 반환함
  • 단건 조회 기능
    • findByGameRoomId(Long gameRoomId) : 특정 ID를 가진 게임룸을 조회하고, 결과는 Optional로 반환되어, 해당 게임룸이 존재하지 않을 경우 쉽게 처리할 수 있음
  • 비관적 락을 통한 동시성 제어
    • findByGameRoomId2(Long gameRoomId) :
      • 특정 ID를 가진 게임룸을 비관적 락(PESSIMISTIC_WRITE)을 사용하여 조회합니다.
      • 이는 동시에 여러 트랜잭션이 같은 레코드에 접근할 때 발생할 수 있는 충돌을 방지하기 위해 사용됩니다.
      • 쿼리 메소드에 @Lock 어노테이션을 사용하여 비관적 락을 설정합니다.

GameStartSetRepository

package com.example.namoldak.repository;

import com.example.namoldak.domain.GameStartSet;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

// 기능 : 게임 시작시 저장되는 스타트셋 레포
public interface GameStartSetRepository extends JpaRepository<GameStartSet, Long> {
    Optional<GameStartSet> findByRoomId(Long roomId);   // 게임룸 ID로 스타트 셋 찾기
    void deleteByRoomId(Long roomId);   // 게임룸 ID로 스타트 셋 지우기
}
  • GameStartSet 엔티티에 대한 데이터 접근을 위한 JPA 레포지토리, 게임 시작 시 사용되는 설정 정보(스타트겟)에 대한 데이터베이스 작업을 수행함
    • 게임 관련 설정 정보의 저장, 조회, 삭제 등의 데이터베이스 작업을 쉽게 처리할 수 있으며, 게임룸과 관련된 로직을 구현하는 데 필요한 핵심 데이터 작업을 지원함
  • 스타트셋 조회
    • findByRoomId(Long roomId) : 특정 게임룸 ID에 해당하는 게임 스타트셋 정보를 조회합니다. 조회 결과는 Optional로 반환되어, 해당 스타트셋이 존재하지 않을 경우 쉽게 처리할 수 있습니다.
  • 스타트셋 삭제
    • deleteByRoomId(Long roomId) : 특정 게임룸 ID에 해당하는 게임 스타트셋 정보를 삭제합니다. 이 메소드는 게임룸이 종료되거나 스타트셋 정보가 더 이상 필요하지 않을 때 사용됩니다.

SessionRepository

package com.example.namoldak.repository;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

// 기능 : 웹소켓에 필요한 세션 정보를 저장, 관리 (싱글톤)
@Slf4j
@Component
@NoArgsConstructor
public class SessionRepository {
    private static SessionRepository sessionRepository;
    // 세션 저장 1) clientsInRoom : 방 Id를 key 값으로 하여 방마다 가지고 있는 Client들의 session Id 와 session 객체를 저장
    private final Map<Long, Map<String, WebSocketSession>> clientsInRoom = new ConcurrentHashMap<>();
    // 세션 저장 2) roomIdToSession : 참가자들 각각의 데이터로 session 객체를 key 값으로 하여 해당 객체가 어느방에 속해있는지를 저장
    private final Map<WebSocketSession, Long> roomIdToSession = new HashMap<>();
    // 세션 저장 3) nicknamesInRoom : 참가자들의 세션 Id와 닉네임을 저장
    private final Map<String, String> nicknamesInRoom = new HashMap<>();

    // Session 데이터를 공통으로 사용하기 위해 싱글톤으로 구현
    public static SessionRepository getInstance(){
        if(sessionRepository == null){
            synchronized (SessionRepository.class){
                sessionRepository = new SessionRepository();
            }
        }
        return sessionRepository;
    }

    // 해당 방의 ClientList 조회
    public Map<String, WebSocketSession> getClientList(Long roomId) {
        return clientsInRoom.get(roomId);
    }

    // 해당 방 존재 유무 조회
    public boolean hasRoom(Long roomId){
        return clientsInRoom.containsKey(roomId);
    }

    // 해당 session이 어느방에 있는지 조회
    public Long getRoomId(WebSocketSession session){
        return roomIdToSession.get(session);
    }

    // 세션을 저장 및 수정해서 저장
    public void addClientInNewRoom(Long roomId, WebSocketSession session) {
        Map<String, WebSocketSession> newClient = new HashMap<>();
        newClient.put(session.getId(), session);
        clientsInRoom.put(roomId, newClient);
    }

    // 끊어진 Client session 하나만 지우고 다시 저장
    public void deleteClient(Long roomId, WebSocketSession session) {
        Map<String, WebSocketSession> clientList = clientsInRoom.get(roomId);
        String removeKey = "";
        for(Map.Entry<String, WebSocketSession> oneClient : clientList.entrySet()){
            if(oneClient.getKey().equals(session.getId())){
                removeKey = oneClient.getKey();
            }
        }
        clientList.remove(removeKey);

        // 끊어진 세션을 제외한 나머지 세션들을 다시 저장
        clientsInRoom.put(roomId, clientList);
    }

    // 하나의 Client session 정보만 추가하기
    public void addClient(Long roomId, WebSocketSession session) {
        clientsInRoom.get(roomId).put(session.getId(), session);
    }

    // 방 정보 모두 삭제 (방 폭파시 연계 작동)
    public void deleteAllclientsInRoom(Long roomId){
        clientsInRoom.remove(roomId);
    }

    // roomIdToSession에서 동일한 roomId에 해당하는 session을 모두 조회
    public Map<WebSocketSession, Long> searchRooIdToSessionList(Long roomId){
        return roomIdToSession.entrySet()
                .stream()
                .filter(entry ->  entry.getValue() == roomId)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    // session을 key로 roomIdToSession에 이 세션이 어느방에 속해 있는 지 저장
    public void saveRoomIdToSession(WebSocketSession session, Long roomId) {
        roomIdToSession.put(session, roomId);
    }

    // session을 key로 roomIdToSession에서 해당 세션 정보 삭제
    public void deleteRoomIdToSession(WebSocketSession session) {
        roomIdToSession.remove(session);
    }

    // session Id로 닉네임 정보 조회
    public String getNicknameInRoom(String sessionId) {
        return this.nicknamesInRoom.get(sessionId);
    }

    // session Id를 키로 닉네임 정보 저장
    public void addNicknameInRoom(String sessionId, String nickname) {
        this.nicknamesInRoom.put(sessionId, nickname);
    }

    // session Id를 키로 닉네임 정보 삭제
    public void deleteNicknameInRoom(String sessionId) {
        this.nicknamesInRoom.remove(sessionId);
    }
}
  • 웹소켓 세션 정보를 저장하고 관리하는 싱글톤 구조의 레포지토리, 웹 소켓 통신 중에 사용자 세션을 효과적으로 관리하기 위해 사용 됨
    • 웹소켓 통신 과정에서 다루어야 하는 다양한 세션 정보를 효율적으로 관리할 수 있으며, 실시간 통신 어플리케이션에서 필요한 세션 관리 로직을 캡슐화함
  • 세션 및 방 정보 관리
    • 각 방에 참여하는 클라이언트의 세션 정보와, 세션 별로 어느 방에 속해 있는지의 정보를 관리함
    • 사용자의 닉네임과 세션 ID를 연결하는 맵을 통해 사용자 정보를 관리함
  • 싱글톤 구현
    • getInstance( ) : SessionRepository 인스턴스의 글로벌 접근 지점, 인스턴스가 없으면 생성하고, 있으면 기존 인스턴스를 반환
  • 세션 정보 조작 메서드
    • addClientInNewRoom, deleteClient, addClient : 특정 방에 클라이언트 세션을 추가하거나 삭제하는 기능을 제공
    • deleteAllclientsInRoom : 특정 방의 모든 클라이언트 세션 정보를 삭제
  • 조회 메서드
    • getClientList, hasRoom, getRoomId, searchRooIdToSessionList : 특정 조건에 따라 저장된 세션 정보를 조회
  • 세션 및 닉네임 정보 관리
    • saveRoomIdToSession, deleteRoomIdToSession, getNicknameInRoom, addNicknameInRoom, deleteNicknameInRoom : 세션에 해당하는 방 ID를 저장하거나 삭제하고, 세션 ID에 해당하는 닉네임 정보를 관리

 

Controller

GameController

package com.example.namoldak.controller;

import com.example.namoldak.dto.RequestDto.GameDto;
import com.example.namoldak.service.GameService;
import com.example.namoldak.util.GlobalResponse.GlobalResponseDto;
import com.example.namoldak.util.GlobalResponse.ResponseUtil;
import com.example.namoldak.util.GlobalResponse.code.StatusCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

// 기능 : 게임 진행 관련 주요 서비스들을 컨트롤
@Slf4j
@RequiredArgsConstructor
@RestController
public class GameController {

    private final GameService gameService;

    // 게임 시작
    @MessageMapping("/game/{roomId}/start")
    public ResponseEntity<GlobalResponseDto> gameStart(@DestinationVariable Long roomId,
                                                       GameDto gameDto) {
        gameService.gameStart(roomId, gameDto);
        return ResponseUtil.response(StatusCode.GAME_START);
    }

    // 건너뛰기
    @MessageMapping("/game/{roomId}/skip")
    public void gameSkip(GameDto gameDto,
                         @DestinationVariable Long roomId) {
        gameService.gameSkip(gameDto, roomId);
    }

    // 발언권 부여
    @MessageMapping("/game/{roomId}/spotlight")
    public void spotlight(
            @DestinationVariable Long roomId) {
        log.info("스포트라이트 - 게임방 아이디 : {}", roomId);
        gameService.spotlight(roomId);
    }

    // 정답
    @MessageMapping("/game/{roomId}/answer")
    public void gameAnswer(@DestinationVariable Long roomId,
                           @RequestBody GameDto gameDto) {
        gameService.gameAnswer(roomId, gameDto);
    }

    // 게임 끝내기
    @MessageMapping("/game/{roomId}/endGame")
    public void endGame(@DestinationVariable Long roomId) {
        gameService.endGame(roomId);
    }
}
  • 게임 진행과 관련된 주요 서비스를 제어하는 컨트롤러, Spring의 메시지 매핑을 활용하여 웹소켓을 통한 실시간 게임 서비스를 제공함
    • @MessageMapping 어노테이션을 사용하여 웹소켓을 통한 메시지 라우팅을 구현하며, 각 게임 액션에 대한 요청을 적절한 서비스 로직으로 연결하여 처리함, 이를 통해 사용자는 웹소켓 기반의 인터렉티브한 게임 경험을 할 수 있음
  • 게임 시작(gameStart)
    • /game/{roomId}/start 엔드포인트를 통해 게임 시작 명령을 처리합니다.
    • roomId를 URL 경로 변수로 받고, GameDto를 메시지 본문으로 받아 게임 서비스의 gameStart 메소드를 호출합니다.
  • 게임 건너뛰기(gameSkip)
    • /game/{roomId}/skip 엔드포인트에서 건너뛰기 액션을 처리합니다.
    • roomId와 GameDto를 받아 게임 서비스의 gameSkip 메소드를 호출합니다.
  • 발언권 부여(spotlight)
    • /game/{roomId}/spotlight 엔드포인트를 통해 특정 사용자에게 발언권을 부여하는 기능을 처리합니다.
    • roomId만 받고, 게임 서비스의 spotlight 메소드를 호출합니다.
  • 정답 제출(gameAnswer)
    • /game/{roomId}/answer 엔드포인트를 통해 사용자의 정답을 처리합니다.
    • roomId와 GameDto를 받아 게임 서비스의 gameAnswer 메소드를 호출합니다.
  • 게임 종료(endGame)
    • /game/{roomId}/endGame 엔드포인트에서 게임 종료 처리를 합니다.
    • roomId를 받아 게임 서비스의 endGame 메소드를 호출합니다.

GameRoomController

package com.example.namoldak.controller;

import com.example.namoldak.dto.RequestDto.GameRoomRequestDto;
import com.example.namoldak.dto.ResponseDto.GameRoomResponseListDto;
import com.example.namoldak.service.GameRoomService;
import com.example.namoldak.util.GlobalResponse.GlobalResponseDto;
import com.example.namoldak.util.GlobalResponse.ResponseUtil;
import com.example.namoldak.util.GlobalResponse.code.StatusCode;
import com.example.namoldak.util.security.UserDetailsImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

// 기능 : 게임룸 관련 CRUD 컨트롤
@Slf4j
@RequiredArgsConstructor
@RestController
public class GameRoomController {
    private final GameRoomService gameRoomService;

    // 게임룸 생성
    @PostMapping("/rooms")
    public ResponseEntity<Map<String, String>> makeGameRoom(@AuthenticationPrincipal UserDetailsImpl userDetails,
                                                              @RequestBody GameRoomRequestDto gameRoomRequestDto) {
        return ResponseUtil.response(gameRoomService.makeGameRoom(userDetails.getMember(), gameRoomRequestDto));
    }

    // 게임룸 전체조회 (페이징 처리)
    @GetMapping("/rooms") // '/rooms?page=1'
    public ResponseEntity<GameRoomResponseListDto> mainPage(@PageableDefault(size = 4, sort = "gameRoomId", direction = Sort.Direction.DESC) Pageable pageable) {
        return ResponseUtil.response(gameRoomService.mainPage(pageable));
    }

    // 게임룸 키워드 조회
    @GetMapping("/rooms/search") // '/rooms/search?keyword=검색어'
    public GameRoomResponseListDto searchGame(@PageableDefault(size = 4, sort = "gameRoomId", direction = Sort.Direction.DESC) Pageable pageable, String keyword) {
        return gameRoomService.searchGame(pageable, keyword);
    }

    // 게임룸 입장
    @PostMapping("/rooms/{roomId}")
    public ResponseEntity<Map<String, String>> enterGame(@PathVariable Long roomId,
                                                         @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return ResponseUtil.response(gameRoomService.enterGame(roomId, userDetails.getMember()));
    }

    // 게임룸 입장 검증
    @PostMapping("/rooms/{roomId}/verify")
    public void enterVerify(@PathVariable Long roomId,
                            @AuthenticationPrincipal UserDetailsImpl userDetails) {
        gameRoomService.enterVerify(roomId, userDetails);
    }

    // 게임룸 나가기
    @DeleteMapping("/rooms/{roomId}/exit")
    public ResponseEntity<GlobalResponseDto> roomExit(@PathVariable Long roomId,
                                                     @AuthenticationPrincipal UserDetailsImpl userDetails) {
        gameRoomService.roomExit(roomId, userDetails.getMember());
        return ResponseUtil.response(StatusCode.EXIT_SUCCESS);
    }

    // 게임룸 방장 조회하기
    @GetMapping("/rooms/{roomId}/ownerInfo")
    public ResponseEntity<Map<String, String>> ownerInfo(@PathVariable Long roomId) {
        return ResponseUtil.response(gameRoomService.ownerInfo(roomId));
    }
}
  • 게임룸 관련 CRUD 작업을 수행하는 REST 컨트롤러, 각 메서드는 HTTP 요청을 처리하고, GameRoomService를 통해 비즈니스 로직을 실행한 후 응답을 반환함
    • 컨트롤러는 RESTful 원칙을 따르며, 각 엔드포인트는 게임룸 관련 작업을 명확하게 수행하고 ResponseUtil.response() 메소드를 통해 일관된 응답 구조를 제공합니다.
  • 게임룸 생성(makeGameRoom)
    • @PostMapping("/rooms") : 사용자로부터 게임룸 생성 요청을 받아 처리합니다. 사용자의 상세 정보와 게임룸 정보가 담긴 DTO를 매개변수로 받습니다.
  • 게임룸 전체 조회(mainPage)
    • @GetMapping("/rooms") : 페이징 처리된 게임룸 목록을 조회합니다. Pageable 객체를 사용하여 페이징과 정렬을 처리합니다.
  • 게임룸 키워드 조회(searchGame)
    • @GetMapping("/rooms/search") : 키워드를 포함하는 게임룸을 조회합니다. 검색어와 페이징 정보를 매개변수로 받습니다.
  • 게임룸 입장(enterGame)
    • @PostMapping("/rooms/{roomId}") : 특정 게임룸에 사용자를 입장시킵니다. 게임룸 ID와 사용자의 상세 정보를 매개변수로 받습니다.
  • 게임룸 입장 검증(enterVerify)
    • @PostMapping("/rooms/{roomId}/verify") : 특정 게임룸에 사용자의 입장을 검증합니다. 게임룸 ID와 사용자의 상세 정보를 매개변수로 받습니다.
  • 게임룸 나가기 (roomExit)
    • @DeleteMapping("/rooms/{roomId}/exit") : 사용자가 특정 게임룸에서 나가는 작업을 처리합니다. 게임룸 ID와 사용자의 상세 정보를 매개변수로 받습니다.
  • 게임룸 방장 정보 조회 (ownerInfo)
    • @GetMapping("/rooms/{roomId}/ownerInfo") : 특정 게임룸의 방장 정보를 조회합니다. 게임룸 ID를 매개변수로 받습니다.

 

Service

GameRoomService

package com.example.namoldak.service;

import com.example.namoldak.domain.*;
import com.example.namoldak.domainModel.GameCommand;
import com.example.namoldak.domainModel.GameQuery;
import com.example.namoldak.domainModel.MemberCommand;
import com.example.namoldak.domainModel.MemberQuery;
import com.example.namoldak.dto.RequestDto.GameRoomRequestDto;
import com.example.namoldak.dto.ResponseDto.GameRoomResponseDto;
import com.example.namoldak.dto.ResponseDto.GameRoomResponseListDto;
import com.example.namoldak.dto.ResponseDto.MemberResponseDto;
import com.example.namoldak.repository.SessionRepository;
import com.example.namoldak.util.GlobalResponse.CustomException;
import com.example.namoldak.util.security.UserDetailsImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;

import static com.example.namoldak.util.GlobalResponse.code.StatusCode.*;

// 기능 : 게임룸 서비스
@Slf4j
@RequiredArgsConstructor
@Service
public class GameRoomService {
    // 의존성 주입
    private final GameService gameService;
    private final MemberQuery memberQuery;
    private final MemberCommand memberCommand;
    private final GameQuery gameQuery;
    private final GameCommand gameCommand;
    private final SessionRepository sessionRepository = SessionRepository.getInstance();


    // 게임룸 전체 조회
    @Transactional
    public GameRoomResponseListDto mainPage(Pageable pageable) {

        // DB에 저장된 모든 Room들을 리스트형으로 저장 + 페이징 처리
        Page<GameRoom> rooms = gameQuery.findGameRoomByPageable(pageable);

        // DB에 저장된 모든 Room들을 리스트로 가져와
        List<GameRoom> roomList = gameQuery.findAllGameRoomList();

        // 필요한 키값들을 반환하기 위해서 미리 Dto 리스트 선언
        List<GameRoomResponseDto> gameRoomList = new ArrayList<>();

        for (GameRoom room : rooms){
            // 모든 Room들이 모여있는 rooms에서 하나씩 추출 -> Room 객체 활용해서 GameRoomMember DB에서 찾은 후 리스트에 저장
            List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByGameRoom(room);
            // 필요한 키값들을 반환하기 위해서 미리 Dto 리스트 선언
            List<MemberResponseDto> memberList = new ArrayList<>();
            for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
                // GameRoomMember에 저장된 멤버 아이디로 DB 조회 후 데이터 저장
                Member eachMember = memberQuery.findMemberById(gameRoomAttendee.getMember().getId());

                // MemberResponseDto에 빌더 방식으로 각각의 데이터 값 넣어주기
                MemberResponseDto memberResponseDto = MemberResponseDto.builder()
                        .memberId(eachMember.getId())
                        .email(eachMember.getEmail())
                        .nickname(eachMember.getNickname())
                        .build();
                // 완성된 Dto 리스트에 추가
                memberList.add(memberResponseDto);
            }
            // 그냥 Member가 아니라 GameRoomMember를 담아야 하기 때문에 GameRoomResponseDto로 다시 한번 감쌈
            GameRoomResponseDto gameRoomResponseDto = GameRoomResponseDto.builder()
                    .id(room.getGameRoomId())
                    .roomName(room.getGameRoomName())
                    .roomPassword(room.getGameRoomPassword())
                    .member(memberList)
                    .memberCnt(memberList.size())
                    .owner(room.getOwner())
                    .status(room.isStatus())
                    .build();

            // memberList에 데이터가 있다면 gameRoomList에 gameRoomResponseDto 추가
            // for문 끝날 때 까지 반복
            if (!memberList.isEmpty()) {
                gameRoomList.add(gameRoomResponseDto);
            }
        }
        int totalPage = rooms.getTotalPages();
        return new GameRoomResponseListDto(totalPage, gameRoomList);
    }

    // 게임룸 생성
    @Transactional
    public Map<String, String> makeGameRoom(Member member, GameRoomRequestDto gameRoomRequestDto) {

        // 게임방 만든 횟수 추가
        member.updateMakeRoom(1L);
        memberCommand.saveMember(member);

        // 빌더 활용해서 GameRoom 엔티티 데이터 채워주기
        GameRoom gameRoom = GameRoom.builder()
                .gameRoomName(gameRoomRequestDto.getGameRoomName())
                .gameRoomPassword(gameRoomRequestDto.getGameRoomPassword())
                .owner(member.getNickname())
                .status(true)
                .build();

        // DB에 데이터 저장
        gameCommand.saveGameRoom(gameRoom);

        // 생성자로 gameRoom, member 데이터를 담은 GameRoomMember 객체 완성
        GameRoomAttendee gameRoomAttendee = new GameRoomAttendee(gameRoom, member);

        // GameRoomMember DB에 해당 데이터 저장
        gameCommand.saveGameRoomAttendee(gameRoomAttendee);

        // data에 데이터를 담아주기 위해 HashMap 생성
        Map<String, String> roomInfo = new HashMap<>();

        // 앞에 키 값에 뒤에 밸류 값을 넣어줌
        roomInfo.put("gameRoomName", gameRoom.getGameRoomName());
        roomInfo.put("roomId", Long.toString(gameRoom.getGameRoomId()));
        roomInfo.put("gameRoomPassword", gameRoom.getGameRoomPassword());
        roomInfo.put("owner", gameRoom.getOwner());
        roomInfo.put("status", String.valueOf(gameRoom.isStatus()));

        return roomInfo;
    }

    // 게임룸 입장
    @Transactional
    public Map<String, String> enterGame(Long roomId, Member member) {

        // roomId로 DB에서 데이터 찾아와서 담음
        GameRoom enterGameRoom = gameQuery.findGameRoomByRoomIdLock(roomId);

        // 방의 상태가 false면 게임이 시작 중이거나 가득 찬 상태이기 때문에 출입이 불가능
        if (!enterGameRoom.isStatus()) {
            // 뒤로 넘어가면 안 되니까 return으로 호다닥 끝내버림
            throw new CustomException(ALREADY_PLAYING);
        }

        // 입장하려는 게임방을 이용해서 GameRoomMember DB에서 유저 정보 전부 빼와서 리스트형에 저장 (입장 정원 확인 용도)
        List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByGameRoom(enterGameRoom);

        // 만약 방에 4명이 넘어가면
        if (gameRoomAttendeeList.size() > 3) {
            // 입장 안 된다고 입구컷
            throw new CustomException(CANT_ENTER);
        }

        // 멤버가 방에 입장한 횟수 1개 증가
        member.updateEnterGame(1L);
        memberCommand.saveMember(member);

        // for문으로 리스트에서 gameRoomMember 하나씩 빼주기
        for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
            // gameRoomMember에서 얻은 유저 아이디로 Member 객체 저장
            Member member1 = memberQuery.findMemberById(gameRoomAttendee.getMember().getId());
            // 현재 들어가려는 유저의 ID와 게임에 들어가있는 멤버의 ID가 똑같으면 입구컷 해버림
            if (member.getId().equals(member1.getId())) {
//                return new PrivateResponseBody(StatusCode.MEMBER_DUPLICATED, "이미 입장해있닭!!");
                throw new CustomException(MEMBER_DUPLICATED);
            }
        }

        GameRoomAttendee gameRoomAttendee = new GameRoomAttendee(enterGameRoom, member);

        // DB에 데이터 저장
        gameCommand.saveGameRoomAttendee(gameRoomAttendee);

        Map<String, Object> contentSet = new HashMap<>();
        contentSet.put("owner", enterGameRoom.getOwner());
        contentSet.put("memberCnt", gameRoomAttendeeList.size());
        contentSet.put("enterComment", roomId + "번 방에" + String.valueOf(member.getId()) + "님이 입장하셨습니닭!");

        // 게임 메세지 전송
        gameService.sendGameMessage(roomId, GameMessage.MessageType.ENTER, contentSet, null, member.getNickname());

        // 해시맵으로 데이터 정리해서 보여주기
        Map<String, String> roomInfo = new HashMap<>();

        roomInfo.put("gameRoomName", enterGameRoom.getGameRoomName());
        roomInfo.put("roomId", String.valueOf(enterGameRoom.getGameRoomId()));
        roomInfo.put("owner", enterGameRoom.getOwner());
        roomInfo.put("status", String.valueOf(enterGameRoom.isStatus()));

        return roomInfo;
    }

    // 비정상 게임룸 접속자 방지
    public void enterVerify(Long roomId, UserDetailsImpl userDetails) {
        // 비회원일 경우 에러 메세지 보내기
        if (userDetails == null) {
            throw new CustomException(INVALID_TOKEN);
        }

        // 검증을 위한 카운트 미리 선언
        int cnt = 0;

        // 해당 방의 모든 참가자들 리스트로 저장
        List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByRoomId(roomId);

        // 참가자의 닉네임과 접속한 사람의 닉네임이 동일하면 cnt 1개씩 올림
        for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
            if (userDetails.getMember().getNickname().equals(gameRoomAttendee.getMemberNickname())){
                cnt++;
            }
        }

        // cnt가 1이 아닐 경우 뭔가가 오류가 있기 때문에 들어갈 수 없다고 에러 메세지 띄워줌
        if (cnt != 1) {
            throw new CustomException(BAD_REQUEST);
        }
    }

    // 게임룸 키워드 조회
    public GameRoomResponseListDto searchGame(Pageable pageable, String keyword) {
        // 게임룸 이름을 keyword(검색어)로 잡고 조회 + 페이징 처리
        Page<GameRoom> rooms = gameQuery.findGameRoomByContainingKeyword(pageable, keyword);

        if(rooms.isEmpty()){
            throw new CustomException(NOT_EXIST_ROOMS);
        }

        List<GameRoomResponseDto> gameRoomList = new ArrayList<>();
        for (GameRoom room : rooms) {
            // 게임룸에 입장해 있는 멤버 조회
            List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByGameRoom(room);
            List<MemberResponseDto> memberList = new ArrayList<>();

            for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList){
                Member eachMember = memberQuery.findMemberById(gameRoomAttendee.getMember().getId());

                // 멤버로부터 필요한 정보인 id, email, nickname만 Dto에 담아주기
                MemberResponseDto memberResponseDto = MemberResponseDto.builder()
                        .memberId(eachMember.getId())
                        .email(eachMember.getEmail())
                        .nickname(eachMember.getNickname())
                        .build();
                // 담긴 정보 저장
                memberList.add(memberResponseDto);
            }
            // 게임룸에 필요한 정보를 Dto에 담아주기
            GameRoomResponseDto gameRoomResponseDto = GameRoomResponseDto.builder()
                    .id(room.getGameRoomId())
                    .roomName(room.getGameRoomName())
                    .roomPassword(room.getGameRoomPassword())
                    .member(memberList)
                    .memberCnt(memberList.size())
                    .owner(room.getOwner())
                    .status(room.isStatus())
                    .build();
            // 방에 멤버가 1명 이상이라면, 담아줬던 데이터 저장하기
            if (!memberList.isEmpty()) {
                gameRoomList.add(gameRoomResponseDto);
            }
        }
        // 저장된 정보가 담긴 리스트를 반환
        int totalPage = rooms.getTotalPages();
        return new GameRoomResponseListDto(totalPage, gameRoomList);
    }

    // 방 나가기
    @Transactional
    public void roomExit(Long roomId, Member member) {
        // 나가려고 하는 방 정보 DB에서 불러오기
        GameRoom enterGameRoom = gameQuery.findGameRoomByRoomId(roomId);

        // 나가려고 하는 GameRoomMember를 member 객체로 DB에서 조회
        GameRoomAttendee gameRoomAttendee = gameQuery.findAttendeeByMember(member);

        // 위에서 구한 GameRoomMemeber 객체로 DB 데이터 삭제
        gameCommand.deleteGameRoomAttendee(gameRoomAttendee);

        // 게임방에 남아있는 유저들 구하기
        List<GameRoomAttendee> existGameRoomAttendee = gameQuery.findAttendeeByGameRoom(enterGameRoom);

        // 남아있는 유저의 수가 0명이라면 게임방 DB에서 데이터 삭제
        if (existGameRoomAttendee.size() == 0) {
            // 혼자 있을 때 방에서 나간 횟수 증가
            member.updateSoloExit(1L);
            memberCommand.saveMember(member);
            gameCommand.deleteGameRoom(enterGameRoom);

            // 게임 채팅방도 삭제해줌
            sessionRepository.deleteAllclientsInRoom(roomId);
        }

        // 게임이 시잓된 상태에서 나갔을 경우
        if (!enterGameRoom.isStatus()){
                // 게임을 끝내버림
                gameService.forcedEndGame(roomId, member.getNickname());
        }

        // 방을 나갈 경우의 알림 문구와 나간 이후의 방 인원 수를 저장하기 위한 해시맵
        Map<String, Object> contentSet = new HashMap<>();
        contentSet.put("memberCnt", existGameRoomAttendee.size());
        contentSet.put("alert", member.getNickname() + " 님이 방을 나가셨습니닭!");

        // 누가 나갔는지 알려줄 메세지 정보 세팅
        gameService.sendGameMessage(roomId, GameMessage.MessageType.LEAVE, contentSet, null, member.getNickname());

        // 만약에 나간 사람이 그 방의 방장이고 남은 인원이 0명이 아닐 경우에
        if (member.getNickname().equals(enterGameRoom.getOwner()) && !existGameRoomAttendee.isEmpty()){
            // 남은 사람들의 수 만큼 랜덤으로 돌려서 나온 멤버 ID
            String nextOwner = existGameRoomAttendee.get((int) (Math.random() * existGameRoomAttendee.size())).getMemberNickname();
            enterGameRoom.setOwner(nextOwner);
            gameService.sendGameMessage(roomId, GameMessage.MessageType.NEWOWNER, null, null, nextOwner);
        }
    }

    // 방장 정보 조회
    public Map<String, String> ownerInfo(Long roomId) {
        // 전달받은 roomId로 DB 조회 후 저장
        GameRoom enterRoom = gameQuery.findGameRoomByRoomId(roomId);
        // 방에서 방장의 닉네임을 저장
        String ownerNickname = enterRoom.getOwner();
        // 닉네임을 통해서 유저 객체를 불러온 후에 ID를 저장
        Member member = memberQuery.findMemberByNickname(ownerNickname);
        String ownerId = member.getId().toString();

        // 데이터를 전달할 해시맵 생성 후 넣어주기
        Map<String, String> ownerInfo = new HashMap<>();
        ownerInfo.put("ownerId", ownerId);
        ownerInfo.put("ownerNickname", ownerNickname);

        return ownerInfo;
    }

    // signalHandler에서 세션 끊김을 감지했을 때 게임방에서 참가자 정보를 정리
    public void exitGameRoomAboutSession(String nickname, Long roomId) {
        Member member = memberQuery.findMemberByNickname(nickname);
        List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByRoomId(roomId);
        for(GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
            if(nickname.equals(gameRoomAttendee.getMemberNickname())){
                roomExit(roomId, member);
            }
        }
    }
}
  • 게임룸 관련 서비스 로직을 담당하는 스프링 서비스 컴포넌트, 이 클래스는 게임룸의 생성, 조회, 입장, 퇴장 및 관련 정보 처리를 위한 다양한 메서드를 제공함
  • 게임룸 생성(makeGameRoom)
    • 새 게임룸을 생성하고, 생성 정보를 데이터베이스에 저장합니다.
  • 게임룸 전체 조회 (mainPage)
    • 페이징 처리된 게임룸 목록을 조회하고, 관련 정보를 DTO로 변환하여 반환합니다.
  • 게임룸 키워드 조회 (searchGame)
    • 특정 키워드를 포함하는 게임룸을 페이징 처리하여 조회합니다.
  • 게임룸 입장 (enterGame)
    • 사용자를 게임룸에 입장시키며, 게임룸이 가득 찼거나 게임이 이미 시작된 경우 예외 처리를 수행합니다.
  • 게임룸 입장 검증 (enterVerify)
    • 사용자의 게임룸 입장을 검증합니다.
  • 게임룸 퇴장 (roomExit)
    • 사용자를 게임룸에서 퇴장시키고, 필요한 데이터 조정 및 클린업을 수행합니다.
  • 방장 정보 조회 (ownerInfo)
    • 게임룸의 방장 정보를 조회합니다.
  • 세션 끊김 시 게임룸 퇴장 처리 (exitGameRoomAboutSession)
    • 웹소켓 세션 종료 시 해당 사용자를 게임룸에서 자동으로 퇴장 처리합니다.
  • 각 메서드는 비즈니스 로직을 수행하기 위해 GameQuery, GameCommand, MemberQuery, MemberCommand, GameService, SessionRepository 등의 다른 컴포넌트와 협력하고 트랜잭션 관리는 @Transactional 어노테이션을 통해 수행되며, 이는 메서드 실행 도중 예외 발생 시 변경사항을 롤백하여 데이터 일관성을 보장함

GameService

package com.example.namoldak.service;

import com.example.namoldak.domain.*;
import com.example.namoldak.domainModel.GameCommand;
import com.example.namoldak.domainModel.GameQuery;
import com.example.namoldak.domainModel.MemberCommand;
import com.example.namoldak.domainModel.MemberQuery;
import com.example.namoldak.dto.RequestDto.GameDto;
import com.example.namoldak.dto.ResponseDto.VictoryDto;
import com.example.namoldak.util.GlobalResponse.CustomException;
import com.example.namoldak.util.GlobalResponse.code.StatusCode;
import com.example.namoldak.util.converter.GameStartSetConverter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;

import static com.example.namoldak.util.GlobalResponse.code.StatusCode.*;

// 기능 : 게임 진행 서비스
@Slf4j
@RequiredArgsConstructor
@Service
public class GameService {
    private final SimpMessageSendingOperations messagingTemplate;
    private final RewardService rewardService;
    private final MemberQuery memberQuery;
    private final MemberCommand memberCommand;
    private final GameQuery gameQuery;
    private final GameCommand gameCommand;
    private final GameStartSetConverter gameStartSetConverter;


    // 게임 시작
    @Transactional
    public void gameStart(Long roomId, GameDto gameDto) {

        // 현재 입장한 게임방의 정보를 가져옴
        GameRoom gameRoom = gameQuery.findGameRoomByRoomId(roomId);

        // 게임 시작은 방장만이 할 수 있음
        if (!gameDto.getNickname().equals(gameRoom.getOwner())) {
            throw new CustomException(StatusCode.UNAUTHORIZE);
        }

        // 게임방에 입장한 멤버들 DB(GameRoomMember)에서 가져오기
        List<GameRoomAttendee> gameRoomAttendees = gameQuery.findAttendeeByGameRoom(gameRoom);
        // 게임방의 상태를 start 상태로 업데이트
        gameRoom.setStatus(false);

        // 랜덤으로 뽑은 키워드의 카테고리
        String category = Category.getRandom().name();
        // 같은 카테고리를 가진 키워드 리스트 만들기
        List<Keyword> keywordList = getRandomKeyword(gameRoomAttendees.size(), category);
        // 웹소켓으로 방에 참가한 인원 리스트 전달을 위한 리스트 (닉네임만 필요하기에 닉네임만 담음)
        List<String> memberNicknameList = getNicknameList(gameRoomAttendees);
        //게임룸 멤버한테 키워드 배당
        Map<String, String> keywordToMember = matchKeywordToMember(keywordList, memberNicknameList);

        GameStartSet gameStartSet = GameStartSet.builder()
                .roomId(roomId)
                .category(category)
                .keywordToMember(gameStartSetConverter.getStrFromMap(keywordToMember))
                .round(0)
                .spotNum(0)
                .winner("")
                .gameStartTime(System.currentTimeMillis())
                .build();

        // StartSet 저장
        gameCommand.saveGameStartSet(gameStartSet);

        GameStartSet searchOneGameStartSet = gameQuery.findGameStartSetByRoomId(roomId);
        log.info("카테고리 : " + searchOneGameStartSet.getCategory());
        for (String memberNick : memberNicknameList) {
            log.info("키워드 : " + keywordToMember.get(memberNick));
        }

        // 웹소켓으로 전달드릴 content 내용
        Map<String, Object> startSet = new HashMap<>();
        startSet.put("category", gameStartSet.getCategory()); // 카테고리
        startSet.put("keyword", gameStartSetConverter.getMapFromStr(gameStartSet.getKeywordToMember())); // 키워드
        startSet.put("memberList", memberNicknameList); // 방에 존재하는 모든 유저들
        startSet.put("startAlert", "총 8라운드닭! 초록색으로 하이라이트된 사람만 말할 수 있고 다른 사람들은 마이크 기능이 제한되니까 채팅으로 알려주면 된닭!");

        sendGameMessage(roomId, GameMessage.MessageType.START, startSet, null, null);
    }

    // 건너뛰기
    @Transactional
    public void gameSkip(GameDto gameDto, Long roomId) {
        String msg = gameDto.getNickname() + "님이 건너뛰기를 선택하셨습니다.";
        sendGameMessage(roomId, GameMessage.MessageType.SKIP, msg, null, gameDto.getNickname());
    }


    @Transactional
    public void spotlight(Long roomId) {

        GameRoom playRoom = gameQuery.findGameRoomByRoomId(roomId);

        // 해당 게임룸의 게임셋을 조회
        GameStartSet gameStartSet = gameQuery.findGameStartSetByRoomId(roomId);

        // 게임이 진행이 불가한 상태라면 초기화 시켜야 함
        if (playRoom.isStatus()) { // false : 게임이 진행 중, true : 게임 시작 전
            gameStartSet.setRound(0);
            gameStartSet.setSpotNum(0);
            gameCommand.saveGameStartSet(gameStartSet);
        }

        // 유저들 정보 조회
        List<GameRoomAttendee> memberListInGame = gameQuery.findAttendeeByGameRoom(playRoom);

        // 라운드 진행 중
        if (gameStartSet.getSpotNum() < memberListInGame.size()) {

            // 현재 스포트라이트 받는 멤버
            Member spotMember = memberQuery.findMemberById(memberListInGame.get(gameStartSet.getSpotNum()).getMember().getId());

            // 메세지 알림
            String msg = spotMember.getNickname() + "님의 차례입니닭!";
            sendGameMessage(roomId, GameMessage.MessageType.SPOTLIGHT, msg, spotMember.getNickname(), spotMember.getNickname());

            // 다음 차례로!
            gameStartSet.setSpotNum(gameStartSet.getSpotNum() +1);
            gameCommand.saveGameStartSet(gameStartSet);

        } else if (gameStartSet.getSpotNum() == memberListInGame.size()) {

            if (gameStartSet.getRound() < 7) {
                // 한 라운드 종료, 라운드 +1 , 위치 정보 초기화
                gameStartSet.setRound(gameStartSet.getRound() +1);
                gameStartSet.setSpotNum(0);
                gameCommand.saveGameStartSet(gameStartSet);
                spotlight(roomId);

                // 0번부터 시작이다
            } else if (gameStartSet.getRound() == 7) {
                // 메세지 알림
                String msg = "너흰 전부 바보닭!!!";
                sendGameMessage(roomId, GameMessage.MessageType.STUPID, msg, null, null);

                forcedEndGame(roomId, null);
            }
        }
    }

    // 정답
    @Transactional
    public void gameAnswer(Long roomId, GameDto gameDto) {
        // 모달창에 작성한 정답
        String answer = gameDto.getAnswer().replaceAll(" ", "");

        // gameStartSet 불러오기
        GameStartSet gameStartSet = gameQuery.findGameStartSetByRoomId(roomId);

        // 정답을 맞추면 게임 끝
        if (gameStartSetConverter.getMapFromStr(gameStartSet.getKeywordToMember()).get(gameDto.getNickname()).equals(answer)){

            // 정답자
            gameStartSet.setWinner(gameDto.getNickname());
            gameCommand.saveGameStartSet(gameStartSet);

            // 메세지 알림
            String msg = gameDto.getNickname() + "님이 작성하신" + answer + "은(는) 정답입니닭!";
            sendGameMessage(roomId, GameMessage.MessageType.SUCCESS, msg, gameDto.getNickname(), null);
        } else {
            // 메세지 알림
            String msg = gameDto.getNickname() + "님이 작성하신" + answer + "은(는) 정답이 아닙니닭!";
            sendGameMessage(roomId, GameMessage.MessageType.FAIL, msg, gameDto.getNickname(), null);
        }
    }

    // 게임 강제 종료
    @Transactional
    public void forcedEndGame(Long roomId, String nickname){

        // 현재 게임방 정보 불러오기
        GameRoom enterGameRoom = gameQuery.findGameRoomByRoomId(roomId);

        // 메세지 알림
        String msg = nickname == null ? "게임이 종료되었닭!!" : nickname + " 님이 방에서 탈주해서 강제 종료되었닭!!";
        sendGameMessage(roomId, GameMessage.MessageType.FORCEDENDGAME, msg, null, null);

        // DB에서 게임 셋팅 삭제
        gameCommand.deleteGameStartSetByRoomId(roomId);

        // 현재 방 상태 정보를 true로 변경
        enterGameRoom.setStatus(true);
    }

    // 게임 정상 종료
    @Transactional
    public void endGame(Long roomId){
        // 승리자와 패배자를 list로 반환할 DTO 생성
        VictoryDto victoryDto = new VictoryDto();

        // 방 게임셋 정보 불러오기
        GameStartSet gameStartSet = gameQuery.findGameStartSetByRoomId(roomId);

        // 현재 게임룸 데이터 불러오기
        GameRoom enterGameRoom = gameQuery.findGameRoomByRoomId(roomId);

        // 불러온 게임룸으로 들어간 GameRoomMember들 구하기
        List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByGameRoom(enterGameRoom);

        // 닉네임을 구하기 위해서 멤버 객체를 담을 리스트 선언
        List<Member> memberList = new ArrayList<>();

        // for문으로 하나씩 빼서 DB 조회 후 List에 넣어주기
        for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList){
            Member member = memberQuery.findMemberById(gameRoomAttendee.getMember().getId());
            // 멤버 총 게임 횟수 증가
            member.updateTotalGame(1L);
            memberCommand.saveMember(member);
            memberList.add(member);
            rewardService.createTotalGameReward(member);
        }
        Long startTime = gameStartSet.getGameStartTime();
        Long currentTime = System.currentTimeMillis();
        Long playTime = (currentTime - startTime) / 1000;
        // member의 닉네임이 정답자와 같지 않을 경우 전부 Loser에 저장하고 같을 경우 Winner에 저장
        for (Member member : memberList){
            if (!member.getNickname().equals(gameStartSet.getWinner())){
                victoryDto.setLoser(member.getNickname());
                // 멤버 패배 기록 추가
                member.updateLoseNum(1L);
                member.updatePlayTime(playTime);
                memberCommand.saveMember(member);
                rewardService.createLoseReward(member);
            } else {
                victoryDto.setWinner(member.getNickname());
                // 멤버 승리 기록 추가
                member.updateWinNum(1L);
                member.updatePlayTime(playTime);
                memberCommand.saveMember(member);
                rewardService.createWinReward(member);
            }
        }

        // 메세지 알림
        sendGameMessage(roomId, GameMessage.MessageType.ENDGAME, gameStartSet.getKeywordToMember(), null, null);

        // DB에서 게임 셋팅 삭제
        gameCommand.deleteGameStartSetByRoomId(roomId);

        // 현재 방 상태 정보를 true로 변경
        enterGameRoom.setStatus(true);
    }

    // 게임 운영자 메세지 전송
    public <T> void sendGameMessage(Long roomId, GameMessage.MessageType type, T Content, String nickname, String sender) {

        String senderName = sender == null ? "양계장 주인" : sender;

        GameMessage<T> gameMessage = new GameMessage<>();
        gameMessage.setRoomId(Long.toString(roomId));
        gameMessage.setType(type);
        gameMessage.setSender(senderName);
        gameMessage.setContent(Content);
        gameMessage.setNickname(nickname);

        messagingTemplate.convertAndSend("/sub/gameRoom/" + roomId, gameMessage);
    }

    // 키워드 생성
    public List<Keyword> getRandomKeyword(int size, String category) {
        // 같은 카테고리를 가진 키워드 리스트 만들기
        List<Keyword> keywordList;

        if (size == 4) {
            // 참여 멤버가 4명 이라면, 랜덤으로 키워드 4장이 담긴 리스트를 만들어 준다.
            keywordList = gameQuery.findTop4KeywordByCategory(category);
        } else if (size == 3) {
            // 참여 멤버가 3명 이라면, 랜덤으로 키워드 3장이 담긴 리스트를 만들어 준다.
            keywordList = gameQuery.findTop3KeywordByCategory(category);
        } else {
            throw new CustomException(NOT_ENOUGH_MEMBER);
        }

        return keywordList;
    }

    // 키워드와 참가자 매칭
    public Map<String, String> matchKeywordToMember(List<Keyword> keywordList, List<String> memberNicknameList) {
        Map<String, String> keywordToMember = new HashMap<>();

        //게임룸 멤버한테 키워드 배당
        for (int i = 0; i < keywordList.size(); i++) {
            keywordToMember.put(memberNicknameList.get(i), keywordList.get(i).getWord());
        }

        return keywordToMember;
    }

    // 방의 참가자들 닉네임
    public List<String> getNicknameList(List<GameRoomAttendee> gameRoomAttendees) {
        // 웹소켓으로 방에 참가한 인원 리스트 전달을 위한 리스트 (닉네임만 필요하기에 닉네임만 담음)
        List<String> memberNicknameList = new ArrayList<>();

        for (GameRoomAttendee gameRoomAttendee : gameRoomAttendees) {
            memberNicknameList.add(gameRoomAttendee.getMemberNickname());
        }

        return memberNicknameList;
    }
}
  • 게임의 진행에 필요한 주요 서비스 로직을 처리함, 이 클래스에서는 게임의 시작, 진행, 종료, 메시지 전송 등 다양한 기능을 제공하며, 게임룸 관리와 플레이어 상호작용을 중개함
  • 게임 시작 (gameStart)
    게임 시작 조건을 확인하고, 게임룸의 상태를 업데이트합니다.
    랜덤 키워드 배정 및 게임 시작 정보를 게임룸 참가자에게 전송합니다.
  • 건너뛰기 (gameSkip)
    플레이어가 게임 내에서 건너뛰기 액션을 선택할 때 처리합니다.
  • 발언권 부여 (spotlight)
    특정 플레이어에게 게임 내 발언권을 부여하고 게임 진행 상태를 관리합니다.
  • 정답 처리 (gameAnswer)
    플레이어의 정답 제출을 처리하고, 정답 여부에 따라 게임 상태를 업데이트합니다.
  • 게임 강제 종료 (forcedEndGame)
    게임 강제 종료 시 필요한 처리를 수행합니다.
  • 게임 정상 종료 (endGame)
    게임의 정상 종료 시 점수 집계 및 보상 처리를 수행합니다.
  • 메시지 전송 (sendGameMessage)
    게임 상태 변경, 플레이어 액션, 시스템 메시지 등을 웹소켓을 통해 참가자에게 전송합니다.
  • 키워드 생성 및 매칭 (getRandomKeyword, matchKeywordToMember)
    게임룸 참가자에게 배정될 랜덤 키워드를 생성하고 매칭합니다.
  • 닉네임 리스트 생성 (getNicknameList)
    게임룸 참가자의 닉네임 리스트를 생성합니다.

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

Call by Reference  (0) 2024.04.04
WebSocket 활용 웹 게임 구현  (0) 2024.04.03
WebSocket - STOMP 2  (0) 2024.03.30
WebSocket - STOMP 1  (1) 2024.03.29
WebSocket - SockJS  (0) 2024.03.28