티스토리 뷰

11월 8일 마피아 게임으로 프로젝트 주제를 정하고 나서,

11월 10일 오후 3시부터 오후 10시??쯤까지 figma로 Model, View, Controller, Server, Client 에 관한 클래스 다이어그램을 함께 짰다. 회의를 하다보니 각자 생각한 구조가 달라서 꽤나 오래 걸렸지만 최종적으로 그래도 MVC 패턴과 SOLID원칙 준수, 전략 패턴, 팩토리메소드 패턴 등 다양한 패턴을 사용할 수 있도록 구조를 짜보았다.

 

이후 사다리타기를 통해 역할 분담을 했는데, 나는 Model과 Controller쪽 코드를 짜기로 결정됐다. Model쪽 Player, ISkill(인터페이스), Kill, Heal, Inspect 클래스들과 Controller쪽 IState 인터페이스와 RoleFactory, 밤, 투표, 토론, 사회자쪽 코드를 짰다.


가장 먼저 내가 했어야 하는 일은 다음과 같다.

1. 일단 Player 생성하고, 직업을 랜덤으로 배정해주기 - Player, 사회자, RoleFactory 클래스

2. 게임이 시작되면 가장 먼저 밤 State가 되기 때문에 밤 클래스 만들기, 밤에는 능력을 사용하므로 IState, Heal, Kill, Inspect 클래스 만들고 연결하기

3. 토론 클래스, 투표 클래스 만들기

 

우선순위에 따라 먼저 Player클래스를 필요한 변수와 생성자를 만들어두었다. 

public class Player {
	private String nickname;
	private int id;
	private String role;
	private boolean is_alive = true;
	private ISkill skill = null;
	private String skillName = null;
    
    public Player(String nickname, int id) {
		// TODO Auto-generated constructor stub
		this.setNickname(nickname);
		this.setId(id);
	}
}

 

 

이제 RoleFactory에서 중복 없게 id 부여, 랜덤 직업 배정 이 두 가지를 해줘야 했다. 

- 일단, 중복 없게 id 부여는 플레이어가 입장하는 순서대로 id를 주기로 했다.

public Player createNewPlayer(String nickname) {
		Player newPlayer = roleFactory.createPlayer(nickname, players.size()+1);
		addPlayer(newPlayer);
		return newPlayer;
}

[문제 1] - 직업 랜덤 부여 문제

- [문제 1]이 발생했다. 직업을 어떻게 랜덤 부여할 수 있을까??????

처음에는 직업 배정도 들어온 순서대로 주면 안되나... 싶었지만 말이 안되므로 구글링을 하다가 

java 컬렉션(Collection) shuffle() 메소드를 이용한 간단하나 카드게임 만들기

 

java 컬렉션(Collection) shuffle() 메소드를 이용한 간단하나 카드게임 만들기

▶ shuffle() 메소드 컬렉션의 shuffle(메소드)는 리스트와 같은 컬렉션에서 배열안에 있는 데이터를 랜덤으로 섞어주는 기능을 한다. ▶ shuffle() 예제(1) import java.util.ArrayList; import java.util.Collections; im

javabeater.tistory.com

이런 걸 봤다. 그래서 Collection.shuffle()을 사용하기로 했다. roles 어레이리스트를 만들고 인원 수에 따라 만들어진 직업들을 shuffle하면 된다. 이후 이걸 플레이어들에게 역할로 준다. 

public void randomRole(List <Player> players) {
		/*인원 수에 따라 의사, 경찰 1로 고정, 마피아는 4-1, 5-2, 6-2
		 * 4명:마피아1,시민1,의사1,경찰1, 
		 * 5명:마피아2,시민1,의사1,경찰1, 
		 * 6명:마피아2,시민2,의사1,경찰1
		 */
		
		List <String> roles = new ArrayList<>();
		int n = players.size();

		//n나누기3 반올림하면 마피아수
		int numMafia = (int)Math.round(n/3.0);

		for(int i=0; i<numMafia; i++) {
			roles.add("mafia");			
		}

		roles.add("doctor");
		roles.add("police");

		//나머지 시민
		int numCitizen = n - roles.size();
		for(int i=0; i<numCitizen; i++) {
			roles.add("citizen");
		}

		//직업 랜덤 섞기
		//역할 리스트 roles를 완전히 뒤섞음. 피셔-예이츠 알고리즘?
		Collections.shuffle(roles);

		//플레이어들에게 역할 배정 스킬 배정
		for(int i=0; i<n; i++) {
			Player p = players.get(i);
			String role = roles.get(i);//섞인 역할 배분
			p.setRole(role);

			switch(role) {
			case "mafia":
				p.setSkill(new Kill());
				p.setSkillName("kill");
				break;
			case "doctor":
				p.setSkill(new Heal());
				p.setSkillName("heal");
				break;
			case "police":
				p.setSkill(new Inspect());
				p.setSkillName("Inspect");
				break;
			default:
				p.setSkill(null);
				p.setSkillName(null);
			}			
			
		}

	}

 

