초보 개발자 코드 트레이닝, Part 4. TDD 수련 - 야구게임 1편

초보 개발자 코드 트레이닝, Part 4. TDD 수련


야구게임을 TDD방식으로 구현해 보는 것입니다. 게시된지 좀 지났는데, 사실 TDD를 잘몰라 책부터 구입해서 보느라고 좀 늦었습니다. (나름 사정이) 다 읽은건 아니지만 대충 감은 와서 달려보았습니다.


일단 야구게임의 규칙은


  • 컴퓨터는 미리 3개의 1~9 사이의 숫자를 가지고 있다(예. 1 3 5).
  • 사용자는 컴퓨터가 가지고 있는 숫자를 정확히 맞춰야 한다.
  • 사용자는 미리 지정된 만큼의 횟수 동안 시도할 수 있다. 지정된 횟수까지 시도해도 맞추지 못하면 "out"이다.
  • 사용자는 매번 3개의 숫자를 넣고 컴퓨터는 미리 가지고 있는 3개의 숫자와 비교해 다음 응답을 해야 한다.


1) 3개의 숫자가 맞고 위치도 맞으면 "hit"

2) 1)이 아닐 경우 입력한 숫자가 컴퓨터가 가지고 있는 숫자 중 하나이고 그 위치도 맞으면 strike가 1씩 올라간다.

예를 들어 컴퓨터의 숫자가 1 3 5인데 사용자가 입력한 숫자가 2 3 4이면 "1 strike"이고 2 3 5를 입력했다면 "2 strikes"이다.

3) 숫자는 맞으나 위치가 다르면 ball이 1씩 올라간다. "1 ball", "2 balls", "3 balls"


구체적인 요구사항 목록입니다.

  • BaseballGame 클래스를 메인 클래스로 작성한다.
  • 완성된 코드는 여러 개의 클래스로 구성해도 상관없다.
  • 게임은 여러 번 진행할 수 있다.
  • 게임을 시작하려면 컴퓨터가 가진 숫자와 최대 시도 횟수가 등록되어 있어야 한다.
  • 게임이 시작되면 이번이 몇 번째 게임인지 정보를 돌려준다.
  • 두 번째 게임부터 컴퓨터의 숫자는 이전 게임에서 사용한 것과 똑 같은 것을 쓸 수 없다. 단 최대 시도 횟수는 같아도 상관없다.
  • 게임이 종료되는 것은 "hit" 또는 "out"이 되었을 때다.
  • 게임이 종료된 후 다시 게임을 시작하기 전에는 게임을 시도할 수 없다.
  • 게임 중에는 게임의 현재 상태를 요청할 수 있다. 게임 상태는 최대 시도 가능 횟수, 현재 시도한 횟수, 그리고 현재까지의 최고기록 정보를 돌려준다. 최고 기록은 strike는 3점, ball은 1점으로 계산해 그 중 최고 점수를 가진 시도의 정보(몇번째 시도와 시도한 숫자)를 돌려준다. 최고 기록이 같은 경우가 두 번 이상 있으면 모두 돌려준다.
  • 게임의최고 기록을 요청하면 지금까지 진행된 게임 중 가장 적은 시도로 맞춘 결과(시도 횟수)를 보여준다. 게임의 최고 기록을 요청하는것은 게임이 종료된 이후에만 가능하며 한 번도 게임을 시도하지 않았으면 결과를 요청할 수 없다.
  • 모든 단서조항(...한다, ...할 수 없다, ...일 경우는)에 위배되는 경우는 적절한 Exception을 던져야 한다.
  • 각 결과는 적절한 형태로(String 또는 임의의 Object) 리턴되면 된다(화면에 출력될 필요는 없다. 엔진이니까. UI는 이번 엔진을 이용해 나중에 따로 개발할 것이다).


