[우테코 6기] 프리코스 2주차 미션 - 2일차
우아한테크코스 6기 백엔드 프리코스 - 자동차 경주 게임
테스트 목록
앞으로 구현해야 할 테스트는 6개이다.
m2t01 - 자동차 이름은 5자 이하의 문자열이며 쉼표로 구분하여 입력받는다.
m2t02 - 이동 횟수는 숫자만을 입력받는다.
m2st01 - 정규표현식 학습 테스트
m2t03 - 이외의 값을 입력할 경우 IllegalArgumentException
을 발생시키며 프로그램은 종료된다.
m2t04 - 우승한 자동차의 이름을 모두 출력한다. 한 개 이상인 경우 쉼표로 구분하여 출력한다.
m2t05 - 지정된 범위의(0부터 9까지) 난수를 생성할 수 있어야 한다.
m2t06 - 1회의 이동에서 생성된 난수가 4 이상인 경우에만 전진한다.
m2t07 - 입력받은 횟수 만큼의 전진이 끝난 뒤 가장 앞에 있는 자동차를 선택한다.
구현
m2t06
모든 자동차에 대해 1회 이동을 실시한다. 이 때 생성된 난수가 4 이상인 경우에만 전진한다. 이 테스트를 작성하기 위해서는 일단 자동차, 이동, 난수가 무엇인지 정의해야 한다.
난수는 앞의 m2t05 테스트에서 구현했으니 자동차와 이동에 대해 구현하면 된다.
테스트를 통과하기 위해 아래와 같이 Car
클래스를 구현했다.
테스트를 약간 수정했다. 구현하다 보니 Car
객체가 난수를 생성 함수를 호출하기 위해 GameRule
에 대해 알아야 했다.
m2t07
n회 전진 후 선두의 차량을 모두 구한다. 이동거리가 가장 긴 자동차들의 이름을 반환하는 클래스가 필요하다. 이 클래스의 이름은 Referee
이다.
위의 테스트를 통과시키기 위해 Referee
클래스를 아래와 같이 작성했다. 이 클래스의 역할은 우승자들을 결정하는 것이다.
1번 전진할 때 이름의 길이만큼 전진하도록 Mock 객체를 설정하여 이름이 5자인 자동차들이 우승하도록 테스트를 작성했다.
모든 자동차가 동일한 거리를 전진하도록 Mock 객체를 설정하여 모든 자동차가 우승하도록 작성했다.
테스트 코드의 중복이 상당하다. ☺️
테스트코드 중복 제거
앞에서 작성한 m2t07 테스트의 중복을 제거해보자. 중복이 발생하는 부분은 다음과 같다.
GameRule
객체 생성Referee
객체 생성- 경주에 참가할
Car
객체 생성 RandomNumberGenerator
mock 객체 생성 & 해제
중복되는 작업을 @BeforeEach
@AfterEach
메서드로 묶어 주었다.
Mock 객체는 모든 테스트를 실행하는 동안 변하지 않기 때문에 굳이 각각의 테스트마다 새로 만들고 해제하기를 반복할 필요가 없다. 하지만 Car
객체들과 Referee
객체의 상태는 각각의 테스트에서 변하기 때문에 매번 새로운 객체가 필요하다.
불필요한 초기화와 해제를 줄였더니 예상대로 테스트코드의 성능이 개선되었다.
아쉬운 점
Car
클래스의 moveForward
메서드의 구체적인 동작 방식을 숨기지 못했다. RandomNumberGenerator.pickRandomNumber
메서드의 반환값을 직접 설정해주고 있다.
m2t01
길이가 1이상, 5이하의 자동차 이름을 ,로 구분된 문자열 형태로 입력받는다.
자동차 이름을 표현하는 클래스와 입력을 받는 클래스가 필요하다.
아래의 테스트는 실패한다. actualNames
와 expectedNames
속에는 같은 자동차 이름들이 들어가 있지만 서로 다른 List
인스턴스이기 때문이다. 즉 동등성에 대한 적절한 오버라이딩이 필요하다. List
의 동등성을 오버라이딩 할 수는 없다. CarName
을 감싸는 일급 컬렉션이 필요하다.
그런데 테스트를 통과하기 위해 모델을 변경하는게 맞는건지는 모르겠다… 라고 생각했는데 애초에 TDD가 테스트를 통과하기 위한 코드를 작성하면서 프로그램을 완성해가는데 안 될 것도 없다.
List<CarName>
를 감싸는 CarRecord
클래스를 도입했다.
대규모 리팩토링
CarRecord
일급 컬렉션을 도입하는 과정에서 대규모 리팩토링이 있었다…
리팩토링을 하면서 이런 일(대규모 리팩토링)을 사전에 방지할 수는 없는 걸까 고민했다.
문제가 된 건 대부분 위와 같은 형태였다. 위와 같은 형태란, 클라이언트 코드가 Car
라는 구체적인 구현체에 의존하고 있는 것이다. 그렇기 때문에 Car
클래스가 수정되면 클라이언트 코드들이 민감하게 반응한다. 클라이언트 코드를 구체적인 클래스로부터 분리하여 클래스의 수정에 전체 코드가 둔감해지도록 만들자.
후…🫠
많은 변화가 있었다. 하나씩 차근차근 되짚어보자.
CarFactory
Car
객체 생성에 팩토리 패턴을 적용했다. Car
객체 생성을 팩토리 메서드로 감싸 객체를 생성하는 구체적인 과정을 클라이언트 코드로부터 감췄다.
Car
객체의 생성자에는 CarName
객체를 전달해야 하지만 팩토리 메서드에는 String
인스턴스를 전달해도 객체가 정상적으로 생성된다.
RefereeFactory
Referee
객체 생성을 돕는다.
CarRecord
Car
컬렉션을 감싸는 일급 컬렉션이다. 내부적으로 List<Car>
를 저장하고 있다. Car
객체 목록을 추상화한 것이며 Car
는 경주에 참여한 자동차들의 상태를 추상화 한 것이다.
Car
들에 대한 연산(상태 변경, 조회)을 제공한다.
아래는 CarRecord
객체를 생성하는 팩토리 클래스이다.
리팩토링의 효과
m2t07 테스트 코드가 아래와 같이 간결해졌다. 그리고 모든 API는 통제 가능한(내가 작성한) 코드이다. 외부 API 변경에 대한 충격을 최소화 했다.
여러 군데에서 중복되던 mocked.when ...
부분을 setDistance
메서드로 묶고 CarRecord
객체로 모든 Car
객체에 한 번에 접근할 수 있게 되었기 때문이다.
모든 Car
객체가 CarRecord
에 숨겨져 각각에 대해 움직일 거리를 지정해 줄 수는 없었지만 아래와 같은 방법으로 테스트를 구현할 수 있었다!
테스트 로직
@BeforeEach
에서 초기화된Car
객체들을 3만큼laps
회 이동(4 미만이기 때문에 전진하지 않음)- 5만큼 이동한
winner
객체를carRecord
에 추가 carRecord
에서 우승자를 선택
m2t01
그럼 이제 m2t01 테스트를 통과하기 위해 CarRecord
클래스를 구현해보자.
테스트 코드를 약간 수정했다. 중요한 점은 expectedNames
와 actualNames
의 생성 로직이 다르다는 점이다. expectedNames
는 이미 검증된 방식으로, actualNames
는 테스트하고자 하는 로직으로 생성된 객체이다.
m2st01
m2t02를 진행하기 전에 정규 표현식을 학습하는 테스트를 작성했다.
m2t02
정상적인 입력에 대한 테스트이다.
사용자의 입력을 NumberOfRepetition
클래스로 추상화했다.
아래는 InputPrompt
클래스의 사용자 입력을 받는 메서드이다.
m2t03
올바르지 않은 입력값에 대해 IllegalArgumentException
예외를 던질 수 있어야 한다.
마무리
9시간 16분
Comments