본문 바로가기
Java & Kotlin

객체지향 좀 더 이해하기 - 블랙잭 게임 구현 (4)

by 향로 (기억보단 기록을) 2016. 11. 27.
반응형

2-4. Player 구현

코드를 보시면 불편해 보이는 코드가 대거 보이실것 같습니다.
2개의 메소드 모두 dealer와 gamer만 다르지 같은 일을 하는 코드가 대부분입니다.
반대로 생각해보면 dealer와 gamer만 하나로 볼 수 있으면 코드 중복을 제거할 수 있지 않을까요??
Gamer와 Dealer는 여러 조건들에 의해 서로 다른 구현 코드를 가지고 있습니다.
하지만 카드를 받아야하고, 가진 카드를 보여줘야 한다는 공통점을 가지고 있습니다.
우린 이 공통점을 묶어 Player라는 객체를 생성하여 Gamer와 Dealer를 Player에 속하도록 수정하겠습니다.
(참고로, 이렇게 서로 다른 객체들의 차이점을 배제하고, 공통점만을 취해 단순화 하는 것을 추상화 라고 합니다.
객체지향을 좀 더 단순하게 바라보기 위해서는 이렇게 추상화를 어떻게 하느냐에 따라 달라집니다.)


Player.java

public interface Player {
    void receiveCard(Card card);

    void showCards();

    List<Card> openCards();
}

Player 인터페이스는 단순합니다. Gamer와 Dealer의 공통점인 receiveCard, showCards, openCards를 추상메소드로 갖고 있습니다.
(참고로 인터페이스는 상수와 추상메소드만 가질 수 있습니다.
반대로 추상 클래스는 상수,변수,일반메소드,추상메소드 모두를 가질 수 있습니다.)
그리고 Player 인터페이스를 구현(Implements) 하도록 Gamer클래스와 Dealer클래스 코드를 수정하겠습니다. 한가지 주의하셔야 할 점은, 추상 클래스와 상속은 최대한 피하는 것이 좋습니다. 참고
프로젝트 초기에는 못느끼지만, 프로젝트가 점점 커지고 운영기간이 3년, 4년 지나면서 상속과 추상 클래스로 범벅이 된 코드는 부모 클래스를 도저히 수정할 수 없는 지경에 이르게 됩니다.
현재는 Dealer와 Gamer의 showCards 메소드와 openCards 메소드가 동일하지만, 시간이 지나서도 2개의 메소드 코드는 동일할 수 있을까요?
receiveCard 메소드처럼 똑같이 카드를 받는 역할이지만 구현은 다른 경우가 과연 없을까요?
서로 다른 객체는 최대한 느슨한 관계를 가지는 것이 좋습니다.

public class Gamer implements Player {
    ...

    @Override
    public void receiveCard(Card card) { ... }

    @Override
    public void showCards(){ ... }

    @Override
    public List<Card> openCards(){ ... }
}

public class Dealer implements Player {
    ...

    @Override
    public void receiveCard(Card card) { ... }

    @Override
    public void showCards() { ... }

    @Override
    public List<Card> openCards() { ... }
}

이렇게 Gamer와 Dealer를 Player의 구현체로 보게 되면 이전의 중복된 코드를 하나의 코드로 해결할 수 있게 됩니다.
먼저 가장 쉬운 코드인 initPhase 메소드를 수정해보겠습니다.


Game.java

    public void play(){
        ...
        List<Player> players = Arrays.asList(new Gamer(), new Dealer());
        initPhase(cardDeck, players);
        ...
    }

    private void initPhase(CardDeck cardDeck, List<Player> players){
        System.out.println("처음 2장의 카드를 각자 뽑겠습니다.");
        for(int i = 0; i< INIT_RECEIVE_CARD_COUNT; i++) {
            for(Player player : players) {
                Card card = cardDeck.draw();
                player.receiveCard(card);
            }
        }
    }

이전보다 훨씬 줄어든 코드양을 확인할 수 있습니다.
(기회가 되면 현재 코드를 전부 람다와 스트림으로 변경해보는 것도 좋을것 같습니다.
현재는 Java8을 접하지 않는 분들이 더 많으실꺼라는 생각에 모던 Java 문법은 배제하였습니다.
사실 저희팀 개발 스펙이 아직까진 Java7인것도 이유중 하나 입니다^^;)

이제 playingPhase 메소드를 수정해보겠습니다.

    private void playingPhase(Scanner sc, CardDeck cardDeck, List<Player> players) {
        while(true){
            boolean isAllPlayerTurnOff = receiveCardAllPlayers(sc, cardDeck, players);

            if(isAllPlayerTurnOff){
                break;
            }
        }
    }

    private boolean receiveCardAllPlayers(Scanner sc, CardDeck cardDeck, List<Player> players) {
        boolean isAllPlayerTurnOff = true;

        for(Player player : players) {
            if(isReceiveCard(sc)) {
                Card card = cardDeck.draw();
                player.receiveCard(card);
                isAllPlayerTurnOff = false;            
            }else{
                isAllPlayerTurnOff = true;
            }
        }

        return isAllPlayerTurnOff;
    }

    private boolean isReceiveCard(Scanner sc) {
        System.out.println("카드를 뽑겠습니까? 종료를 원하시면 0을 입력하세요.");
        return !STOP_RECEIVE_CARD.equals(sc.nextLine());
    }

