우아한테크코스 6기 백엔드 프리코스 - 1주차 미션

IDE 설정

우테코 코드 스타일을 자동으로 적용하는 방법을 참고하여 저장할때마다 코드를 포매팅하도록 설정했다.

TDD? OOP? Now or never!

지금까지 개인, 팀프로젝트를 진행할 때에는 코드의 품질, 유지보수성에는 크게 신경 쓰지 않았다. 항상 “나중에 공부해야지” 라며 미뤄왔다.
프로젝트를 진행해도 성장한다는 느낌이 전혀 들지 않았다. 매번 그냥 돌아가기만 하는 1회성의 코드를 작성했다. 버그가 발생하면 많은 광범위한 코드 수정이 필요했고 프로그램에 대한 주도권은 개발자가 아닌 코드에 있었다.
“나중” 이라는 것은 없다. 하고 싶다면 지금 당장 해야 한다.
책장에 쳐박혀 있던 TDD를 꺼냈다.

It’s now or never.
지금이 아니면 안 돼.
- Vinny Daniel (영화 Big Short)

TDD 규칙

오직 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
중복을 제거한다.

조금 더 구체적으로는 아래의 리듬이 반복되며 개발이 진행된다.

TDD 리듬

  1. 빠르게 테스트 하나를 추가한다.
  2. 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인한다.
  3. 코드를 조금 바꾼다. (실패한 테스트를 통과시키기 위해)
  4. 모든 테스트를 실행하고 전부 성공하는지 확인한다.
  5. 리팩토링을 통해 중복을 제거한다.

구현 시작

TDD와 OOP 두 마리 토끼를 한 번에 잡는 것이 베스트겠지만 멀리 가려면 천천히 가야한다!

TDD부터 시작한다.

TODO list

요구사항을 만족시키려면 어떤 객체 테스트가 필요할까?
(객체가 아니라 테스트를 먼저 만들어야 한다!)

m1t01 - 1부터 9까지의 숫자 중 서로 다른 세 자리 숫자를 생성할 수 있어야 한다.
m1t02 - 서로 다른 세 자리 수가 아닌 사용자의 입력에 대해서는 IllegalArgumentException을 발생시킨다.
m1t03 - IllegalArgumentException이 발생하면 애플리케이션이 종료되어야 한다.
m1t04 - n개의 같은 수가 같은 자리에 있는 경우 n개의 스트라이크를 결과로 얻을수 있어야 한다. (n <= 3)
m1t05 - n개의 같은 수가 다른 자리에 있는 경우 n개의 볼을 결과로 얻을수 있어야 한다. (n <= 3)
m1t06 - 같은 수가 0개인 경우 낫싱이라는 결과를 얻을수 있어야 한다.
m1t07 - 사용자의 유효한 입력에 대한 적절한 결과를 출력할 수 있어야 한다.
m1t08 - 3스트라이크인 경우 게임이 종료된다.(애플리케이션은 종료되지 않는다.)
m1t09 - 게임이 종료된 뒤 1을 입력받으면 새로운 게임이 진행된다.
m1t10- 게임이 종료된 뒤 2를 입력받으면 애플리케이션이 종료된다.
m1t11 - m1t01을 연속해서 여러 번 실행해도 유효한 세 개의 숫자를 생성해야 한다.

각 오퍼레이션의 인터페이스에 대해 생각하며 테스트를 하나씩 작성해보자.

그 전에 camp.nextstep.edu.missionutils 라이브러리에 대한 학습 테스트를 작성해보자.

학습테스트

Randoms.pickNumberInRange 메서드의 사용방법을 익히고 변경을 감지할 수 있게 되었다.

public class createRandomNumberTest {

    @Test
    void 랜덤으로_숫자_세_개_생성() {
        // given
        List<Integer> computer = new ArrayList<>();
        // when
        while (computer.size() < 3) {
            int randomNumber = Randoms.pickNumberInRange(1, 9);
            if (!computer.contains(randomNumber)) {
                computer.add(randomNumber);
            }
        }
        // then
        Assertions.assertTrue(isThreeDifferentNumbers(computer));
    }

    boolean isThreeDifferentNumbers(List<Integer> numbers) {
        int[] countOf = new int[10];
        for (Integer n: numbers) {
            countOf[n] += 1;
            if (countOf[n] > 1)
                return false;
        }
        return true;
    }
}

Console.readLine 메서드의 변화를 자동화된 테스트로 감지할 수 있게 되었다.


public class readLineTest {

