티스토리 뷰

미션 - 숫자 야구게임을 참고해주세요. (회고 말고 코드 설명을 한다라고 보시면 될 것 같습니다.)

 

 

클래스보다 객체의 속성과 행위가 우선이다

클래스는 객체를 추상화하는 도구일 뿐이다. 그럼 먼저 주요 객체들을 알아보자. 구현해야할 게임은 숫자 야구게임으로, 큰 흐름을 생각하면 다음과 같다.  

 

1. 컴퓨터가 랜덤한 3자리 숫자를 생성한다. 

2. 게임을 하는 사람은 3자리 수를 입력한다. 

3. 입력한 3자리 수에 대한 힌트를 출력한다. 

4. 위 과정을 반복한다.

5. 만약 3자리 수가 모두 일치한다면 게임종료를 알리고 재시작 여부를 묻는다. 

6. 재시작을 요청하면 재시작을, 종료를 요청하면 종료한다.

 

위 과정을 프로그래밍적으로 생각하기 전에 현실적으로 봐보자. 우선 3자리 숫자를 생성하는 컴퓨터가 필요할 것 같고, 이 게임을 즐기는 게이머, 컴퓨터의 수를 맞추기 위한 힌트, 힌트를 구하는 규칙이 있어야 할 것 같다. 이제 이러한 생각을 정리해보자. 

 

주요 객체

  • 컴퓨터 (상대방)
  • 게이머
  • 규칙
  • 힌트

 

주요 객체들의 속성과 행위

  • 컴퓨터 (상대방)
    • 3자리의 랜덤한 수를 가진다. 
  • 게이머
    • 3자리 숫자를 입력한다. 
  • 규칙
    • 게이머가 입력한 수와 컴퓨터의 수를 검증한다.
    • 승리를 판단한다.
  • 힌트
    • 스트라이크, 볼, 낫싱을 가지고 있다.
    • 각 힌트들의 수를 구한다.

 

 

객체 구현

컴퓨터

public class Computer {
    private final List<Integer> randomNumbers;

    public List<Integer> getRandomNumbers() {
        return this.randomNumbers;
    }
}

 

게이머

public class Gamer {
    public String inputNumbers() {
        return Console.readLine();
    }
}

 

규칙

public class Rule {
    public void applyHint(Gamer gamer, Computer computer) {
    }

    public boolean isReset(Gamer gamer) {
    }
}

 

힌트

public enum Hint {
    STRIKE("스트라이크", 0),
    BALL("볼", 0),
    NOTHING("낫싱", 0);

    private String value;
    private int count;

    Hint() {
    }

    Hint(String value, int count) {
        this.value = value;
        this.count = count;
    }

    public static String getHintByCount() {
        return "";
    }
}

 

이렇게 틀만 잡아두고, 나중에 구현하는 식으로 했다.

 

 

게임 구현

이제 제일 먼저 해야 할 일은 게임을 시작하고 진행해야 한다. 그럼 게임을 진행하는 Game.java를 만들어준다. 

public class Game {
    private final Computer computer;
    private final  Gamer gamer;
    private final Rule rule;

    public Game() {
        computer = new Computer();
        gamer = new Gamer();
        rule = new Rule();
    }
    
    public void play() {

    }
}

 

Game 인스턴스를 생성할 때 생성자로 다른 도메인 객체들을 생성해준다. 만들어준 다음 적용해보자. 

public class Application {
    public static void main(String[] args) {
        // TODO: 프로그램 구현
        Game game = new Game();
        game.play();
    }
}

 

 

게임이 시작되고 컴퓨터가 랜덤한 3자리 숫자를 생성해야 한다. 숫자가 중복되면 안되기 때문에 따로 중복되는 것을 확인하는 메서드를 만들어줬다. 

    public Computer() {
        List<Integer> numbers = new ArrayList<>();
        while (numbers.size() != 3) {
            int randomNumber = Randoms.pickNumberInRange(1, 9);
            if (!isDuplicateNumber(randomNumber, numbers)) {
                numbers.add(randomNumber);
            }
        }
        return numbers;
    }
    
    private static boolean isDuplicateNumber(int randomNumber, List<Integer> numbers) {
        for (Integer number : numbers) {
            if (number == randomNumber) {
                return true;
            }
        }
        return false;
    }

 

 