요구사항이 단위가 크다고 생각되어 좀 잘랐습니다.



  1. 게임숫자는 3자리이다.
  2. 숫자의 범위는 1부터 9까지이다.
  3. 숫자를 비교할수 있어야한다. (같다와 다르다를 알수 있어야한다.)
        예)1,2,3 == 1,2,3, 1,2,4 != 1,3,4
  4. 숫자들의 비교결과를 상세히 알수 있어야한다..(strike, ball, hit, out)
  5. 특정 시도 횟수 내에 맞출수도 있고, 아웃이 될수도 있다. 다시 게임을 하면 이전에 생성한 숫자는 쓸수 없다.
        숫자를 다시 생성할때 이전 기록과 중복여부를 판단하고 같다면 다시 생성한다.
  6. 사용자가 숫자를 등록하면 컴퓨터와 비교하고 결과를 리턴, 매번 시도 횟수도 출력해야한다.
        예) 현재 1strike 기회 3번시도중  1사용했습니다.
  7. BaseballGame이라는 클래스를 써야한다(main이 여기서 시작)
  8. 게임을 처음실행하면 사용자에게 입력을 받을수 있다.
        콘솔에서 게임숫자를 입력받아야 한다. 1,2,3 하면 1,2,3을 파싱해서 담아야한다.
  9. 매번 게임은 현재 게임횟수를 출력해야한다.(시도횟수와는 다르다)
        현재 첫번째 게임을 하고 있습니다.
        현재 두번째 게임을 하고 있습니다.
  10. 게임은 hit이거나 out이 되야 한게임이 끝이 난다.
        hit나 out이 나오면 게임을 다시 시작한다.
  11. 게임이 종료되면 다시 게임을 시작한다.
  12. 게임중 현재 상태를 요청할수 있다.(현재 상태를 출력해주는 루틴만 있으면 되겠다)
  13. 게임이 기록이 없다면 기록을 둘려줄수없다.





1부터 5까지는 기본 게임 엔진에 해당 되는 부분이라 생각되고, 이부분만 해결되면 나머지는 그냥 조금씩 끼워 맞추면될것같았습니다. 원래 이전에 한번 쫙 만들었는데, 나중에 테스트 구현이고 뭐고 뒤죽박죽되어 포기를 했습니다. 작업 Task관리가안되었고, 실제 구현을 하게 되면 예전 습관대로 그냥 설계를 막하면서 구현을 하다보니 제거해야될 코드가 너무 많이보여 TDD로구현한다는것이 무색하게 되더군요. 그래서 일단 1번 부터 5번까지 단계별로 다시 작업을 시작했습니다. 빨간막대도많이 보였고,처음 클래스나 메소드를 생성할때 Stub구현을 꾸준히 적용했습니다. Stub을 구현하자마자 실제 메소드를구현하였고, 테스트코드를 적기전에는 클래스나 메소드를 설계하지 않았습니다.


개발환경은 Eclipse 3.3을 사용하였고, junit은 4.0을 이용했습니다. java 버전은 1.5 입니다. 따로 ant를 이용해 빌드를 하지는 않았고, eclipse를 이용했습니다.


1.게임숫자는 3자리이다.

당연한 작업이지만, 이전에 작성을 했을때 자리 숫자마다 맴버 변수를 따로 주었습니다. 처음에는 그냥 그러려니 하다가보니자릿수 비교가 많은데 변수가 따로 존재하면 작업하기가 쉽지 않더군요. 그래서 좀 의미있는 설계가 되라 하는 맘에 집어넣었습니다.


테스트코드


  1.     @Test
        public void createNumber() {
            assertEquals(3, new GameNumber(1,2,3).getNumberCount());
        }


실제 GameNumber에 getNumberCount()라는 메소드가 쓰일지는 모르겠지만,  이렇게 하면 number는 Collection이나 Array를 쓰겠지요.

GameNumber

  1. public class GameNumber implements Evaluatable {
        private int[] numbers;   
        public int getNumberCount() {
            return numbers.length;
        }
  2. }

물론 빨간 딱지 붙었지만, 생성자를 추가했습니다.


  1.     private final int numberCount = 3;
  2.     public GameNumber(int number1, int number2, int number3) {
            numbers = new int[numberCount];
            numbers[0] = number1;
            numbers[1] = number2;
            numbers[2] = number3;
        }


게임숫자를 세어 갯수를 확인하는 테스트로 숫자 구조를 설계하는 성과를 내었군요!!!(꿈보다 해몽이 더 좋다고--;)

2.숫자의 범위는 1부터 9까지이다.

