ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TDD 기반 야구 게임 개발하기 5. 게임 결과 출력기
    레거시/레거시-야구 게임 (Feat. TDD) 2018. 8. 9. 19:49
    반응형

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


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

    5단계. 콘솔 기반 게임 결과 출력기


    1. 기능 정의와 테스트 시작


    이번에 개발해볼 기능은 게임 결과 출력기입니다. 이전에 만들어두었던 계산기가 만든 결과를 이용하여 시스템에 맞게 출력하는 기능이죠. 현재 야구 게임에서 나올 수 있는 결과는 다음과 같습니다.


    1. 3 스트라이크 
    2. 2 스트라이크
    3. 1 스트라이크 2볼
    4. 1 스트라이크 1볼
    5. 1 스트라이크
    6. 3볼
    7. 2볼
    8. 1볼
    9. 낫싱


    총 9개의 결과가 있습니다. 지금 저희가 만든 기능으로는 결과를 2개짜리 int형 배열로 생성합니다. 따라서 그 배열을 적절히 결과 표현만 해주면 됩니다. 예를 들어 결과가 (3, 0)이라면 "3 스트라이크" 라는 문자열을 출력하면 됩니다. 얼추 기능 정의가 완료된 것 같으니 바로 테스트를 시작해볼까요? 먼저 테스트 디렉토리에 ConsoleGameResultPrinterTest라는 클래스를 다음과 같이 만들어주세요.


    [src/test/java/ConsoleGameResultPrinterTest.java]

    import org.junit.Test;

    public class ConsoleGameResultPrinterTest {
    @Test
    public void createConsoleGameResultPrinter(){
    ConsoleGameResultPrinter printer = new ConsoleGameResultPrinter();
    }
    }

    이제 익숙하실 겁니다. 테스트를 실패했으니 바로 소스코드를 작성하죠.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    }

    자 테스트가 통과하는지 살펴볼까요? 테스트가 통과하는군요. 리팩토링이 있는지 살펴볼 차례입니다. 역시나 없습니다. 딱히 만든 것이 없으니까요. 출력기는 여지껏 만들었던 기능들과 다르게 maxLen이라는 게임에서 주어지는 길이 정보 자체가 필요하지 않습니다. 왜냐하면 그냥 이 녀석은 결과를 받고 그에 따른 출력만 해주면 되는 아이니까요. 바로 Git에 "ConsoleGameResultPrinter 생성 테스트 완료"라고 해서 올려두죠.


    2. 본격적인 테스트 시작


    일단 결과를 출력하기 전에 게임 결과를 받고 정확한 문자열을 출력하는지 살펴보도록 하겠습니다. 테스트 코드에 다음과 같이 작성해주세요.


    [src/test/java/ConsoleGameResultPrinterTest.java]

    import org.junit.Test;
    import static org.junit.Assert.assertEquals;

    public class ConsoleGameResultPrinterTest {
    //이전과 동일
    @Test
    public void printerConvertGameResultNothing(){
    ConsoleGameResultPrinter printer = new ConsoleGameResultPrinter();
    assertEquals(printer.convertGameResult(new int[]{0, 0}), "낫싱");

    }
    }

    자 이제 테스트가 실패합니다. convertGameResult는 출력하기 쉽게 결과에 따라 원하는 문자열을 만들어주는 메소드입니다. 실패했으니 소스 코드를 만들어볼까요?


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    public String convertGameResult(int[] res) {
    return "낫싱";
    }
    }

    이제 테스트를 통과하게 되었습니다. 이제 리팩토링을 적용해봅시다. 소스 코드에는 중복된 곳이 없습니다. 테스트 코드에서는 출력기를 생성하는 부분이 중복되었군요. 이전처럼 내부 필드로 만들어서 @Before + setUp 메소드로 빼두도록 하겠습니다. 다음처럼 말이죠.


    [src/test/java/ConsoleGameResultPrinterTest.java]

    import org.junit.Before;
    import org.junit.Test;
    import static org.junit.Assert.assertEquals;

    public class ConsoleGameResultPrinterTest {

    private ConsoleGameResultPrinter printer;
    @Before
    public void setUp() throws Exception {
    printer = new ConsoleGameResultPrinter();
    }
    @Test
    public void printerConvertGameResultNothing(){
    assertEquals(printer.convertGameResult(new int[]{0, 0}), "낫싱");
    }
    }

    자 이제 리팩토링까지 마쳤으니 Git에 "ConsoleGameResultPrinter : convertGameResult 생성" 이라고 코멘트를 달아서 올리도록 하죠. 이제 그 다음 쉬운 테스트는 스트라이크만 존재할 때, O 스트라이크를 변환시키는지에 대한 테스트입니다. 테스트 코드를 다음과 같이 작성해주세요. 


    [src/test/java/ConsoleGameResultPrinterTest.java]

    import org.junit.Before;
    import org.junit.Test;

    import static org.junit.Assert.assertEquals;

    public class ConsoleGameResultPrinterTest {

    //이전과 동일
    @Test
    public void printerConvertGameResultStrikes(){
    assertEquals(printer.convertGameResult(new int[]{1, 0}), "1 스트라이크");
    assertEquals(printer.convertGameResult(new int[]{2, 0}), "2 스트라이크");
    assertEquals(printer.convertGameResult(new int[]{3, 0}), "3 스트라이크");
    }
    }

    제일 쉽게 테스트를 통과시키려면 결과가 (0, 0) 일때는 낫싱을 나머지의 경우에는 스트라이크 수 + 스트라이크 라는 문자열을 만들어서 리턴하면 될 것 같습니다. 바로 소스 코드를 작성해보죠.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    public String convertGameResult(int[] res) {
    return (res[0] == 0 && res[1] == 0) ? "낫싱" : res[0] + " 스트라이크";
    }
    }

    자 테스트를 통과했으니 리팩토링 여부를 살펴보죠. convertGameResult 내에서 낫싱을 판단하는 조건문을 private 메소드로 빼두도록 하지요. 다음처럼 말입니다.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    public String convertGameResult(int[] res) {
    return isNothing(res) ? "낫싱" : res[0] + " 스트라이크";
    }
    private boolean isNothing(int[] res) {
    return res[0] == 0 && res[1] == 0;
    }
    }

    리팩토링도 마쳤으니 Git에 올리도록 하죠. 코멘트는 "ConsoleGameResultPrinter : convertGameResult 1. 낫싱, 2. 단일 스트라이크 처리" 라고 해두겠습니다. 이제 단일 볼 처리를 해보도록 하죠. 기본적으로 단일 스트라이크 처리와 같습니다.(여기서 단일이란 게임 결과가 스트라이크만 혹은 볼만 나온 것을 의미합니다.) 테스트 코드를 다음과 같이 작성해주세요.


    [src/test/java/ConsoleGameResultPrinterTest.java]

    import org.junit.Before;
    import org.junit.Test;

    import static org.junit.Assert.assertEquals;

    public class ConsoleGameResultPrinterTest {

    //이전과 동일
    @Test
    public void printerConvertGameResultBalls(){
    assertEquals(printer.convertGameResult(new int[]{0, 1}), "1 볼");
    assertEquals(printer.convertGameResult(new int[]{0, 2}), "2 볼");
    assertEquals(printer.convertGameResult(new int[]{0, 3}), "3 볼");
    }
    }

    이제 테스트가 실패합니다. 어떻게 해야 통과될까요? 역시 제일 쉬운 방법은 낫싱처럼 조건문을 만들어서 처리하는겁니다. 다음처럼 말이죠.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    public String convertGameResult(int[] res) {
    if (isNothing(res))
    return "낫싱";
    if(res[0] > 0 && res[1] == 0)
    return res[0] + " 스트라이크";
    if (res[0] == 0 && res[1] > 0)
    return res[1] + " 볼";
    return "";
    }
    private boolean isNothing(int[] res) {
    return res[0] == 0 && res[1] == 0;
    }
    }

    이제 테스트가 통과하는군요. 테스트를 통과시켰으니 바로 리팩토링에 들어가겠습니다. 소스 코드 내 조건문들을 다 메소드로 바꾸도록 하지요.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    public String convertGameResult(int[] res) {
    if (isNothing(res))
    return "낫싱";
    if(isUnitStrikes(res))
    return res[0] + " 스트라이크";
    if (isUnitBalls(res))
    return res[1] + " 볼";
    return "";
    }
    private boolean isNothing(int[] res) {
    return res[0] == 0 && res[1] == 0;
    }
    private boolean isUnitStrikes(int[] res) {
    return res[0] > 0 && res[1] == 0;
    }
    private boolean isUnitBalls(int[] res) {
    return res[0] == 0 && res[1] > 0;
    }
    }

    자 이제 리팩토링을 마쳤으니 잊지 말고 Git에 "ConsoleGameResultPrinter : convertGameResult 3. 단일 볼 처리"라고 코멘트를 달아서 올리도록 하지요. 이제 스트라이크와 볼이 혼합되었을 때의 처리입니다. 테스트 코드를 다음과 같이 작성해주세요.


    [src/test/java/ConsoleGameResultPrinterTest.java]

    import org.junit.Before;
    import org.junit.Test;

    import static org.junit.Assert.assertEquals;

    public class ConsoleGameResultPrinterTest {

    //이전과 동일
    @Test
    public void printerConvertGameResultStrikesAndBalls(){
    //2, 1 은 불가능하다
    assertEquals(printer.convertGameResult(new int[]{1, 2}), "1 스트라이크 2 볼");
    assertEquals(printer.convertGameResult(new int[]{1, 1}), "1 스트라이크 1 볼");
    }
    }

    테스트를 진행하면 역시 실패합니다. 소스 코드를 고쳐보기 전에 스트라이크와 볼이 섞여 나올때는 어떤 때일 까요? 당연하게도 스트라이크, 볼 개수가 둘 다 0보다 많을 때입니다. 이전에 검사했던 조건들을 살펴볼까요?


    1. 스트라이크 == 0 && 볼 == 0
    2. 스트라이크 > 0 && 볼 == 0
    3. 스트라이크 == 0 && 볼 > 0


    이전 조건들을 거치면서 자연스럽게 이제 스트라이크와 볼이 0개보다 많은 경우만이 현재 ""을 반환하게 됩니다. 따라서 소스코드를 다음처럼 변경하시면 됩니다.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    public String convertGameResult(int[] res) {
    if (isNothing(res))
    return "낫싱";
    if(isUnitStrikes(res))
    return res[0] + " 스트라이크";
    if (isUnitBalls(res))
    return res[1] + " 볼";
    return res[0] + " 스트라이크 " + res[1] + " 볼";
    }
    //이전 동일
    }

    자 이제 테스트도 통과하니 리팩토링을 해볼까요? 아무래도 더 명확학게 res[0], res[1]을 각각 strikesCnt, ballsCnt로 바꿔주는게 좋을 것 같군요 다음처럼 말이죠.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    public String convertGameResult(int[] res) {
    int strikesCnt = res[0], ballsCnt = res[1];

    if (isNothing(res))
    return "낫싱";
    if(isUnitStrikes(res))
    return strikesCnt + " 스트라이크";
    if (isUnitBalls(res))
    return ballsCnt + " 볼";
    return strikesCnt + " 스트라이크 " + ballsCnt + " 볼";
    }
    //이전과 동일

    }

    이제 리팩토링까지 무사히 마쳤으니 Git에 "ConsoleGameResultPrinter : convertGameResult 완료" 라고 코멘트를 달아서 올려줍시다. 이제 결과를 적절한 포맷으로 변환시켰으니 print라는 메소드를 만들도록 하겠습니다. 이번에도 살짝 편법을 쓰겠습니다. 왜냐하면 print는 결과를 받아서 적절한 포맷으로 바꾼 후 출력하는 것 밖에 없거든요. 대신 이번에는 main 메소드를 만들어서 테스트해보도록 하죠. 다음과 같이 소스 코드를 작성해주세요.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter {
    //이전과 동일

    public void print(int[] res){
    String convertRes = convertGameResult(res)
    ;
    System.out.println(convertRes);
    }

    public static void main(String[] args) {
    ConsoleGameResultPrinter p =
    new ConsoleGameResultPrinter();
    //야구 게임이 가질 수 있는 경우의 수
    p.print(new int[]{3, 0});
    p.print(new int[]{2, 0});
    p.print(new int[]{1, 2});
    p.print(new int[]{1, 1});
    p.print(new int[]{1, 0});
    p.print(new int[]{0, 3});
    p.print(new int[]{0, 2});
    p.print(new int[]{0, 1});
    p.print(new int[]{0, 0});
    }
    }

    이제 main을 실행해보면 적절한 값을 출력하는 것을 확인 할 수 있을 겁니다. 이제 게임에 필요한 모든 기능을 만들었으니 넘어가도 되겠네요? 하하 이전 포스팅에서 공부했던 SRP "하나의 클래스는 하나의 기능만을 담당한다"라는 규칙에 따라서 클래스를 분리하도록 하죠. 출력기는 출력만 담당하는게 옳습니다. 따라서 게임 결과를 적절한 포맷으로 형태를 변환시켜주는 컨버터를 따로 만들어야 하지요. 다만 이 컨버터는 출력기에 종속된 형태입니다. 출력하기 위해 변환시키는 것이니까요. 이제 클래스를 분리해보도록 하겠습니다.


    [src/main/java/ConsoleGameResultPrinter.java]

    public class ConsoleGameResultPrinter{
    private GameResultConverter converter;

    public ConsoleGameResultPrinter(){
    converter = new GameResultConverter();
    }

    public void print(int[] res){
    String convertRes = converter.convertGameResult(res);
    System.out.println(convertRes);
    }

    //테스트를 위한 main 메소드 삭제
    }


    [src/main/java/GameResultConverter.java]

    public class GameResultConverter {
    public String convertGameResult(int[] res) {
    int strikesCnt = res[0], ballsCnt = res[1];

    if (isNothing(res))
    return "낫싱";
    if(isUnitStrikes(res))
    return strikesCnt + " 스트라이크";
    if (isUnitBalls(res))
    return ballsCnt + " 볼";
    return strikesCnt + " 스트라이크 " + ballsCnt + " 볼";
    }

    private boolean isNothing(int[] res) {
    return res[0] == 0 && res[1] == 0;
    }

    private boolean isUnitStrikes(int[] res) {
    return res[0] > 0 && res[1] == 0;
    }

    private boolean isUnitBalls(int[] res) {
    return res[0] == 0 && res[1] > 0;
    }
    }

    그리고 기존 테스트는 변환에 대한 테스트이므로 파일 이름을 GameResultConverterTest로 바꾸고 클래스를 변경시켜주면 됩니다. 다음처럼 말이죠.


    [src/test/java/GameResultConverterTest.java]

    import org.junit.Assert;
    import org.junit.Before;
    import org.junit.Test;

    import static org.junit.Assert.assertEquals;

    public class GameResultConverterTest {

    private GameResultConverter converter;
    @Before
    public void setUp() throws Exception {
    converter = new GameResultConverter();
    }
    @Test
    public void printerConvertGameResultNothing(){
    Assert.assertEquals(converter.convertGameResult(new int[]{0, 0}), "낫싱");
    }
    @Test
    public void printerConvertGameResultStrikes(){
    assertEquals(converter.convertGameResult(new int[]{1, 0}), "1 스트라이크");
    assertEquals(converter.convertGameResult(new int[]{2, 0}), "2 스트라이크");
    assertEquals(converter.convertGameResult(new int[]{3, 0}), "3 스트라이크");
    }
    @Test
    public void printerConvertGameResultBalls(){
    assertEquals(converter.convertGameResult(new int[]{0, 1}), "1 볼");
    assertEquals(converter.convertGameResult(new int[]{0, 2}), "2 볼");
    assertEquals(converter.convertGameResult(new int[]{0, 3}), "3 볼");
    }
    @Test
    public void printerConvertGameResultStrikesAndBalls(){
    //2, 1 은 불가능하다
    assertEquals(converter.convertGameResult(new int[]{1, 2}), "1 스트라이크 2 볼");
    assertEquals(converter.convertGameResult(new int[]{1, 1}), "1 스트라이크 1 볼");
    }
    }

    자 이제 SRP 원칙에 맞춰 클래스도 분리했으니 Git에 "ConsoleGameResultPrinter SRP 적용, GameResultConverter 분리. 클래스 완성"이라고 코멘트를 달아서 올리도록 하지요.


    3. 마치며....


    이번 포스팅에서는 콘솔 기반 게임 결과 출력기를 만들었고 SRP 원칙 "한 클래스는 하나의 기능만을 담당해야 한다" 에 따라서 컨버터를 따로 출려기에 분리해놓았습니다. 전반적으로 TDD에 대해 조금씩 익숙해지는 느낌이 드시나요? 사실 TDD에 대한 지식이 부족해서 여러 편법을 함께 적용하고 있자니 저는 마음이 불편합니다만... 아무튼 이제 이 프로젝트도 끝이 보이는 군요. 다음 포스팅에서는 대망의 Game 클래스를 만들어보겠습니다.

Designed by Tistory.