ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TDD 기반 야구 게임 개발하기 6. 야구 게임 마무리
    24년 11월 이전/레거시-야구 게임 (Feat. TDD) 2018. 8. 13. 21:29
    반응형

    * 먼저 이 프로젝트의 지적 자산은 '코드 스쿼드'에 있음을 밝힙니다. 필자의 프로젝트의 소스코드는 https://github.com/gurumee92/baseballtdd/ 에 있습니다.


    테스트 주도 개발 기반 야구 게임 만들기

    6단계. 야구 게임 마무리


    1. 게임 클래스 완성하기


    오늘은 마지막으로 게임 클래스를 완성시키도록 하겠습니다. 기능별로 단위 테스트가 끝났기 때문에 적절하게 Game 클래스는 별도의 테스트가 필요없이 로직에 따라 완성시키면 됩니다. 우선 야구 게임의 흐름을 다시 한 번 살펴볼까요?


    1. 임의의 서로 다른 3자리 숫자열을 생성한다.
    2. 사용자 입력을 서로 다른 3자리 숫자열로 받는다.
    3. 생성된 숫자열과 입력 받은 숫자열을 비교한 결과를 얻는다.
    4. 결과를 출력한다.
    5. 3스트라이크가 나올 때까지 2~4번을 반복한다.


    이대로 코드를 옮기면 됩니다. 저는 Game클래스를 생성시킨 후, 저 흐름을 main에서 바로 실행시키는 것이 아닌 새로운 메소드로 추상화해서 호출할 계획입니다. 저의 메인 코드는 다음과 같습니다.


    [src/main/java/Game.java]

    public class Game {
    public static final int DEFAULT_LEN = 3;

    private int maxLen;

    //이전과 동일

    public static void main(String[] args) {
    Game game = new Game(DEFAULT_LEN);
    game.run();
    }

    }


    바로 이 run 메소드에 의해서 앞서 정의했던 흐름을 적으면 되지요. 그러기 위해서는 이전의 모델링에서 했던 것처럼 Game은 랜덤 숫자열 생성기, 사용자 입력 숫자열 생성기, 결과 계산기, 결과 출력기를 가져야만 합니다. 그 후 생성자에서 필드를 적절히 초기화한 후 로직대로 코드를 짜면 되죠.


    [src/main/java/Game.java]

    public class Game {
    //이전과 동일

    private RandomBallsGenerator systemGenerator;
    private UserInputBallsGenerator userGenerator;
    private GameResultCalculator calculator;
    private ConsoleGameResultPrinter printer;

    //이전과 동일

    public Game(int maxLen){
    this.maxLen = maxLen;

    systemGenerator = new RandomBallsGenerator(maxLen);
    userGenerator = new UserInputBallsGenerator(maxLen);
    calculator = new GameResultCalculator(maxLen);
    printer = new ConsoleGameResultPrinter();
    }

    public void run(){
    String systemBalls =
    systemGenerator.generateBalls();
    int [] res;

    do {
    String userBalls =
    userGenerator.generateBalls();
    res = calculator.calculateGameResult(systemBalls, userBalls);
    printer.print(res);
    }while(res[0] != 3);

    System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
    }

    //이전과 동일
    }

    자 이제 실행 결과를 돌려보면 정확하게 동작하는 것을 알 수 있죠. 이렇게 과감하게 코드를 짤 수 있던 것은 이미 위의 기능들의 테스팅이 되었기 때문입니다. 따라서 저희는 실행 흐름에 맞게 동작하는지만 확인만 하면 됩니다. 자 모두 끝났습니다. 진짜 끝내기 전에 테스트는 안 했지만 리팩토링을 해볼까요? 우선 게임의 클리어 조건입니다. 이는 꽤 중요한 정보이니 따로 메소드로 빼두겠습니다 다음과 같이 말이죠.


    [src/main/java/Game.java]

    public class Game {
    //이전과 동일

    public void run(){
    String systemBalls =
    systemGenerator.generateBalls();
    int [] res;

    do {
    String userBalls =
    userGenerator.generateBalls();
    res = calculator.calculateGameResult(systemBalls, userBalls);
    printer.print(res);
    }while(!isGameClear(res));

    System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
    }

    private boolean isGameClear(int[] result) {
    int strikesCnt = result[0];
    return strikesCnt == 3;
    }

    //이전과 동일
    }

    자 이제 더 이상 코드적으로 리팩토링할 것은 없어 보입니다. 이제 Git에 "Game 클래스 완성"이라는 코멘트를 달아서 올리도록 하죠.


    2. 끝내도 될까...?


    프로그래밍 제한 사항도 지켰고, 코드도 나름 깔끔하게 짠듯해서 뿌듯합니다. 물론 지금 끝내도 되지만, 한번 더 고칠 곳이 없나 살펴보도록 하죠. 기존 설계는 다음과 같습니다.



    그런데 현재는 SRP 원칙에 따라서 클래스를 더 나누었기 때문에 살짝 모습이 변경됐죠? 현재 클래스들의 모습은 다음과 같습니다.


    현재 여기서 빨간색으로 이어진 것은 시스템과 강하게 결힙되어 있단 뜻입니다. 무슨 말이냐면 UserBallsGenerator는 ConsoleUerInputReader를 직접적으로 생성하고 있다는 뜻이지요. 이렇게 되면 다른 시스템에 갔을 때 이것들을 다시 작업해야할 가능성이 높아집니다. 원래 저의 설계에서는 여러 시스템 내에서도 잘 동작할 수 있게끔 이들을 분류하였는데 잘 안된 것이라 볼 수 있죠. 다시 한번 여지껏 만들어두었던 클래스들의 분류가 필요한 시점입니다. 다시 게임에 종속된 기능과 시스템에 종속된 기능들을 분류해볼까요?


     게임 종속적인 기능

     둘 다?

     시스템 종속적인 기능

     랜덤 숫자열 생성기

     결과 계산기

     입력 검증기

     결과 변환기

     입력 숫자열 생성기

     콘솔 입력기

     결과 출력기


    여기서 한 번만 더 잘 생각해봅시다. 결과 출력기는 정확히 말해 게임 내 종속적인 기능인 결과 변환기에 의해 게임 결과가 알맞는 문자열(게임에서 정한 포맷)을 그냥 출력하는 역할밖에 하지 않습니다. 그렇다면 Game 클래스 자체를 ConsoleGame이라고 바꾸고 run에서 System.out.println()으로 바꿔도 이상한 모습이 아닐 듯 싶습니다. 왜냐하면 콘솔에서 실행되는 게임이니까요. 따라서 결과 출력기 자체를 제거하도록 하지요. 그리고 입력 숫자열 생성기는 엄밀히 말하면 게임 종속적인 기능이나 생성기에서 사용할 기능인 입력기는 시스템에 종속적인 녀석이지요. 따라서 최종 모델은 다음과 같이 변경시킬 수 있습니다.


    이제 설계도에 맞게 클래스를 재편해볼까요? 일단 앞서 말했듯이 입력기는 시스템에 종속적인 녀석입니다. 따라서 시스템에 맞는 사용자 입력기를 만들어야 하겠죠? 따라서 이 부분은 인터페이스가 필요합니다. 여러 시스템에서 조금씩만 바꿔서 쓰게끔요. 사용자 입력기에 대해서 다음과 같은 인터페이스를 생성시켜주세요.


    [src/main/java/UserInputReader]     

    public interface UserInputReader {
    String
    read(); } 

    그 후 ConsoleUserInputReader를 해당 인터페이스를 구현하게 합니다.


    [src/main/java/ConsoleUserInputReader]

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;

    public class ConsoleUserInputReader implements UserInputReader{

    //이전과 동일
    }

    이제 UserInputBallsGenerator를 보겠습니다. 이 아이는 설계에 따르면 게임 클래스에 속하면서도 시스템에게서 입력기를 전달 받아 생성되어야 마땅합니다. 그리고 현재는 시스템이 Console이니 ConsoleUserInputBallsGenerator로 다시 명명하겠습니다. 그리고 코드를 다음과 같이 바꿔주세요.

    [src/main/java/ConsoleUserInputBallsGenerator]

    import java.util.Arrays;

    public class ConsoleUserInputBallsGenerator {
    //게임에서 제공받는다.
    private UserInputValidator validator;
    //시스템에게 제공받는다.
    private UserInputReader reader;

    public ConsoleUserInputBallsGenerator(UserInputReader reader, UserInputValidator validator){
    this.reader = reader;
    this.validator = validator;
    }

    //이전과 동일
    }

    자 여기서 인터페이스를 생성자를 갖지요. 여기가 의존성 역전이 일어난 곳입니다. 코드 상으로는 reader는 여전히 입력 숫자열 생성기에 종속된 상태입니다. 하지만 런타임 시에 시스템에서 전달해준 reader에 종속되게 됩니다. 이런 것을 흔히들 의존성 역전 혹은 제어라고 표현합니다. 그리고 Game클래스를 ConsoleGame으로 바꾸고 다음과 같이 바꿔주시면 됩니다.


    [src/main/java/ConsoleGame.java]

    public class ConsoleGame {
    public static final int DEFAULT_LEN = 3;

    private int maxLen;
    //게임 종속저인 기능
    private RandomBallsGenerator systemGenerator;
    private UserInputValidator validator;
    private GameResultCalculator calculator;
    private GameResultConverter converter;
    //게임 종속적인것 + 시스템 종속적인것
    private ConsoleUserInputBallsGenerator userGenerator;

    public ConsoleGame(int maxLen, UserInputReader reader){
    //게임 정보
    this.maxLen = maxLen;
    //게임에 종속된 기능들 생성
    systemGenerator = new RandomBallsGenerator(maxLen);
    validator = new UserInputValidator(maxLen);
    calculator = new GameResultCalculator(maxLen);
    converter = new GameResultConverter();
    //시스템 의존하는 기능 넣어줌.
    userGenerator = new ConsoleUserInputBallsGenerator(reader, validator);
    }
    public void run(){
    String systemBalls = systemGenerator.generateBalls();
    int [] res;
    do {
    String userBalls = userGenerator.generateBalls();
    res = calculator.calculateGameResult(systemBalls, userBalls);
    System.out.println(converter.convertGameResult(res));
    }while(!isGameClear(res));
    System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
    }
    private boolean isGameClear(int[] result) {
    int strikesCnt = result[0];
    return strikesCnt == 3;
    }

    public static void main(String[] args) {
    //시스템 기반 유저 입력기
    UserInputReader reader = new ConsoleUserInputReader();
    ConsoleGame game = new ConsoleGame(DEFAULT_LEN, reader);
    game.run();
    }
    }

    메인 메소드에서 콘솔 기반 입력기를 생성해서 게임에 넣어주는 것을 살펴봐주세요. 이제 Git에 "진짜 마무리!"라고 코멘트를 달아서 올려줍시다. 이제 진짜 끝!


    3. 마치며...


    이번 프로젝트는 전체적으로 TDD 기반으로 작성해서 마무리하였습니다! 라고 자신있게 표현하기가 애매하군요. 아직 저 자신이 TDD를 충분히 알지 않은데 무모하게 덤볐던 탓일까요? 1~5장까지는 그래도 TDD에 잘 준수했다고 생각했는데 6장에 와서 망가진 느낌입니다ㅠㅠ 아무튼 오늘로 TDD기반 야구 게임 개발하기를 마치겠습니다. 여기까지 두서 없는 글 읽으시느라 고생하셨습니다. 사실 이 프로젝트는 하루만에 끝냈었는데 포스팅에 이것 저것 제 욕심을 채우느라 글도 지저분하고 오래 걸리게 되었네요. 끝이 찝찝하지만 아무튼 정말 고생하셨습니다. 다음엔 더 잘 갈무리된 포스팅을 올리도록 더 노력하겠습니다~!

    728x90
Designed by Tistory.