테스트

  1.     @Test
        public void checkNumberRange() {
            int number1 = new GameNumber(1,2,3).getNumber1();
            assertTrue(number1 >= 1 && number1 <= 9);
            int number2 = new GameNumber(1,2,3).getNumber2();
            assertTrue(number2 >= 1 && number2 <= 9);
            int number3 = new GameNumber(1,2,3).getNumber3();
            assertTrue(number3 >= 1 && number3 <= 9);
        }

1부터 9까지 확인하는 코드인데 그다지 의미를 못찾았습니다. 지금 생각해보니, 단순 숫자가 가 범위이냐는 범위외에 값이들어갔을때Exception처리를 하는지 여부를 확인하는 코드였다면 더 좋았겠다 싶네요. 그리고GameNumber에서getNumber1()이라는 메소드는 빼고 싶습니다. 실제 구현에서도 쓰지는 않았구요. 단순히 범위 확인을위해 적어버렸는데,테스트 코드 작성을 좀더 생각을 하고 처리하는게 좋겠습니다. 좋은 아이디어 테스트코드가 또 좋은 설계로 갈수있게 해주다는걸알것 같네요.

GameNumber

  1.     public int getNumber1() {
            return numbers[0];
        }

        public int getNumber2() {
            return numbers[1];
        }

        public int getNumber3() {
            return numbers[2];
        }

3.숫자를 비교할수 있어야한다. (같다와 다르다를 알수 있어야한다.)

테스트

  1.     @Test
        public void equalNumber() {
            assertNotSame(new GameNumber(1,2,3), new GameNumber(3,2,1));
            assertEquals(new GameNumber(1,2,3), new GameNumber(1,2,3));
        }


객체의 인스턴스간의 비교를 위해서 작성한 코드입니다. equals메소드와 hashCode를 제 작성하면 유용할것 같았습니다. (실제 유용하지는 않았어요,)

GameNumber

  1.     @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof GameNumber)) {
                return super.equals(obj);
            }

            GameNumber convObj = (GameNumber) obj;

            boolean equal = true;
            for(int idx = 0; idx < numbers.length ; idx++) {
                int convNumber = convObj.numbers[idx];
                if(numbers[idx] != convNumber) {
                    equal = false;
                    break;
                }
            }
           
            return equal;
        }

        @Override
        public int hashCode() {
            int hashNumber = getNumber1() * 100 + getNumber2() * 10 + getNumber3();
            return hashNumber;
        }

number들이 모두 같다면 true를 반환합니다. 점수를 계산할때 어떻게 구현해볼까 하는데 힌트를 줘서 도움은 됐는데, 따로 더쓰일지는모르겠습니다. hashCode도 HashMap에서 key로 파티셔닝을 할때 도움이 될까 싶어서 따로 구현을 했습니다.그렇지만당장 어디다가 쓸지는 모르겠네요.

4.숫자들의 비교결과를 상세히 알수 있어야한다.(strike, ball, hit, out)

비교결과를 표현을 해야하는데, 단순결과가 아니라 객체를 얻어야 겠죠. 객체와의 비교를 해야하는데 비교하여 결과를 내보내는거라다른클래스로 완전히 빼고 싶었습니다. 그런데 Number인스턴스간에 비교를 해야하는데, 다른 클래스로 전적으로 위임이된다면,public 메소드를 이용해서 꺼내 비교랠 해야 되더군요. 좀 아깝다는 생각이 들었습니다. 그건 그렇고 일단 테스크코드나갑니다.

  1.     @Test
        public void evaluateNumber() {
            Evaluation evaluateResult = new GameNumber(1,2,3).evaluate(1, new GameNumber(1,2,4));
            assertNotNull(evaluateResult);
            assertEquals(2, evaluateResult.getStrikeCount());
            assertEquals(0, evaluateResult.getBallCount());
            assertEquals(false, evaluateResult.isHit());
           
            Evaluatable evaluateGameNumber = new EvaluateGameNumber(new GameNumber(1,2,3));
            evaluateResult = evaluateGameNumber.evaluate(3, new GameNumber(1,2,4));
            assertEquals(false, evaluateResult.isOut());
            evaluateResult = evaluateGameNumber.evaluate(3, new GameNumber(1,2,4));
            assertEquals(false, evaluateResult.isOut());
            evaluateResult = evaluateGameNumber.evaluate(3, new GameNumber(1,2,4));
            assertEquals(true, evaluateResult.isOut());
            assertEquals(2, evaluateResult.getStrikeCount());
            assertEquals(0, evaluateResult.getBallCount());
            assertEquals(false, evaluateResult.isHit());
        }
       