기존의 코드에서 playingPhase의 역할을 추가된 2개의 메소드에 나눠주었고, 그 역할은 아래와 같습니다.

  • receiveCardAllPlayers : 모든 Player가 Card를 뽑도록 하는 역할
  • playingPhase : receiveCardAllPlayers 결과에 따라 receiveCardAllPlayers를 반복시키는 역할
  • isReceiveCard : Player 개개인에게 카드를 뽑을건지 의사를 묻는 역할


여기서 다른 메소드에 비해 receiveCardAllPlayers가 이상해보입니다.
receiveCardAllPlayers 메소드는 모든 Player가 카드를 받도록 하는 메소드인데 그 목적보다 많은 일을 하고 있습니다.
모든 Player가 카드를 받는 역할과 모든 Player가 카드를 받았다는 신호를 보내는 것 이 2가지를 하고 있습니다.
하나의 메소드는 하나의 역할만 하는 원칙에 따라 이를 분리하도록 하겠습니다.

Game.java

    public void play(){
        System.out.println("========= Blackjack =========");
        Scanner sc = new Scanner(System.in);
        Rule rule = new Rule();
        CardDeck cardDeck = new CardDeck();

        List<Player> players = Arrays.asList(new Gamer("사용자1"), new Dealer());
        List<Player> initAfterPlayers = initPhase(cardDeck, players);
        List<Player> playingAfterPlayers = playingPhase(sc, cardDeck, initAfterPlayers);

        Player winner = rule.getWinner(playingAfterPlayers);
        System.out.println("승자는 " + winner.getName());
    }

    private List<Player> playingPhase(Scanner sc, CardDeck cardDeck, List<Player> players) {
        List<Player> cardReceivedPlayers;
        while(true){
            cardReceivedPlayers = receiveCardAllPlayers(sc, cardDeck, players);

            if(isAllPlayerTurnOff(cardReceivedPlayers)){
                break;
            }
        }
        return cardReceivedPlayers;
    }

    private List<Player> receiveCardAllPlayers(Scanner sc, CardDeck cardDeck, List<Player> players) {
        for(Player player : players) {
            if(isReceiveCard(sc)) {
                Card card = cardDeck.draw();
                player.receiveCard(card);
                player.turnOn();
            }else{
                player.turnOff();
            }
        }

        return players;
    }

    private boolean isAllPlayerTurnOff(List<Player> players){
        for(Player player : players) {
            if(player.isTurn()) {
                return false;
            }
        }

        return true;
    }

    private boolean isReceiveCard(Scanner sc) {
        System.out.println("카드를 뽑겠습니까? 종료를 원하시면 0을 입력하세요.");
        return !STOP_RECEIVE_CARD.equals(sc.nextLine());
    }

    private List<Player> initPhase(CardDeck cardDeck, List<Player> players){
        System.out.println("처음 2장의 카드를 각자 뽑겠습니다.");
        for(int i = 0; i < INIT_RECEIVE_CARD_COUNT; i++) {
            for(Player player : players) {
                Card card = cardDeck.draw();
                player.receiveCard(card);
            }
        }

        return players;
    }

receiveCardAllPlayers 메소드에서 모든 게임 참가자가 카드뽑기종료 상태인지를 확인하는 역할을 새로운 메소드인 isAllPlayerTurnOff에 맡겼습니다.
여기서 주의하셔야 할것은 receiveCardAllPlayers의 리턴타입이 void가 아닌 List 라는 것입니다.
players와 같은 컬렉션 혹은 인스턴스는 Java의 특성으로 인해 Call by reference 입니다.
즉, 리턴을 하지 않더라도 players는 변경 상태를 유지하는 것입니다.
그럼에도 굳이 변경된 players를 리턴하는 이유는 receiveCardAllPlayers의 목적을 명확히 하기 위함입니다.
"receiveCardAllPlayers는 CardDeck과 List를 인자로 받아 특별한 과정을 통해 변경된 List를 준다."
이것이 receiveCardAllPlayers의 목적입니다.
만약 void로 할 경우 List가 변경은 될지언정, 최종적으로 무얼 위함인지 코드상에서 확인하기 어렵고 목적이 모호해지게 됩니다.
좋은 메소드란 결국 어떤 인자가 필요하고, 그 인자를 통해 어떤 결과를 뱉어내는지 명확한 것이라고 생각합니다.
자 그럼 위 Game의 변경된 코드에 맞춰 Player, Gamer, Dealer 코드를 수정하겠습니다.


Player.java

public interface Player {
    void receiveCard(Card card);

    void showCards();

    List<Card> openCards();

    void turnOff();

    void turnOn();

    boolean isTurn();
}

Gamer.java + Dealer.java

public class Gamer implements Player {
    private List<Card> cards;
    private boolean turn;

    .....

    @Override
    public void turnOff() {
        this.setTurn(false);
    }

    @Override
    public void turnOn() {
        this.setTurn(true);
    }

    @Override
    public boolean isTurn() {
        return this.turn;
    }

    private void setTurn(boolean turn) {
        this.turn = turn;
    }
}


public class Dealer implements Player {
    private List<Card> cards;
    private boolean turn;

    .....

    @Override
    public void turnOff() {
        this.setTurn(false);
    }

    @Override
    public void turnOn() {
        this.setTurn(true);
    }

    @Override
    public boolean isTurn() {
        return this.turn;
    }

    private void setTurn(boolean turn) {
        this.turn = turn;
    }
}

Dealer와 Gamer의 대결까지도 구현이 되었습니다.
자 그럼 마지막으로 게임의 결과를 나타내주는 Rule 객체를 구현해보겠습니다.


반응형