이제는 사회자 클래스를 짜보러 가자. 

사회자 클래스가 전체 게임 로직을 관리하도록 설계했으므로 사회자 객체는 게임 안에서 단 하나만 존재해야 한다고 생각했다. 그렇기에 사회자 객체에 싱글톤 패턴을 적용하기로 했다. (나중에 어떤 폭풍을 불러오는진 생각 못한 채.....)

public class 사회자 {
	//사회자 객체 하나만 있어야 되니까 싱글톤 생성해봤음
	private static 사회자 매니저;
	private 사회자() {};
    
    	public static synchronized 사회자 getInstance() {
		if(매니저 == null) {
			매니저 = new 사회자();
		}
		return 매니저;
	}
}

 

그리고는 우리는 각각의 State(밤, 토론, 투표)에 대해 전략 패턴과 유사한 상태 패턴을 적용하기로 했으므로 start() 함수에다가 그걸 짜보았다. 사실 이 부분이 배운 패턴을 적용한 부분이어서 가장 맘에 든다. 

public void start() {
		initGame();
		
        while(true) {
        	this.setState(new 밤());
        	this.gameState.execute(매니저);
        	checkEnd();      	
        	
        	this.setState(new 토론());
        	this.gameState.execute(매니저);
        	//Server.execute(Player player);
        	
        	this.setState(new 투표());
        	this.gameState.execute(매니저);
        	checkEnd();
        	
        	dayCount = getDayCount() + 1;     	

        }
	}

코드가 너무 깔끔하고 아름답다. 이런 게 디자인 패턴을 배우는 이유인 것 같다.

 

이제 밤 클래스 및 능력 사용 단계로 넘어가보자.

- 밤 클래스에서는 각 플레이어가 수행한 kill / heal / inspect 결과를 한 번에 수집하고, 해당 정보를 기반으로 최종 밤 결과(사망자, 생존자, 조사 결과)를 계산해야 한다.

 


[문제 2] - targetId만 받아오니 밤 클래스에서 if-else 난무하는 문제

여기서 [문제 2] 가 발생했다.

설계단계에서 ISkill을 targetId만 받아서 처리하려고 했었으므로 다음과 같이 짜놨었다.

public int skill(Player self) {
    return self.getNightTargetId();
}

 

원래는 kill, heal, inspect에서 타겟아이디 반환해서 밤에서 결과 처리하려고 했으나 문제는... 받은 아이디가 kill한건지, heal한건지, inspect한건지 모른다...

 

그래서 어떻게 바꿨냐면, 밤 클래스에서 if-else문으로 확인해서 target을 mafia, doctor, police 의 targetId로 집어넣는... 무식한 방법을 택했다. 사실 일단 돌아가게 하자는 급한 마음에 이렇게 짰다. 반성하고 있다...