여기서 문제가 있다. 바로 Computer가 생성할 때 3자리를 생성하는 것이다. 랜덤한 3자리 수 생성하는 역할은 생성자의 역할이 아니기 때문에 따로 실행되어야 하기 때문에 분리해주어야 한다. 총 코드는 다음과 같다. 

public class Computer {
    private final List<Integer> randomNumbers;
    
   	public Computer() {
      	this.randomNumbers = generateNumbers();
    }

    public List<Integer> getRandomNumbers() {
        return this.randomNumbers;
    }

    private static List<Integer> generateNumbers() {
        List<Integer> numbers = new ArrayList<>();
      	while (numbers.size() != 3) {
        int randomNumber = Randoms.pickNumberInRange(1, 9);
        if (!isDuplicateNumber(randomNumber, numbers)) {
             numbers.add(randomNumber);
           }
        }
        return numbers;
    }
    
    private static boolean isDuplicateNumber(int randomNumber, List<Integer> numbers) {
        for (Integer number : numbers) {
            if (number == randomNumber) {
                return true;
            }
        }
        return false;
    }    
}

 

하지만 이렇게 보니 또 이상한 점이 있다. 컴퓨터 생성 시에 랜덤한 수를 생성하는데, 

new Computer();

 

위처럼 생성자가 실행할 때 3자리 숫자를 생성하는 흐름이 이상하게 느껴졌다. 그래서 이번 기회에 정적 팩토리 메서드를 써보기로 했다. 

    public static Computer createRandomNumbers() {
        return new Computer(generateNumbers());
    }

    private Computer(List<Integer> randomNumbers) {
        this.randomNumbers = randomNumbers;
    }

 

이렇게 간접적으로 new를 생성하고, 네이밍을 한 덕분에 Computer가 생성시에 어떤 일을 하는지 알 수 있게 되었다. 다음 기능으로 넘어가기 전에 한가지 더 해야할 일이 있다. 바로 아래 코드에서 3, 1, 9같은 숫자를 상수로 바꿔주는 작업이다. 