EvaluateGameNumber가 값을 비교하는 루틴을 가졌으면 했지만, number를 밖으로 적출!하기 싫었습니다.(현재 코드는 꺼낼수 있지만제거할예정입니다) 그래서 GameNumber에 evaludate하는 기능을 작성했고, 메쏘드는 Interface를 하나만들어EvaluateGameNumber에도 넣어주었습니다. EvaludateGameNumber도 인터페이스가 생기고,기능은GameNumber에 있는 evaluate를 사용합니다. TDD를 통해서 필요할때마다 적용을 했는데, 딱히 무슨페턴이다라고말하기는 좀 그렇네요. Interface도 가지고 실제 기능은 외부에서 제공받은 인스턴스기능을 덮었을뿐인데,나중에찾아봐야겠습니다.

GameNumber

  1.     public Evaluation evaluate(int maxTryCount, GameNumber gameNumber) {
            int strikeCount = 0;
            int ballCount = 0;
            for(int idx = 0; idx < numbers.length ; idx++) {
                int eachIndexNumber = numbers[idx];
                int compareIndexNumber = gameNumber.numbers[idx];
               
                if(eachIndexNumber == compareIndexNumber) {
                    strikeCount++;
                }
               
                for(int idx2 = 0; idx2 < gameNumber.numbers.length ; idx2++) {
                    if(idx == idx2) continue;
                   
                    if(compareIndexNumber == gameNumber.numbers[idx2]) {
                        ballCount++;
                        break;
                    }
                }
            }
            evaluateTryCount++;
            return new Evaluation(strikeCount, ballCount, maxTryCount, evaluateTryCount);
        }

처음에는 strike와 ball, hit만 결과를 내주었는데, out을 구현하기 위해서는 평가횟수와 최대 평가횟수를 알고있어야하더군요. maxTryCount와 evaluateTryCount 변수는 추가로 구현이 된것입니다. 계속 고민이 되는게결과를저장하는 클래스인 Evaluation인데, 실제 비교한 GameNumber는 들어가지 않아서, 좀 고민입니다. 뭐 이런건다른테스트가 튀어나온다면 고쳐질수 있겠네요. 문제점이 보일것 같지만 미리 걱정은 안할랍니다.

5. 특정 시도 횟수 내에 맞출수도 있고, 아웃이 될수도 있다. 다시 게임을 하면 이전에 생성한 숫자는 쓸수 없다.

아웃을 확인할수 있다면 위의 테스트와 중복이 되지만, 컴퓨터가 뽑아낸 숫자들이 중복되면 안된다는 규칙이 있네요. 계속 생성할때마다 다른 GameNumber를 만들면 되겠네요.

  1.     @Test
        public void duplicateCheckingNumber() {
            GenerateGameNumber generateGameNumber = new GenerateGameNumber();
            GameNumber gameNumber1 = generateGameNumber.generate();
            GameNumber gameNumber2 = generateGameNumber.generate();
            GameNumber gameNumber3 = generateGameNumber.generate();
            GameNumber gameNumber4 = generateGameNumber.generate();
           
            System.out.println(generateGameNumber.toString());

            System.out.println(gameNumber1.toString());
            System.out.println(gameNumber2.toString());
            System.out.println(gameNumber3.toString());
            System.out.println(gameNumber4.toString());
            assertNotSame(gameNumber1, gameNumber2);
            assertNotSame(gameNumber2, gameNumber3);
            assertNotSame(gameNumber3, gameNumber4);
        }

생각해보니 (1,2,3) , (2,3,4) 이런식이고, 같은 숫자는 에러가 되야 하는거네요. 형식에 맞지않는 값이나온다면Error뭐 이런걸 추가해야할듯합니다. 이건 나중에. 여하튼 9자리 숫자가 3개 나오는데, 자리수마다 중복되는숫자는없어야합니다. 순열로 보면 중복을 포함하지않는 순열이군요 --; 총갯수는 9*8*7개입니다. 허용갯수보다 더 많은 생성을요구하면 에러를 내야하겠네요. 이전에 구현을 했을때는  중복되는 GameNumber라면 재생성을  요청했는데, 횟수가증가할수록많이 느려졌습니다. 그래서 머리좀 굴렸습니다.

