자프실 프로젝트 중간기록#2

박서영·2025년 12월 2일

✔️ 주제: 캐치 마인드


✅ 진행 상황

클래스 다이어그램 상황

  1. 클라이언트

  1. 서버
  2. 커맨드

1. 완료된 기능

✔️ View (클라이언트 UI 상황)

  • LoginPanel: 초기 게임 접속 화면
  • MainFrame: 게임 시작 화면. 캔버스, 채팅, 제시어, 색깔 바꾸는 패널 등 모든 패널을 가지고 있는 JFrame을 상속한 클래스
  • GamePanel: 게임 시작 및 게임 종료 버튼이 들어있는 JPanel 상속 클래스. MainFrame에 add하게됨
  • ColorPalette: 색을 바꿀 수 있도록 색깔 버튼이 있는 JPanel 상속 클래스.
  • ChatPanel: 채팅 UI가 있는 JPanel 상속 클래스.
  • CanvasPanel: 그림을 그리는 UI가 있는 JPanel 상속 클래스. 위에 제시어와 타이머 UI 존재. 아래에는 캔버스와 지우기 버튼 존재.

  • 초반 접속 화면: 서버 측 IP 주소와 사용자 이름 입력 후 접속

  • 게임 화면: 그림 그리고, 채팅을 통해 정답 맞추기
    • 초반에는 그림 그리는 것과 채팅이 불가하고, 게임 시작 버튼을 누르면 활성화.

✔️ 클라이언트 (Client)

public class Client {
 private Socket socket;
 private BufferedReader in;
 private PrintWriter out;
 private GameController gameController;

 //송수신
 public void send(String msg) {
     out.println(msg);
     out.flush();
 }
 
 public void listen() {
     Thread listen = new Thread(() -> {
         String msg;
         try {
             while ((msg = in.readLine())!=null) {
                 System.out.println("[DEBUG] 클라이언트 수신함: " + msg);
                 gameController.processMessage(msg);
             }
         } catch (IOException e) {

         }finally {
             disconnect();
         }
     });

     listen.start();
 }