// 각 역할별로 이번 밤에 누굴 골랐는지 모은다.
		for (Player p : 매니저.players) {
			if (!p.is_alive)
				continue;

			int target = p.getNightTargetId();

			System.out.println("밤타겟:"+target); //디버그용

			String role = p.getRole();
			if ("mafia".equals(role)) {
				mafiaTargetId = target;
			} else if ("doctor".equals(role)) {
				doctorTargetId = target;
			} else if ("police".equals(role)) {
				policeTargetId = target;

 

근데 아무리 봐도 이건 OCP 원칙, 개방폐쇄 원칙을 위반하는 예시였다. if-else문으로 하나하나 알아내고 하드하게 코딩하는 건 좋지 않은 거라고 수업에서 배웠기 때문에 계속 찝찝했다.

결국 원래는 return type이 int였던 초기 ISkill 설계 구조에서 벗어나 void로 만든 후 행동 자체를 사회자에게 기록하도록 만들었다. Skill들이 각자 일을 하고 밤 클래스에서는 인터페이스만 부르면 플레이어들이 가진 스킬이 알아서 행동을 하도록 했다. 그럼 밤은 if-else문 필요없이 그냥 결과만 계산하면 된다.

 

public interface ISkill {
    void skill(Player self, 사회자 manager);
}
// 1) 각 플레이어의 스킬에게 할 일 시킴.
        for (Player p : 매니저.getPlayers()) {
            if (!p.getIs_alive()) continue;
            if (p.getSkill() == null) continue;

            p.getSkill().skill(p, 매니저);
        }

        // 2) 스킬들이 manager에 등록해둔 결과만 가지고 마무리 처리
        int mafiaTargetId = 매니저.getMafiaTargetId();
        int doctorTargetId = 매니저.getDoctorTargetId();

이후로는 코드를 합치는 와중 발생한 문제들이다.

[문제 3] - 역할 분리 미흡 문제

Player들이 들어오고, 시작하기 버튼까지 눌렀는데 직업 배정이 아무리 해도 되지 않았다. 제미나이한테도 물어보고, gpt한테도 물어보고... 이렇게도 해보고, 저렇게도 해보고 2주를 난리쳤으나 이유를 몰랐었는데 아래 링크대로 해결했다. 

[프로젝트] 마피아 게임 - 직업 배정 문제 해결 과정

끊임없는 질문들....

계속 물어봐도 싱글톤 문제라길래 그럼 사회자에서 부르는 Lobby, 즉 View와 클라이언트측 View가 다르냐고 물어봤다. 그랬더니 다르다고 말해주면서 서버는 클라이언트에게 직업 배정된 결과를 통보하고, 클라이언트는 그걸 받아서 자신의 View에 set하게끔 해야 한다고 답변해줬다. 그렇게 고쳐보니...

직업이 배정되고 이미지가 떴다!!!!!!!!! 엉엉..... 사실 세미나 듣는 중에 했었는데 옆자리 모르는 분을 부둥켜 안고 울고싶었다. 저 이미지가 뜨기를 얼마나 소망했었는지....ㅠㅠㅠㅠ 이렇게 고치면서 놀란 건 생각보다 팀원들이 주석해놓은 코드를 다시 풀고 재사용하는 게 많았다는 것이다. 각자 잘 짜놓으니 오류를 고칠 때에도 수월하다는 걸 느꼈다. 또한 명확한 역할 분리가 얼마나 중요한건지를 뼈저리게 느꼈다. 서버가 클라이언트의 뷰까지 침범하려고 하는 게 문제였다니....


[문제 4] - 멀티 쓰레드 문제

밤 상태에서 플레이어가 타겟을 아무리 입력해도 서버가 타겟을 받지 못했다. 그래서 그런지 오류가 나고 경찰에게만 조사 결과를 알려줘야 하는데 알려주지 못하는 상황이었다. 4명이서 2-3시간을 잡고 있어도 안되길래 또 지피티에게 물어봤다. 근데 지피티도 갈피를 못 잡고 또 싱글톤 때문에 에러가 나는거라고 하는 것이다. Player 객체가 2개씩 생겼다는 것이다. 근데 우리가 Join 시퀀스를 처리할 땐 전혀 문제 없이 잘됐었다. 그래서 JoinCommand 코드에서 사회자에 들어가는 Player 객체와 ServerThread가 갖고 있는 Player 객체가 같다는 걸 지피티에게 알려줬다. 

그랬더니 내 말을 반복하면서 똑똑한 척을 하는 지피티....

 

이번에는 쓰레드로 인한 문제일 수 있다면서 공유하고 있는 변수들, 특히 현재는 밤 단계니까 nightTargetId 변수에 volatile을 붙여보라고 해결책을 줬다. 붙여도 결과는 바뀌지 않았다. 

그렇게 좌절하고 있었는데 쓰레드 문제인 거니까 혹시 서버 쓰레드가 밤 클래스의 Thread.sleep()을 보고 잠들 수 있냐고 물었다. (당시에는 말도 안된다고 생각했다.) 

그런데 지피티가 이렇게 말했고 해결책으로 CommandManager에서 Start: 명령을 처리할 때의 사회자 start() 함수 호출하는 부분에서 새로운 쓰레드를 만들어서 호출해보라고 했다. 

// Start 명령어일 시 모든 클라이언트에게 게임이 시작되었다고 알려주기.
        	broadcastAll("Start:");

        	new Thread(() -> logicBrain.start()).start();

그랬더니 거짓말처럼 밤이 지나가고 경찰 조사 결과가 떴다!!!!!!!!!!!!!!!!!!!!

팀원들 다같이 소리 지르고 난리도 아니었다!!! 정말 올 한 해 가장 행복한 순간이었다... 물론 지피티 덕분이긴 했지만.... 그래도 너무나 계속 target이 0이라고 해서 정말 힘들었으므로...^^...  그렇지만 그때 당시에는 저렇게 수정한 것에 대한 구체적인 이유를, 논리를 몰랐어서 AI에게 진 기분도 들고, 찝찝하고.... 인간 4명으로는 ai를 이길 수 없는건가 허탈하기도 했다. 팀원 한 명은 울기까지 했다....

암튼 다음 날, 지피티와의 대화 내용을 다시 보면서 이해한 결과 

 

쓰레드란... 인간의 머리로 생각하기 어려운 주제인 것 같다. 

 

[문제 5] - 아이디 인덱스 문제

기존에는 플레이어의 아이디를 들어오는 순서대로 부여해서 1, 2, 3, 4가 되도록 +1을 했었다. 근데 나중에 가서 Player리스트에서 remove할 때 인덱스 때문에 헷갈려지기 시작했다. 그래서 그냥 0부터 시작하도록 +1 부분을 다 없앴다. 그렇게 아이디가 0부터 시작하게 되니 밤 클래스나 투표 클래스에서 각 플레이어들이 친 id를 target으로 받을 때 초기화를 -1부터로 수정했어야 했다. 

 

[문제 6] - Model, Controller 변수들 public인 문제

일단 만들어놓고 보자(매우 문제적인 태도이다....) 라는 생각으로 public으로 설정해놓은 변수들이 많았다. 이를 모두 private으로 바꾸었다. 사실 이는 교수님께서 강의 초반부터 강조하셨던 내용인데, 다른 오류 해결에 한눈이 팔려 가장 중요한 걸 놓치고 있었다. 이에 따라 모두 private으로 바꾸었다. 


암튼 이렇게 우당탕탕 프로젝트를 마무리했다. 확실한 건, 나 혼자서는 절대 못 해냈을 거라는 것. 배우는 게 너무나도 많았던 시간이었다. 사실 너무 많아서 중간중간 암울해지기도 했지만… 그만큼 팀원들에게서 얻은 것도 컸다.

깃에 익숙하지 않아 헤맬 때마다 깃 관련 설정부터 사용법까지 전부 맡아서 알려주고 도와준 현경이에게 정말 고마웠고,
처음부터 완성도 높은 서버 코드와 클라이언트 코드를 작성해온 현경이와 은서를 보면서도 많이 배웠다.

그리고 구조를 잡아야 하는 초반, 무엇을 어떻게 시작해야 하는지 막막했던 시점부터 거침없는 속도로 SOLID 원칙과 디자인 패턴들을 적용하며 전체 구조를 이끌어주고, 나중에 각자 코드를 합치는 중요한 순간에 중추적인 역할을 해준 윤하에게도 정말 많은 것을 배웠다.

또, 자신의 시간을 할애해서 너무 훌륭한 영상을 만들어준 은서에게도 고마웠다. 

 

싱글톤 관련 오류들을 디버깅해나가며 싱글톤 개념에 대해 확립했고, 서버와 클라이언트의 역할 분리 오류를 마주하며 서버는 직업 배정 결과만, 클라이언트가 뷰를 업데이트하는 식의 명확한 분리의 중요성을 알게 됐다. 특히 역할 분리는 강의에서도 계속 강조되었던 부분이라 절대 잊지 못할 것 같다. 또한 멀티 쓰레드 환경에서 서버쪽 쓰레드가 함수를 타고 타고 와서 Thread.sleep()을 보고 잠들어버리는 바람에 targetId를 받는 자신의 일을 못하게 되는 걸 보며 배웠던 쓰레드에 대해서도 깊은 생각을 해볼 수 있었다. 돌이켜 보니 자프실2의 모든 단원들을 직접 경험할 수 있는, 좋은 주제로 프로젝트를 한 것 같다.

 

오류들을 마주하고 해결해나가는 과정 속에서 배우는 것도 많아지고, 사실 힘듦을 다 잊게하는 기쁨들이 있었다. 역할 배정 이미지가 딱 떴을 때, 밤이 끝나고 경찰의 조사 결과가 나왔을 때 팀원들과 함께 머리 끝까지 기뻐지는 경험을 했다. 또한 마지막에 테스트 차원에서 다같이 게임을 해보면서 너무 재밌어서 놀라웠다. 

 

배움은 끝이 없고, 부족한 스스로를 매번 마주하는 시간이기도 했다. 특히나 문제2번(if-else 난무), 문제6번(private변수 설정)은 스스로 처음부터 배운 것을 잘 적용해서 코드를 짰다면 발생하지 않았을 문제이므로 반성을 많이 하게 된다. 

 

그리고 적용해보고 싶었지만 시간 문제로 해보지 못한 것들이 많다. DB도 연결해보면 더 재밌을 것 같고, Spring Boot로 웹으로 구현해도 매우 재밌을 것 같다. 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함