    @Test
    void 임의의_값을_입력받는_경우() {
        // given
        String input = "hello world 12345 6789";
        // when
        writeToStdin(input);
        // then
        Assertions.assertEquals(input, Console.readLine());
    }

    void writeToStdin(String input) {
        try (InputStream in = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))) {
            System.setIn(in);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

m1t01

아래의 테스트는 컴파일조차 되지 않는다.
(InvalidateNumberException이 아니라 InvalidNumberException이다.)

ComputerPlayer 테스트

이제 테스트가 통과할 정도의 코드만 작성한다.

ComputerPlayer 첫 번째 구현

m1t01 통과

구현하고 책을 다시 보니 저자는 진짜 컴파일만 되도록 메서드는 테스트에서 요구하는 고정된 결과값을 반환하고 있었다…😅 이렇게까지 한다고??

m1t11

m1t01이 무사히 통과인줄 알았는데 `pickNumberInRange`를 여러 번 실행하는 경우 중복된 숫자를 생성하는 문제가 있음을 알게 되었다. 뿌듯해서 위의 테스트를 여러 번 실행하다가 발견했다.

고반복 테스트

테스트를 통과하도록 createNumber메서드를 수정했다.

고반복 테스트를 통과하는 코드

고반복 테스트 통과

생각해보니 애초에 Randoms.pickNumberInRange 메서드를 독립적으로 세 번 실행하는데 결과가 모두 다를 거라고 기대한 내가 바보였다. 😅
자동화된 테스트는 바보라도 버그를 발견하게 해준다…

m1t04

스트라이크 개수를 판정하는 기능에 대한 테스트이다.
스트라이크 개수가 0, 1, 2, 3개인 상황에 대해 테스트한다.

m1t04

아래는 이 테스트를 통과하기 위해 작성한 메서드이다.

m1t04을 통과하기 위한 코드 1

테스트가 두 개 이상 쌓이니 슬슬 중복되는 코드(빨간색 네모)가 나타나기 시작했다.

ComputerPlayer가 기억하고 있는 숫자와 사용자의 추측을 비교하는 모든 장소에서 특정 자료구조(int[])에 종속적인 코드가 필요하다. 뿐만 아니라 ComputerPlayer 클래스의 책임이 너무 많다.
임의의 세 자리 숫자를 생성하고, 숫자가 세 자리임을 기억하고, 사용자의 추측과 비교하여 strike, ball 개수를 센다.

리팩토링

클래스의 책임을 한 개로 줄여 보자. 클래스의 책임을 줄이면 작은 클래스 여러 개가 나온다.

  • ComputerPlayer - 자신의 숫자를 관리한다.
  • BaseBallNumber - 게임에서 사용하는 숫자에 대한 연산을 제공하는 일급 컬렉션이다.
  • RandomNumberGenerator - 난수를 제공한다.
  • GameRule - 게임 룰에 대한 정보를 저장한다.

기존의 ComputerPlayer 클래스에서 책임을 조금씩 분리해보자.

몇 시간 뒤...

이렇게 하는거 맞나??라는 생각이 들 정도로 오래 걸렸다… 그리고 테스트도 많이 수정됐다…

일단 ComputerPlayer 클래스가 엄청 단순해졌다.

리팩토링된 ComputerPlayer

GameRule 클래스. 게임 규칙에 대한 정보를 제공한다.

GameRule

RandomNumberGenerator 클래스. 전달받은 게임 규칙을 토대로 적절한 개수의 난수를 제공한다. 난수는 Randoms API를 사용한다.

RandomNumberGenerator

BaseBallNumber 클래스. 두 명의 플레이어가 주고 받는 숫자를 추상화 한 클래스이다.
설정되는 값에 대한 유효성 검사동일한 클래스 사이의 연산을 담당한다.
두 가지 책임을 담당하고 있다. 더 작은 클래스로 분리할 여지가 남아 있다. 🤔
유효성 검사를 어디로 분리할지 잘 모르겠어서 일단 그대로 뒀다.

BaseBallNumber

여러 가지 일을 하던 클래스를 작은 클래스로 분리했더니 테스트 작성하기가 편해졌다. private으로 가려져있던 영역에 접근할 수 있게 되었기 때문이다.

m1t05

볼 개수를 판정하는 테스트

m1t05

countStrike 메서드와 구조가 비슷하다. 람다 표현식을 사용하면 구조적 중복을 없앨 수 있을 것 같다…

countBall

1일차 마무리

오늘은 여기까지…

구현 시간

reference

Comments