while (numbers.size() != 3) {
        int randomNumber = Randoms.pickNumberInRange(1, 9);

 

우리는 이러한 숫자를 매직넘버라고 부르는데, 매직넘버란 정체를 알 수 없지만 특정한 기능을 하는 숫자들을 의미한다. 딱 위 코드를 예시로 들 수 있는데, 3이 뭐하는 숫자인지, 1이랑 9가 뭘 뜻하는 건지 알 수 없다. 그래서 상수로 바꿈으로써 메세지로 전달할 수 있게 바꾸는 것이 좋다. 

public class Constant {
    public static final int RANDOM_NUMBERS_LENGTH = 3;
    public static final int START_RANDOM_NUMBER = 1;
    public static final int END_RANDOM_NUMBER = 9;
    public static final String RESET_GAME = "1";
}

 

다음과 같이 적용한다. 

while (numbers.size() != RANDOM_NUMBERS_LENGTH) {
            int randomNumber = Randoms.pickNumberInRange(START_RANDOM_NUMBER, END_RANDOM_NUMBER);

 

그리고 숫자를 입력받는 화면을 만들었다. 

public class View {
    private static final String START_GAME = "숫자 야구 게임을 시작합니다.";
    private static final String INPUT_NUMBERS = "숫자를 입력해주세요 : ";

    private View() {}

    public static void printStartGame() {
        System.out.println(START_GAME);
    }

    public static void inputNumbers() {
        System.out.print(INPUT_NUMBERS);
    }
}

 

Game.java

public Game() {
        printStartGame();
    }
    
    public void play() {
        while (!rule.isGameOver()) {
            inputNumbers();

        }
    }

 

게임 시작 화면과 입력하라는 화면을 띄웠으니, 게이머가 입력받을 수 있게 구현해보자. 

public class Gamer {
    public List<Integer> inputNumbers() {
        String inputNumbers = Console.readLine();
        validatesNumber(inputNumbers);
        return toList(inputNumbers);
    }
}

 

게이머가 입력한 숫자를 리스트로 반환했는데, 그 이유는 숫자를 검증할 때 좀 더 편하게 검증하기 위함이다. 게이머가 입력할 때는 3자리 숫자여야 한다. 만약 게이머가 잘못된 값을 입력하면 그게 어떤 상황인지 생각해보자. 

 

  • 숫자인지
  • 3자리인지
  • 중복된 숫자는 없는지

 

위 3가지 상황이라면 게임을 종료시켜야 한다. validatesNumber()을 자세히 보도록 하겠다. 

public class InputException {
    private static final String NOT_NUMBER = "숫자가 아닙니다.";
    private static final String NOT_NUMBER_LENGTH = "숫자의 길이가 맞지 않습니다.";
    private static final String DUPLICATE = "중복된 숫자가 있습니다.";
    private static final String NUMBER_REGEX = "^[0-9]*$";

    private InputException() {}

    public static void validatesNumber(String inputNumbers) {
        isNumber(inputNumbers);
        isLength(inputNumbers);
        isDuplicate(inputNumbers);
    }

    public static void isNumber(String inputNumbers) {
        if (!Pattern.matches(NUMBER_REGEX, inputNumbers)) {
            exception(NOT_NUMBER);
        }
    }

    public static void isLength(String inputNumbers) {
        if (inputNumbers.length() != RANDOM_NUMBERS_LENGTH) {
            exception(NOT_NUMBER_LENGTH);
        }
    }

    public static void isDuplicate(String inputNumbers) {
        List<String> inputNumberList = Arrays.asList(inputNumbers.split(""));
        if (inputNumberList.stream().distinct().count() < RANDOM_NUMBERS_LENGTH) {
            exception(DUPLICATE);
        }
    }

    private static void exception(String message) {
        throw new IllegalArgumentException(message);
    }
}

 

예외 메세지 역시 상수로 처리하였고, 중복되는 코드인 IllegalArgumentException()도 분리해주었다. 함부로 클래스 인스턴스를 생성하지 못하도록 생성자 접근 제어자를 private로 설정하였다. 

 

이제 문자를 리스트로 반환해야 한다. 먼저 문자를 int형 배열로 바꾼 뒤, 다시 리스트로 바꾸는 작업을 한다. 형 변환하는 작업은 게이머의 역할이 아니기 때문에 유틸로 따로 빼주었다. 

public class Util {
    private Util() {}

    public static int[] toIntArray(String inputNumbers) {
        return Stream.of(inputNumbers.split("")).mapToInt(Integer::parseInt).toArray();
    }

    public static List<Integer> toList(String inputNumbers) {
        int[] numbers = toIntArray(inputNumbers);
        return Arrays.stream(numbers)
                .boxed()
                .collect(Collectors.toList());
    }
}

 

그럼 게이머가 입력한 숫자를 적용해보겠다. 매개변수로 바로 입력한 숫자를 넣어주는 것이 아닌 객체를 넘겨주었다. 

 

Game.java

rule.applyHint(gamer, computer);

 

Rule.java

public void applyHint(Gamer gamer, Computer computer) {
        List<Integer> numbers = gamer.inputNumbers();

 

이제 입력한 숫자에 대해 컴퓨터의 숫자와 일치하는지 검증해야 한다. 

public void applyHint(Gamer gamer, Computer computer) {
        List<Integer> numbers = gamer.inputNumbers();
        List<Integer> computerNumbers = computer.getRandomNumbers();
        Hint.init();

        for(int i=0; i<numbers.size(); i++) {
            if(numbers.get(i) == computerNumbers.get(i)) {
                Hint.addCount(Hint.STRIKE);
            }
            else if(computerNumbers.contains(numbers.get(i))) {
                Hint.addCount(Hint.BALL);
            }
        }
    }

 

검증하기 전에 먼저 스트라이크, 볼, 낫싱 상수의 카운트를 초기화 시켜준다. 사실상 낫싱이라는 상수는 필요없다. 포함하는 숫자가 없을 때 값을 띄우기 위함으로 정의했다.

 

Hint.java

public static void init() {
        STRIKE.count = 0;
        BALL.count = 0;
        NOTHING.count = 0;
    }

 

예를 들어서, 컴퓨터의 숫자가 456이고 게이머가 입력한 숫자는 478이라고 했을 때, 1스트라이크라는 힌트를 보여줘야 한다. 따라서 해당 힌트의 카운트를 증가시키는 기능을 만들어야 한다. 

 

Hint.java

public static void addCount(Hint hint) {
        hint.count++;
    }

 

아까의 예시를 들고와서, 1스트라이크라면 스트라이크는 카운트가 1, 볼과 낫싱은 0일 것이다. 그럼 값과 카운트로 게이머한테 보여주어야 한다. 

 

Hint.java

public static String getHintByCount() {
        StringBuilder hint = new StringBuilder();
        if (BALL.count == 0 && STRIKE.count == 0) {
            return NOTHING.value;
        }

        if (BALL.count > 0) {
            hint.append(BALL.count).append(BALL.value).append(" ");
        }

        if (STRIKE.count > 0) {
            hint.append(STRIKE.count).append(STRIKE.value);
        }
        return hint.toString();
    }

 

먼저 스트라이크와 볼의 개수가 0이라면 낫싱을 반환한다. 그리고 볼부터 확인하면서 개수와 값을 더한 후 반환한다. 지금 if문이 3개가 되었는데, else를 없애려고 하다 보니 3개가 되었다. 힌트에 대한 결과값을 확인하는 역할은 Hint지만, 출력하는 역할은 아니다. 따라서 화면을 출력하는 클래스로 분리했다. 

 

View.java

    public static void printHint() {
        System.out.println(Hint.getHintByCount());
    }

 

만약 3스트라이크라면 게임승리 화면을 띄워야 한다. 원래는 승리를 판단하는 역할은 Rule이 한다. 

 

Hint.java

public static int getStrikeCount() {
        return STRIKE.count;
    }

 

Rule.java

public boolean isGameOver() {
	Hint.getStrikeCount() == RANDOM_NUMBERS_LENGTH;
}

 

Game.java

while (!rule.isGameOver) {

 

게임을 승리했으니 승리했다는 화면과 게임 재시작 여부 화면을 띄워줘야 한다. 

 

View.java

private static final String GAME_OVER = "3개의 숫자를 모두 맞히셨습니다! 게임 종료";
private static final String INPUT_RESET = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.";

 public static void printGameOver() {
        System.out.println(GAME_OVER);
        System.out.println(INPUT_RESET);
    }

 

Game.java

public void play() {
        while (!rule.isGameOver) {
            inputNumbers();
            rule.applyHint(gamer, computer);
            printHint();
        }

        printGameOver();
    }

 

마지막으로 입력을 받고 재시작을 하거나 종료시키면 게임이 완성된다. 

 

Gamer.java

public String inputReset() {
        String inputReset = Console.readLine();
        validatesResetNumber(inputReset);
        return inputReset;
    }

 

게임 재시작 여부 입력 역시 예외처리를 해주어야 한다. validatesResetNumber()을 자세히 봐보자. 

private static final String RESET_NUMBER_REGEX = "^[1-2]*$";
private static final String NOT_RESET_NUMBER = "재시작 여부 입력 형식이 맞지 않습니다.";

public static void validatesResetNumber(String resetNumber) {
        isNumber(resetNumber);
        isResetNumberFormat(resetNumber);
    }
    
 public static void isResetNumberFormat(String resetNumber) {
        if (!Pattern.matches(RESET_NUMBER_REGEX, resetNumber)) {
            exception(NOT_RESET_NUMBER);
        }
    }

 

검증하는 상황은 총 2가지다. 

 

  • 숫자인지
  • 1또는 2만 입력했는지

 

이제 게임에서 1이라면 재시작을, 2라면 종료를 하면 된다. 

 

Game.java 

if (rule.isReset(gamer)) {
            play();
        }

 

Rule.java

public boolean isReset(Gamer gamer) {
        return gamer.inputReset().equals(RESET_GAME);
    }

 

마지막으로 Game을 생성할 때에 게임 시작화면을 출력하고, 게임 실행 시 다른 객체들을 생성하도록 바꾸겠다. 총 코드는 다음과 같다. 

public class Game {
    public Game() {
        printStartGame();
    }

    public void play() {
        Computer computer = createRandomNumbers();
        Gamer gamer = new Gamer();
        Rule rule = new Rule();
        Hint.init();

        while (Hint.getStrikeCount() != RANDOM_NUMBERS_LENGTH) {
            inputNumbers();
            rule.applyHint(gamer, computer);
            printHint();
        }

        printGameOver();
        if (rule.isReset(gamer)) {
            play();
        }
    }
}

 

 

생각

이번 과제를 하면서 배웠던 점은 3가지였다. 

 

  • 클래스보다 객체의 속성과 행위가 우선이다.
  • enum클래스의 장점인 상태와 행위를 한곳에서 관리하는 코드를 짤 수 있었다.
  • 테스트코드에 대한 관점이 바뀌게 되었다. 

 

테스트코드는 예외처리밖에 짜지 못했는데, 2주차 피드백에서 받은 도메인에 대한 단위 테스트에 대해서 알게 되었고, 한번 적용해봐야겠다는 생각이다. 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday