옵저버 패턴 적용하기 (빙고 게임)

junto·2024년 11월 2일
0

design-patterns

목록 보기
1/3
post-thumbnail

옵저버 패턴

옵저버 패턴이란

  • 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. (위키 백과)

  • 잘 와닿지 않는다. 간단한 빙고 게임을 생각해보자. 심판(Referee)이 임의의 숫자를 하나씩 부르고, 플레이어(Player)들은 심판이 부른 숫자를 보드판에 마킹하여 빙고를 찾아내는 게임이다. 단순하게 구현하면 아래와 같은 코드를 생각해볼 수 있다.


while (!hasWinner()) {
	
    int CalledNumber = Referee.getCalledNumber();
    player.markBoard(calledNumber);
    player.checkBingo();
    for (var player : players) {
    	if (player.isBingo()) {
        	winners.add(player);
            hasWinner = true;
        }
    }
    
    if (hasWinner) {
    	System.out.printf("우승자는 ");
    	for (var winner : winners) {
    		System.out.printf("winner.getName()), ");
    	}
    	System.out.println(" 입니다.");
   }
}

필요한 이유

  • 이런식으로 작성하면 제대로 동작하지 않을까? 간단한 게임에선 크게 문제가 되지 않는다. 하지만 기능이 추가될수록 해당 while문 안에 코드를 점점 추가되어 가독성이 떨어지는 문제가 생긴다.
  • 예를 들어, 특정 플레이어는 marking을 한 번에 두 번 할 수 있다거나 특정 숫자를 맞출 때 보너스를 주거나 하는 등의 기능을 추가할 때 코드 로직이 복잡해진다.
  • 옵저버 패턴을 사용하면 해당 객체의 기능을 캡슐화할 수 있다. 특히 이벤트(심판이 부르는 숫자)가 발생하고, 해당 이벤트를 다른 객체들(플레이어)이 바로 처리해야 할 때 유용하다. 새로운 기능이 추가될 때 단순히 관찰자만 추가하고 행동만 다르게 정의하면 되기 때문에 확장성에도 좋다.

Observer 패턴 구조

전체 코드: https://github.com/ji-jjang/Learning/commit/cee8f544da3e5d368cb260d012d4ac5dad96bbfd

  • Observer들을 관리하는 주체 Class를 만든다. 관찰자를 등록, 해제, 그리고 이벤트를 발행하는 역할을 한다. 이벤트가 발생하면 Observer는 단순히 자신의 역할에 맞게 행동하면 된다.
public class BingoGame {
	private List<Observer> observers = new ArrayList<>();
    
    public void addObserver(Observer observer) {
    	observers.add(observer);
    }
    
    public void deleteObserver(Observer observer) {

    	observers.remove(observer);
  	}
    public void notifyObservers() {
    	for (var o : observers) {
        	o.update(this);
        }
    }
}
  • 만약 관찰자들을 관리하는 주체가 위의 기본 기능을 가지면서, 상황에 따라 다른 기능을 포함해야 한다면 추상 클래스로 선언하고 해당 추상 클래스를 상속한 클래스에서 재정의하게 설계하면 좋다.

  • 관찰자는 자신의 역할에 따라 행동을 다르게 할 수 있도록 인터페이스만 제공한다.

public interface Observer {

  public abstract void update(NumberGenerator generator);
}
  • 빙고 게임에서 크게 번호를 부르고, 자신이 부른 번호를 관리하는 심판(refree) 객체, 보드판에 불린 숫자를 마킹하고 빙고를 외치는 플레이어(player) 객체, 승리자를 말해주는 Checker가 있다고 해보자. 그럼, Observer 인터페이스를 구현한 후 update 메소드에서 각자 역할을 수행하면 된다.

1. 플레이어

public class Player implements Observer {

  private String name;
  private int[][] board;
  private boolean isBingo;
  
  @Override
  public void update(BingoGame game) {

     markBoard(game.getBingoNumber());
     checkBingo();
  }
}

2. 심판

public class Referee implements Observer {

  private List<Integer> calledNumbers = new ArrayList<>();
  Random random = new Random();

  @Override
  public void update(BingoGame game) {

    int number = 0;
    do {
      number = random.nextInt(game.getMaxNumber() + 1);
    } while (calledNumbers.contains(number));

    System.out.println("call: " + number);
    game.setBingNumber(number);
    calledNumbers.add(number);
  }
}

3. WinnerChecker

public class WinnerChecker implements Observer {

  private List<String> winners = new ArrayList<>();

  @Override
  public void update(BingoGame game) {

    for (var observer : game.getObservers()) {
      if (observer instanceof Player) {
        if (((Player) observer).isBingo()) {
          winners.add(((Player) observer).getName());
        }
      }
    }
    if (winners.size() > 0) {
      game.setHasWinner(true);
      System.out.printf("우승자는 {");
      for (var e : winners) {
        System.out.printf(e + " ");
      }
      System.out.println("} 입니다.");
    }
  }
}

4. Main

public static void main(String[] args) throws IOException {

    BingoGame bingoGame = new BingoGame();

    for (int i = 0; i < bingoGame.getPlayerCount(); ++i) {
      Player player = new Player("player " + i, bingoGame);
      bingoGame.addObserver(player);
    }

    Referee referee = new Referee();
    bingoGame.addObserver(referee);

    WinnerChecker winnerChecker = new WinnerChecker();
    bingoGame.addObserver(winnerChecker);

    bingoGame.startGame();
}

public void startGame() {

    while (!hasWinner) {
      notifyObservers();
    }
}
  • 메인 함수에서는 단순히 Player, Refree, WinnerChecker를 관찰자로 등록하고 게임을 시작한다. 이제는 클라이언트 코드가 변경될 때 단순히 클라이언트 코드만 수정하면 된다. BingoGame 객체는 관찰자들이 어떤 메소드를 가지고 있는지, 어떤 상태값을 주는지 등 알 필요가 없어진다. 즉, 유연하고 느슨한 설계가 된다.

여담 (React)

코드 링크: https://github.com/ji-jjang/ebrainsoft/tree/main/BingoGame

  • React로 빙고 게임을 작성하면서 위 내용을 그대로 구현해 보았다. 옵저버 패턴을 적용해 보려고 했지만, react는 이미 상태 변화와 리랜더링으로 옵저버 패턴을 이미 유사하게 적용하고 있기에 적용할 부분을 찾지 못했다. useEffect()로 Referee 컴포넌트가 호출한 숫자들이 변경되면(notify) Player 컴포넌트는 화면을 리랜더링하는 식으로 동작하기 때문이다.
      {gameStart && (
        <>
          <Referee
            callNumber={callNumber}
            isGameOver={isGameOver}
            calledNumbers={calledNumbers}
          />
          <div className="boards-container">
            {!isNaN(players) &&
              Array(players)
                .fill()
                .map((undefined, playerIndex) => (
                  <Player
                    key={playerIndex}
                    playerIndex={playerIndex}
                    rows={rows}
                    cols={cols}
                    maxNumber={maxNumber}
                    calledNumbers={calledNumbers}
                    handleWin={() => handleWinners(playerIndex)}
                    isGameOver={isGameOver}
                  />
                ))}
          </div>
          <WinnerChecker
            winningPlayers={winningPlayers}
            players={players}
            setIsGameOver={setIsGameOver}
          />
        </>
      )}
profile
꾸준하게

0개의 댓글