    //연결
    public void connect(String ip, int port){
        try {
            socket = new Socket(ip, port);
            InputStreamReader input = new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8);
            in = new BufferedReader(input);
            OutputStreamWriter output = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);
            out = new PrintWriter(output, true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void disconnect() {
        try {
            socket.close();
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 public void setGameController(GameController gameController) {this.gameController = gameController;}

}
  • 연결 및 연결 종료 코드: 메소드 호출해서 연결 및 연결 종료

    public void connect(String ip, int port){
            try {
                socket = new Socket(ip, port);
                InputStreamReader input = new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8);
                in = new BufferedReader(input);
                OutputStreamWriter output = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);
                out = new PrintWriter(output, true);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public void disconnect() {
            try {
                socket.close();
                in.close();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    • connect(): 소켓을 새로 만들어서, 접속하려는 ip주소와 포트번호 인자로 전달.
      • 입력: BufferedReader로 소켓의 입력을 읽어오기
              `InputStreamReader`로 클라이언트 측 input을 읽어오기 
      • 출력: OutputStreamWriterPrintWriter를 통해 소켓에 출력함.
    • disconnect(): 연결 종료. 소켓, 입출력 모두 close()해서 자원 정리.
  • listen() 스레드: 서버 측에서 보내는 내용을 소켓으로 수신하기 위해 스레드로 생성.

    public void listen() {
         Thread listen = new Thread(() -> {
             String msg;
             try {
                 while ((msg = in.readLine())!=null) {
                     System.out.println("[DEBUG] 클라이언트 수신함: " + msg);
                     gameController.processMessage(msg);
                 }
             } catch (IOException e) {
    
             }finally {
                 disconnect();
             }
         });
    
         listen.start();
     }
    • 소켓에서 보내는 내용을 while문을 통해 읽어들여 내용이 있으면(= null이 아니면), 해당 내용을 컨트롤러로 보내서 파싱 등 분석.

✔️ 컨트롤러(Controller) | 클라이언트 측

  • 게임 관련 로직을 처리하는 게임 컨트롤러와, UI 관련 로직을 처리하는 ViewController로 분리
  • GameController

    public class GameController {
    
        private Client client;
        private final CommandFactory commandFactory = CommandFactory.getInstance();
        private ViewController viewController;
    
        //게임 접속-종료
        public void accessGame(String ip, String name) {
            ClientTest.startGame(ip);
            client.send("NAME:"+name);
        }
        public void noticeStart() {
            client.send("START:");
        }
    
        public void exitRoom() {
            viewController.endPanel();
            System.out.println("서버와의 연결이 종료됩니다.");
            client.disconnect();
            System.exit(0);
        }
    
        //메시지 수신(파싱)
        public void processMessage(String msg) {
            commandFactory.createCommand(viewController,msg);
        }
    
        //메시지 전송
        public void sendChat(String msg) {
            String sendMessage = "CHAT:"+msg;
            client.send(sendMessage);
        }
        public void sendErase() {
            client.send("ERASE:");
        }
        public void sendDrawing(Point from, Point to) {
            String msg = String.format("DRAW:%d:%d:%d:%d", from.x, from.y, to.x, to.y);
            client.send(msg);
        }
        public void sendColor(String colorCode) {client.send("COLOR:"+colorCode);}
    
        //Setter
        public void setViewController(ViewController viewController) {this.viewController = viewController;}
        public void setClient (Client client) {this.client = client;}
    }
    • 메시지 수신 및 전송을 담당하는 컨트롤러
      • 수신: 커맨드 패턴을 사용해서, 커맨드 팩토리에 역할 위임
      • 전송: 필드로 가지고 있는 클라이언트를 이용해 메시지 전송. 우선은 여기서 서버 측 파싱을 위한 헤더 작업 중.
    • 게임 시작 및 종료 담당
      • 시작: 시작 버튼 클릭 시 클라이언트 측에서 처리하지 않고, 우선 서버로 메시지 송신 후 브로드캐스트를 받아서 일괄적으로 처리
      • 종료: 게임 종료 시 서버로 종료 관련 메시지 송신
  • ViewController

    public class ViewController {
        private MainFrame mainFrame;
    
        public void updateDrawState (boolean state) {
            if (state) {
                mainFrame.enableDrawing();
                mainFrame.disableChatting();
            }
            else {
                mainFrame.disableDrawing();
                mainFrame.enableChatting();
            }
        }
    
        //게임 시작-종료 시 화면 세팅
        public void startPanel() {
            this.mainFrame.enablePanel();
            this.mainFrame.disableStartButton();
        }
        public void endPanel() {
            this.mainFrame.dispose();
        }
    
        //화면 업데이트
        public void updateKeyWord(String keyword) {
            mainFrame.updateKeyWord(keyword);
        }
        public void updateCanvasPanel(Point from, Point to) {mainFrame.updateCanvas(from, to);}
        public void eraseCanvasPanel() {mainFrame.eraseCanvas();}
        public void updateChatPanel(String msg) {
            mainFrame.updateTextArea(msg);
        }
        public void updateTimer(int time) {
            mainFrame.updateTimer(time);
        }
    
        public void updateCurrentColor(String colorCode) {mainFrame.updateCurrentColor(colorCode);}
    
        //Setter
        public void setMainFrame (MainFrame mainFrame) {this.mainFrame = mainFrame;}
    
    }
    • 서버에서 메시지 수신 시, 클라이언트 측 UI/GUI 업데이트 담당
    • 현재는 플레이어의 상태 (문제 출제자(그림을 그리는 사람), 정답을 맞추는 사람 두 가지) 관리

✔️ 커맨드 처리(파싱) (Command 패턴) | 클라이언트 측

  • Command: 인터페이스로 정의한 후 execute라는 메소드 생성.
public interface Command {
    public void execute(ViewController viewController, String msg);
}
  • Command 인터페이스를 구현한 구체 클래스들: 각 커맨드에 맞게 서버에서 보낸 메시지 파싱 후, 적합한 컨트롤러를 이용해 관련 동작 처리
public class ChatCommand implements Command {
    @Override
    public void execute(ViewController viewController, String data) {
        viewController.updateChatPanel(data);
    }
}
public class DrawCommand implements Command {
    @Override
    public void execute(ViewController viewController, String msg) {
        String[] tokens = msg.split(":");

        Point from = new Point(Integer.parseInt(tokens[0]),Integer.parseInt(tokens[1]));
        Point to = new Point(Integer.parseInt(tokens[2]),Integer.parseInt(tokens[3]));

        viewController.updateCanvasPanel(from, to);
    }
}

✔️ 커맨드 팩토리 (CommandFactory) 패턴 | 클라이언트 측

  • CommandFactory: 팩토리 패턴을 사용해, 들어온 커맨드에 매칭되는 커맨드 생성 및 실행. 해당 클래스는 싱글톤 패턴을 사용.
    public class CommandFactory {
        private static CommandFactory instance = null;
        Map<String, Command> commandMap = new HashMap<>();
    
        private CommandFactory() {
            commandMap.put("DRAW", new DrawCommand());
            commandMap.put("CHAT", new ChatCommand());
            commandMap.put("ERASE", new EraseCommand());
            commandMap.put("START", new StartCommand());
            commandMap.put("DRAWSTATE", new DrawStateCommand());
            commandMap.put("KEYWORD", new KeywordCommand());
            commandMap.put("COLOR", new ColorCommand());
            commandMap.put("TIMER", new TimerCommand());
        }
    
        public static CommandFactory getInstance() {
            if (instance == null) {
                instance = new CommandFactory();
            }
            return instance;
        }
    
        public void createCommand(ViewController viewController, String msg) {
            String[] tokens = msg.split(":",2);
    
            String data;
            if (tokens.length > 1) data = tokens[1];
            else data = null;
    
            Command commandProcessor = commandMap.get(tokens[0]);
    
            if (commandProcessor == null) viewController.updateChatPanel(msg);
            else commandProcessor.execute(viewController, data);
        }
    
    }
    • Map을 사용해서 들어온 문자열(헤더)에 매칭되는 종류의 커맨드 생성.
    • createCommand(): 들어온 문자열을 split한 후, 헤더(=첫 번째 문자열)를 보고 관련 커맨드 생성. 맵에서 매핑되는 커맨드 생성.
      • 매핑되는 커맨드가 없으면 일반 채팅으로 간주하고 처리
      • 매핑된다면, 해당 커맨드의 execute 메소드 호출해서 관련 로직 실행.

✔️ 서버 (Server)

  • 서버 관련 코드 파일 및 디렉토리 구조: command, controller, domain, repository, service로 구성
  • controller, domain, repository, service 내의 파일들

Server 클래스 코드

  • 클라이언트와 다르게 서버는 클래스 자체를 스레드를 상속
public class Server extends Thread{
	
	private ServerSocket serverSocket;
    private final List<GameRoom> gameRooms = new ArrayList<>();
    private GameService gameService;
    private CheckAnswerService checkAnswerService;
    private DrawerService drawerService;
    private GameWordService gameWordService;
    private QuizWordRepository quizWordRepository;

    public Server(){
        this.quizWordRepository = new QuizWordRepository();

        this.checkAnswerService = new CheckAnswerService();
        this.drawerService = new DrawerService();
        this.gameWordService = new GameWordService();

        this.gameService = new GameService(this.drawerService, this.gameWordService,
                this.checkAnswerService, this.quizWordRepository);
    }

    public void run() {
        makeGameRoom();
        try {
            serverSocket = new ServerSocket(50023);

            while (true) {
                System.out.println("서버 연결 대기중 ....");
                Socket socket = serverSocket.accept();
                System.out.println(socket.getInetAddress().getHostAddress()+"와 연결되었습니다.");
                addPlayer(socket);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void addPlayer(Socket socket) {
        GameRoom gameRoom = gameRooms.get(0);

        ConnectionController handler = new ConnectionController(socket, this);
        handler.setGameRoom(gameRoom);
        handler.start();
    }

    public void makeGameRoom() {
        GameRoom newGameRoom = new GameRoom(gameService);
        this.gameRooms.add(newGameRoom);
    }

}
  • 클래스 생성 시 생성자에서 게임 진행에 필요한 서비스 클래스들과 레포지토리 클래스 생성 서버 클래스의 필드에 저장

  • 스레드의 run():

    • 서버 생성 후에는 TCP의 연결을 위한 welcome 소켓을 열어놓고, 클라이언트들의 접속을 계속 기다림
    • 클라이언트가 접속하면 소켓을 accept() 해준 뒤, 해당 클라이언트와의 연결 소켓을 인자로 넘겨서 게임룸에 플레이어 추가
      • 우선은 게임룸이 여러 개인 것을 상정하고 코드를 작성했지만, 실질적으로 현재는 하나의 게임룸으로 진행
  • addPlayer(): 게임룸에 플레이어 추가.

    • 해당 클라이언트와의 연결 소켓에서 input을 읽어오고, 서버 측 메시지를 송신해 주기 위한 클래스인 ConnectionController 생성.
    • 해당 플레이어와의 연결을 관리하는 클래스(ConnectionController)의 필드로 그 플레이어가 속한 게임룸(GameRoom)을 기록
    • 각 클라이언트와의 연결이 각각 스레드로 생성되므로, 해당 스레드를 시작시킴.

✔️ 커맨드 처리(파싱) (Command 패턴) | 클라이언트 측

  • 기본적으로 클라이언트 측 커맨드 구조와 동일한 구조와 커맨드 팩토리 패턴 사용
  • 수신하게되는 커맨드가 조금 달라 클라이언트 측에는 존재하지 않는 커맨드 구체 클래스 존재
  • 서버 측에서 커맨드 수신 시 실행해야하는 동작/로직이 다르기에 execute 메소드의 내부만 변경

✔️ 컨트롤러 (Controller) | 서버 측

GameRoom

: 현재 게임룸과 게임 컨트롤러의 구분이 조금 애매한 부분이 존재

public class GameRoom {
    private final List<Player> players = new ArrayList<>();
    private String currentWord;
    private Player drawer;
    Timer gameTimer;
    private int remainingTime = 30;
    Map<Player, Integer> scoreBoard = new HashMap<>();
    private final GameService gameService;
    private final CommandFactory commandFactory = CommandFactory.getInstance();

    public GameRoom(GameService gameService) {
        this.gameService = gameService;
    }

    //게임룸에 플레이어 추가/삭제 로직
    public void addPlayer(Player p) {
        players.add(p);
        scoreBoard.put(p, 0); // 점수 초기값 = 0
        if(drawer == null) {
            drawer = p; // 첫번째로 들어오는 사람 자동으로 drawer 배정
        }
    }
    public void removePlayer(Player p) {
        players.remove(p);
        scoreBoard.remove(p);

        // drawer가 나간 경우 다음 drawer 선택
        if (drawer != null && drawer.equals(p)) {
            if (players.isEmpty()) {
                drawer = null;
            } else {
                drawer = gameService.selectNextDrawer(this);
            }
        }

        broadcastToRoom(p.getName()+"님이 방을 나가셨습니다.");
        System.out.println(p.getName()+"님이 방을 나가셨습니다.");
    }

    //메시지 처리 관련 로직
    //들어오는 메시지 1차 처리(각 서비스로 전달)
    public void processMessage (Player player, String msg) {
        commandFactory.createCommand(this, msg, player);
    }

    public void broadcastToRoom (String msg) {
        for(Player p : players) {
            p.sendMessage(msg);
        }
    }

    //getter & setter
    ...

    // 타이머 관련 메소드
    public void startTimer() {
        // 기존 타이머가 있으면 취소
        if (gameTimer != null) {
            gameTimer.cancel();
        }

        remainingTime = 30;
        gameTimer = new Timer();

        gameTimer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                if (remainingTime > 0) {
                    broadcastToRoom("TIMER:" + remainingTime);
                    remainingTime--;
                } else {
                    // 시간 종료 시 다음 라운드로
                    gameTimer.cancel();
                    broadcastToRoom("[System] 시간이 종료되었습니다!");
                    gameService.nextRound(GameRoom.this);
                }
            }
        }, 0, 1000); // 0초 후 시작, 1초마다 실행
    }

    public void stopTimer() {
        if (gameTimer != null) {
            gameTimer.cancel();
            gameTimer = null;
        }
    }
}
  • 게임 컨트롤러에서 상당히 다양한 부분을 담당하고 있어서 분리 필요
    • 게임룸에 접속하는 플레이어 추가/삭제 등 관리
    • 클라이언트 측에서 보내는 메시지 파싱 등 처리 작업
    • 타이머 관련 메소드 추가 예정

ConnectionController

: 클라이언트와 연결된 소켓 등의 관리. 클라이언트 측의 송수신 작업 처리.

public class ConnectionController extends Thread implements MessageSender {
    public Socket socket;
    private Server server;
    private BufferedReader in;
    private PrintWriter out;
    private GameRoom gameRoom;
    private Player player;

    //생성자: 입출력 설정 | 플레이어 만들기 (컨트롤러랑 1:1 대응이라..?) + 플레이어 게임룸에 추가해주기
    public ConnectionController(Socket socket, Server server) {
        this.socket = socket;
        this.server = server;
        try {
            InputStreamReader input = new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8);
            in = new BufferedReader(input);
            OutputStreamWriter output = new OutputStreamWriter(socket.getOutputStream(),StandardCharsets.UTF_8);
            out = new PrintWriter(output, true);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //플레이어 설정해주기
        this.player = new Player();
        player.setMessageSender(this);
    }

    //클라이언트가 보낸게 이쪽의 in으로 들어와서, 브로드캐스트(다른 클라이언트들)한테 보내짐
    public void run() {
        String msg;
            try {
                while ((msg=in.readLine()) != null) {
                    gameRoom.processMessage(this.player, msg);
                }
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println(player.getName()+"님의 연결이 종료되었습니다.");
            } finally {
                if (gameRoom != null) {
                    gameRoom.removePlayer(this.player);
                }
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    }

    //서버의 브로드캐스트 메소드에서 이 메소드 호출돼서 각 클라이언트들한테 재전송됨
    public void send(String msg) {
        System.out.println(msg);
        out.println(msg);
    }

    //getter&setter
    public void setGameRoom(GameRoom gameRoom) {
        this.gameRoom = gameRoom;
        this.gameRoom.addPlayer(player);
    }
    public Player getPlayer() {return player;}
    public void setPlayer(Player player) {this.player = player;}
}
  • run(): 스레드 실행 시. 클라이언트 측에서 메시지를 수신하게 되면 파싱 후 처리. 클라이언트 연결 종료 시 관련 메소드 호출해서 처리.
  • send(): 보내야할 메시지를 클라이언트에게 송신. 게임룸(=GameController)의 브로드캐스트 메소드에서 이 메소드를 호출해서 처리.

✔️ 서비스 (Service) | 서버 측

GameService

: 게임 진행에 관련된 로직을 처리하기 위한 가장 위의 컨트롤러로 세부 로직을 처리하는 서비스들을 필드로 둠

public class GameService {

    private static final int SCORE_PER_ANSWER = 10;
    private final DrawerService drawerService;
    private final GameWordService gameWordService;
    private final CheckAnswerService checkAnswerService;
    private final QuizWordRepository quizWordRepository;

    public GameService(DrawerService drawerService, GameWordService gameWordService,
                       CheckAnswerService checkAnswerService, QuizWordRepository quizWordRepository) {
        this.drawerService = drawerService;
        this.gameWordService = gameWordService;
        this.checkAnswerService = checkAnswerService;
        this.quizWordRepository = quizWordRepository;
    }

    public void startGame(GameRoom gameRoom) {
        gameRoom.broadcastToRoom("[System] 게임이 시작되었습니다!");
        nextRound(gameRoom);
    }

    public void checkAnswer(GameRoom room, Player sender, String data) {
        String message = "CHAT:";
        if (checkAnswerService.correctAnswer(room, sender, data)) {
            nextRound(room);
        }
        message += "[" + sender.getName() + "] "+data;
        room.broadcastToRoom(message);
    }

    public int getPlayerScore(GameRoom gameRoom, Player player) {
        Integer score = gameRoom.getScoreBoard().get(player);
        if (score == null) {
            return 0;
        }
        return score;

    }

    // 다음 라운드 준비
    public void nextRound(GameRoom gameRoom) {
        gameRoom.stopTimer();
        // 다음 그림 그리는 사람 선택
        Player newDrawer = drawerService.selectNextDrawer(gameRoom.getPlayers(), gameRoom.getDrawer());

        if (newDrawer == null) {
            gameRoom.broadcastToRoom("[System] 플레이어가 없어 게임을 진행할 수 없습니다.");
            return;
        }

        gameRoom.setDrawer(newDrawer);
        gameRoom.broadcastToRoom("ERASE:");

        // 사용자 업데이트
        drawerService.updatePlayerStates(gameRoom, newDrawer);

        // 제시어 변경
        changeWord(gameRoom);

        for (Player p : gameRoom.getPlayers()) {
            if (!p.equals(gameRoom.getDrawer()))
                p.sendMessage("[System] 새로운 라운드가 시작되었습니다!");
        }
        gameRoom.broadcastToRoom("다음 그림 그리는 사람은 "+newDrawer.getName()+"님 입니다.");

        gameRoom.startTimer();
    }

    public Player selectNextDrawer(GameRoom room) {
        return drawerService.selectNextDrawer(room.getPlayers(), room.getDrawer());
    }
    // 제시어 바꾸기
    private void changeWord(GameRoom gameRoom) {
        String nextWord = gameWordService.changeWord(gameRoom, quizWordRepository);
        gameRoom.setCurrentWord(nextWord);
    }
}
  • 게임 로직의 전체 관리자 역할로, 세부적인 로직을 처리하는 서비스들을 필드로 가지고 있고, 해당 서비스들은 생성자에서 DI(의존성 주입)의 방식으로 주입받음
  • 게임 진행에 필요한 메소드를 정의해두고, 해당 메소드들 안에서 세부 서비스(Service)들의 처리 메소드를 호출해서 세부 사항들을 처리하는 방식
  • 기능:
    • 게임 시작: 해당 서비스 클래스에서 처리
    • 채팅에서 정답 확인: CheckAnswerService에 위임해서 처리
    • 플레이어들의 점수 가져오기: 아직 구현하지 못함
    • 다음 라운드 진행:
      • 타이머는 해당 서비스 클래스에서 정지, 재시작 관리
      • DrawerService에 다음 그림을 그릴 사람을 고르는 로직과 플레이어의 상태(State) 업데이트 동작을 위임해서, 해당 클래스의 메소드를 호출해서 처리
      • GameWordService에 다음 제시어를 고르는 로직을 위임해서, 해당 클래스의 메소드를 호출해서 처리 ⇒ 해당 부분은 클래스 내에서도 메소드로 만들어서 한 번 더 분리함
      • 라운드 시작을 위한 메시지 브로드캐스트: 해당 클래스에서 처리

DrawerService

: 그림을 그릴 사람 (=출제자)를 선정하고, 관련해서 플레이어들의 상태를 업데이트하기 위한 서비스

public class DrawerService {

    public Player selectNextDrawer(List<Player> players, Player drawer) {
        if (players.isEmpty()) {
            return null;
        }

        if (drawer == null) {
            return players.get(0);
        }

        int currentIndex = players.indexOf(drawer);

        if (currentIndex == -1)
            return players.get(0);

        return players.get((currentIndex + 1) % players.size());
    }

    public void updatePlayerStates(GameRoom gameRoom, Player newDrawer) {
        for(Player p: gameRoom.getPlayers()){
            if(p.equals(newDrawer)) {
                p.setState(new DrawingState());

                p.sendMessage("DRAWSTATE:true");
            } else {
                p.setState(new AnsweringState());

                p.sendMessage("DRAWSTATE:false");

                p.sendMessage("KEYWORD:???");

            }
        }
    }
}
  • selectNextDrawer(): 해당 게임룸에 있는 플레이어들의 리스트와 현재 출제자를 인자로 받아 다음 출제라를 선정해서 반환. 반환값은 GameService에서 받아 업데이트 등 처리
  • updatePlayerStates: 해당 게임룸에 존재하는 플레이어들의 상태를 바뀐 역할에 따라 업데이트하도록 클라이언트들에게 메시지 송신

CheckAnswerService

: 플레이어들이 보낸 채팅이 제시어가 맞는지 확인하고 점수를 올리는 등의 작업을 위한 서비스

public class CheckAnswerService {
    private static final int SCORE_PER_ANSWER = 10;

    public boolean correctAnswer(GameRoom gameRoom, Player player, String msg) {
        String correctWord = gameRoom.getCurrentWord();

        if (msg.equalsIgnoreCase(correctWord)) {
            gameRoom.broadcastToRoom("[System] " + player.getName() + "님이 정답을 맞추셨습니다! (+" + SCORE_PER_ANSWER + "점)");
            addScore(gameRoom, player);

            return true;
        }
        else {
            return false;
        }
    }

    public int getPlayerScore(GameRoom gameRoom, Player player) {
        Integer score = gameRoom.getScoreBoard().get(player);
        if (score == null) {
            return 0;
        }
        return score;

    }

    public void addScore(GameRoom gameRoom, Player player) {
        Map<Player, Integer> board = gameRoom.getScoreBoard();
        Integer currentScore = board.get(player);

        if (currentScore == null)
            currentScore = 0;

        board.put(player, board.get(player) + SCORE_PER_ANSWER);
    }
}
  • checkAnswer(): 제시어가 맞는지 확인하고 이에 따른 boolean 반환.
    • 구체적으로 정답/정답이 아닐 경우 실행되어야할 로직은 GameService에서 이에 해당하는 서비스들을 호출해 실행
  • 점수를 DB에 저장하고 가져오는 메소드들은 아직 완전히 완성되지는 않음

GameWordService

: 다음 제시어를 받아오기 위한 서비스

public class GameWordService {
    public String getNewQuizWord(QuizWordRepository quizWordRepository) {
        String quizWord = quizWordRepository.getRandomWord();
        return quizWord;
    }

    public String changeWord(GameRoom gameRoom, QuizWordRepository quizWordRepository) {
        String nextWord = getNewQuizWord(quizWordRepository);
        gameRoom.setCurrentWord(nextWord);

        System.out.println("[DEBUG] 현재 그림 그리는 사람: " + gameRoom.getDrawer().getName());
        System.out.println("[DEBUG] 선정된 단어: " + nextWord);

        if (gameRoom.getDrawer() != null) {
            gameRoom.getDrawer().sendMessage("KEYWORD:" + nextWord);
            System.out.println("[DEBUG] 서버 -> 클라이언트 전송 완료: KEYWORD:" + nextWord);
        } else {
            System.out.println("[DEBUG] 그림 그리는 사람이 없어서 전송 못함");
        }

        return nextWord;
    }
  • QuizWordRepository에서 제시어를 받아와서 다음 제시어를 반환해주는 메소드
  • 아직 QuizWordRepository와 DB 연동이 이루어지지 않아 우선 하드코딩된 리스트에서 제시어를 가져와서 반환

✔️ 도메인 (domain) | 서버 측

Player

public class Player {
    private String name;
   private  String id;
    private String password;
    private MessageSender messageSender;
    private PlayerState state;
    private int score=0;

    public Player() {
        // default: 답맞추기
        this.state=new AnsweringState();
    }
    public Player(String name, String id, String password) {
        this.name = name;
        this.id = id;
        this.password = password;
        this.state=new AnsweringState();
    }

    public void sendMessage(String msg) {
        if (messageSender != null) {
            messageSender.send(msg);
        } else {
            System.err.println("[WARNING] " + name + "의 messageSender가 null입니다. 메시지 전송 실패: " + msg);
        }
    }

    //Getter & Setter
    ...
}
  • 사용자(클라이언트)의 정보를 필드로 저장
  • MessageSender 인터페이스를 가지고 있음으로 의존성을 줄이는 방식으로 메시지 전송
    public interface MessageSender {
        void send (String msg);
    }
    • ConnectionController에서 위의 인터페이스를 구현함으로써 Player에서 해당 인터페이스를 사용해 클라이언트에게 메시지 송신

State 패턴 사용

public interface PlayerState{
    boolean canDraw();
    boolean canAnswer();
}
public class AnsweringState implements PlayerState {
    @Override
    public boolean canDraw() { return false; } // 답 맞추는 사람은 그림 못그림

    @Override
    public boolean canAnswer() { return true; }
}
public class DrawingState implements PlayerState {
    @Override
    public boolean canDraw() { return true; }

    @Override
    public boolean canAnswer() { return false; } // 화가는 정답 못맞춤
}
  • 플레이어들의 상태 두 가지를 State 패턴을 사용해, 그림을 그릴 수 있는 출제자와, 정답을 맞추고 그림은 그릴 수 없는 플레이어들을 구분
    • DrawingState: 문제 출제자는 그림을 그릴 수 있고, 채팅은 불가함
    • AnsweringState: 정답을 맞추는 사람은 그림은 그릴 수 없고, 채팅만 가능함

✔️ DAO/Repository | 서버 측

  • 우선은 DBConnector, PlayerRepository, QuizWordRepository 로 구성
  • 가능한 범위까지 구현 예정
public class QuizWordRepository {
    private final List<String> wordList = Arrays.asList(
            "사과", "바나나", "자동차", "비행기", "컴퓨터",
            "축구", "피아노", "고양이", "강아지", "아이스크림",
            "우산", "시계", "안경", "모자", "자전거"
    );

    public String getRandomWord(){
        int randomIndex = new Random().nextInt(wordList.size());
        return wordList.get(randomIndex);
    }
}
  • 아직은 하드코딩된 리스트 형태

2. 진행 중인 기능

  • Timer 타이머 기능 작업 중
    • 현재 UI 구현 완료
    • 서버 측 Service에서 타이머 관련 코드 작성 후, 클라이언트와 연결된 소켓으로 전송.
  • DB 연결 및 생성 작업 중
    • 사용자 관련 DB
    • 제시어 관련 DB

3. 1차 보고서 이후 변경 사항

: 대부분은 구조의 변경. SOLID 원칙 혹은 디자인 패턴 적용 시도이고 일부 기능이 추가됨.

✔️ 추가된 기능

1.기본적인 게임로직 구현: 제시어가 랜덤하게 변경되고, 다음 출제자(=그림 그리는 사람) 선정 가능

2.색 변경 기능: 캔버스에 그림을 그릴 때 색을 변경할 수 있도록 기능 추가

✔️ 구조적인 측면에서의 변경 사항

  1. 클라이언트 측 컨트롤러 2개로 분리
  • 게임 관련 로직을 처리하는 GameController와, UI 관련 로직을 처리하는 ViewController로 분리
  1. 클라이언트-서버가 주고받는 메시지(커맨드) 처리에 커맨드 패턴 적용
    • 변경이유: 초반에는 클라이언트-서버 사이의 메시지 송수신과 이에 따른 로직을 처리하는 것을 우선시 해서 바로바로 코드를 작성했는데, 그랬더니 if-else문이 커맨드의 종류에 따라 끝도 없이 길어지게 되어 이를 분리할 수 있는 패턴과 방식에 대해서 조사함
    • 변경 방식:
      • 커맨드 인터페이스 생성: 공통 메소드인 execute() 선언

      • 커맨드 인터페이스를 구체적으로 구현하는 클래스들을 클라이언트-서버 사이 메시지의 종류마다 새롭게 만들어서 사용

      • 수신한 메시지의 파싱은 팩토리 패턴 일부와 맵을 사용해서 헤더(=메시지의 첫 번째 split되는 문자열)에 따라 매핑되는 구체적인 커맨드 클래스를 생성하고 이를 실행(execute)하도록 구현

        public class CommandFactory {
            private static CommandFactory instance = null;
            Map<String, Command> commandMap = new HashMap<>();
        
            private CommandFactory() {
                commandMap.put("DRAW", new DrawCommand());
                commandMap.put("NAME", new NameCommand());
                commandMap.put("CHAT", new ChatCommand());
                commandMap.put("ERASE", new EraseCommand());
                commandMap.put("START", new StartCommand());
                commandMap.put("COLOR", new ColorCommand());
            }
        
            public static CommandFactory getInstance() {
                if (instance == null) {
                    instance = new CommandFactory();
                }
                return instance;
            }
        
            public void createCommand(GameController gameController, String msg, Player player) {
                String[] tokens = msg.split(":");
        
                Command commandProcessor = commandMap.get(tokens[0]);
                commandProcessor.create(msg, player);
                commandProcessor.execute(gameController, player);
            }
        }
        
  2. 서버 측 서비스(Service) 분리
이전 구조
현재 구조

- 변경 이유: GameService 하나의 클래스에서 담당하는 역할들과 메소드들이 점점 늘어나서 비슷한 기능들끼리 최대한 분리 → SRP(단일 책임 원칙)을 나름 고려…?

✅ 기술적 문제 및 해결 방법

발생한 오류들

  • 클라이언트 측 GameController의 의존성? 문제 : ViewController를 필드로 가질 필요가 없는 것 같아서, 클라이언트 클래스에서 인자로 전달하는 방식 고려 중
    public class GameController {
    
        private Client client;
        private final CommandFactory commandFactory = CommandFactory.getInstance();
        private ViewController viewController;
    		...
        
        //Setter
        public void setViewController(ViewController viewController) {this.viewController = viewController;}
        public void setClient (Client client) {this.client = client;}
    }
  • GameRoom과 GameController의 분리 필요성? 문제
    public class GameController {
        private final List<Player> players = new ArrayList<>();
        private String currentWord;
        private Player drawer;
        Timer gameTimer;
        private int remainingTime = 30;
        Map<Player, Integer> scoreBoard = new HashMap<>();
        private final GameService gameService;
        private final CommandFactory commandFactory = CommandFactory.getInstance();
    
        public GameController(GameService gameService) {
            this.gameService = gameService;
        }
        ...
    }
    • 게임 진행 등을 위한 관리자인 GameController가 실제 플레이어 리스트, 현재 제시어, 현재 출제자를 필드로 가지고 있는데 이걸 domain인 GameRoom을 만들어 분리하고 GameController는 GameRoom을 가지고, GameRoom이 구체적인 플레이어 목록, 현재 제시어, 현재 출제자를 필드로 가지게 할 지에 관한 고민....
profile
이불 밖은 위험해.

0개의 댓글