GenerateGameNumber

  1. public class GenerateGameNumber {
        private List<GameNumber> allGameNumbers;
        private List<Integer> generatedGameIndexs;
       
       
        public GenerateGameNumber() {
            allGameNumbers = new ArrayList<GameNumber>();
            generatedGameIndexs = new ArrayList<Integer>();
           
            List<Integer> numbers = new ArrayList<Integer>();
            for(int i = 1 ; i <= 9; i++)
                numbers.add(i);
           
            int number1;
            int number2;
            int number3;
            for(Integer eachNumber1 : numbers) {
                number1 = eachNumber1.intValue();
                for(Integer eachNumber2 : numbers) {
                    number2 = eachNumber2.intValue();
                    if(number1 == number2)
                        continue;
                    for(Integer eachNumber3 : numbers) {
                        number3 = eachNumber3.intValue();
                        if(number1 == number3 || number2 == number3)
                            continue;
                        allGameNumbers.add(new GameNumber(number1, number2, number3));
                    }
                }
            }
        }

        public GameNumber generate() {
            int totalSize = allGameNumbers.size();
            if(totalSize == generatedGameIndexs.size()) {
                throw new NoMoreGenerateGameNumberException();
            }
           
            int randIndex = new Random().nextInt(totalSize - generatedGameIndexs.size() - 1);

            int skipCount = 0;
            for(Integer generatedGameIndex : generatedGameIndexs) {
                if(randIndex <= generatedGameIndex) {
                    skipCount++;
                } else {
                    break;
                }
            }
            randIndex = randIndex + skipCount;
           
            generatedGameIndexs.add(randIndex);
            Collections.sort(generatedGameIndexs);
           
            return allGameNumbers.get(randIndex);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            for(GameNumber eachGameNumber : allGameNumbers) {
                sb.append(eachGameNumber.toString());
                sb.append("\n");
            }
           
            return sb.toString();
        }
    }

GenerateGamenumber가 생성될때 모든 경우의 수의 GameNumber를 만들어 놓고, 랜덤으로 선택을 하고 리턴을 하는데, 이미 선택된GameNumberindex는 skip하는 형식으로 좀 우아하게 구현했습니다.(과연?) TDD와는 무관한 코드니 각자의 방법이 많이 다르겠네요.


일단 기본적인 엔진은 구현했습니다. 좀더 기능적으로 풍부하기위해선 나머지테스트도 모두 수행해야 합니다. 테스트 코드를 보고구현하는건 어렵지 않았습니다. 문제는 적절한 테스트 코드를 만드는게 참어렵더군요. 그리고 테스트 코드를 만드는 순서도 그렇구요. 큰 도메인을 잘게 썰어주는 기술이 가장 중요한것 같습니다.(결국 같은 얘기일수 있네요) 익숙하지 않는 가운데서도 형식은 갖추고 하는듯해 나름희망이 보이네요.


나머지 테스트도 모두 수행해서 마무리 할랍니다. 너무 길어서 한번에 올리지도 못할듯.


소스는 google code에 올렸습니다. http://code.google.com/p/tddbaseball/source/browse/

baseball과 baseball2패키지가 있는데 언급한 코드는 baseball2입니다. baseball은 이전에 삽질했던 코드입니다.

by 소내기 | 2008/08/13 18:17 | TDD | 트랙백 | 핑백(1) | 덧글(2)

트랙백 주소 : http://sonegy.egloos.com/tb/4549566
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at 소내기 블로그 : 초보 개발자.. at 2008/08/14 10:20

... 이전내용 이전에 GameNumber에 대한 값 검사 하는 부분을 추가했습니다. 테스트 내용은 (1,2,10)으로 생성하면 에러를 보내야한다.. (1,2,1)을 생성성해도 마찬가지 에러 ... more

Commented by 쑤짱 at 2008/08/14 09:35
수고 하셨군요^^ 책을 읽진 않았지만 이런거군 하는 생각이 드는군요 ㅋ
Commented by 소내기 at 2008/08/14 10:21
쑤짱/ 우아 댓글 달렸다! 심심하면 책한번 읽어보렴. 내용이 어렵지는 않아.

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