[TDD, 클린 코드 with Java] 자동차 경주 미션 피드백 : 테스트 코드 작성, 전략패턴, 일급컬렉션
by Hi.Claire🐣 TDD, 클린 코드 with Java 19기 (박재성, 넥스트스텝)
자동차 경주 미션 피드백 : 테스트 코드 작성, 전략패턴, 일급 컬렉션
자동차 경주 미션 3단계 피드백 반영
3단계 - 자동차 경주 요구사항
https://edu.nextstep.camp/s/KIXpLZ6i/ls/3Vlw7LtF
NEXTSTEP
edu.nextstep.camp
코드 리뷰
https://github.com/next-step/java-racingcar/pull/5756
Step3 기능 요구사항 구현 by coongya · Pull Request #5756 · next-step/java-racingcar
안녕하세요. 클래스를 분리하고 테스트케이스를 작성하며 구현하는 것이 아직 어렵고 감이 잡히지 않습니다. 그래서 단위 테스트 없이 일단 기능 요구사항만 만족할 정도로만 구현하여 코드 리
github.com
1. TDD, 테스트 코드 작성
단위 테스트 작성이 어렵다는 질문에 대한 피드백을 주셨다.
우선 TDD를 떠나 테스트코드 작성하는 것부터 익숙해지는 연습을 해야 한다.
기본적으로 객체들의 모든 method는 테스트가 될 수 있어야 한다. (테스트 코드로 작성할 수 있어야 한다.)
만약 테스트가 어렵다면, (테스트 코드로 작성하기가 어렵다면) 객체로 분리가 필요한가? 이 객체가 가지고 있는 역할과 책임이 많은가?에 대해 고민하는 시점이 되면 될 것 같다고 하셨다.
2. 전략 패턴
Car 객체에 대해 두 가지 측면에서 피드백을 주셨다.
Step3 기능 요구사항 구현 by coongya · Pull Request #5756 · next-step/java-racingcar
안녕하세요. 클래스를 분리하고 테스트케이스를 작성하며 구현하는 것이 아직 어렵고 감이 잡히지 않습니다. 그래서 단위 테스트 없이 일단 기능 요구사항만 만족할 정도로만 구현하여 코드 리
github.com
1. 테스트 코드 관점
Car 객체는 지금 잘 움직이는지(move) 그리고 움직였을 때 거리가 원하는 만큼 증가했는지(distance)가 테스트 되어야 한다.
하지만 지금 구조는 테스트가 어렵다.
그 이유는 Car객체의 움직임을 외부에서 제어하기 어려운 구조이기 때문이다.
-> 기능 요구사항 중에 아래 조건을 Car의 move() 메서드 안에 구현했다. 이때문에 move() 메서드를 호출하면 랜덤값에 의해 자동차가 전진할지 멈출지 결정되는 구조였어서 위와 같은 피드백을 주셨다.
- 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우이다.
2. 유지보수 관점
Car를 전진시키는 것도, 멈추는 것도 제어가 쉽지 않은 상황이다.
지금은 경기중에 갑자기 돌발상황이 생겨서 차를 멈추게 한다거나, 일종의 이벤트 성으로 2라운드, 4라운드에는 자동차를 무조건 전진하게 한다 등등의 "자동차 움직임에 대한 요구사항"이 발생할 경우 매번 수정이 일어나야 하는 구조다.
전략 패턴에 관한 다음 내용을 참고해서 이 부분부터 개선을 시작해보자.
전략 패턴이란 같은 기능이지만 서로 다른 전략을 가지는 클래스들을 각각 캡슐화하여 상호교환할 수 있도록 하는 패턴이다.
피드백 주신 내용을 참고해서 move()메서드가 이동전략을 받아서 이동할 수 있을 때 이동하도록 수정했다.
Car.java
package step3;
public class Car {
private int distance;
public void move(MoveStrategy moveStrategy) {
if (moveStrategy.isMovable()) {
distance++;
}
}
public int showDistance() {
return distance;
}
}
MoveStrategy.java
package step3;
@FunctionalInterface
public interface MoveStrategy {
public boolean isMovable();
}
RandomMoveStrategy.java
package step3;
import java.util.Random;
public class RandomMoveStrategy implements MoveStrategy {
private static final Random RANDOM = new Random();
private static final int RANDOM_NUMBER_BOUND = 10;
private static final int MOVE_THRESHOLD = 4;
@Override
public boolean isMovable() {
return getRandomNumber() >= MOVE_THRESHOLD;
}
private int getRandomNumber() {
return RANDOM.nextInt(RANDOM_NUMBER_BOUND);
}
}
Car에 대한 테스트를 수행할 때 "이동 가능한" 조건일 때 원하는 거리만큼 이동하는지, "이동 가능하지 않은" 조건일 때 정지하는지에 대한 테스트가 필요하다.
MoveStrategy 인터페이스는 추상 메서드를 하나만 가지는 함수형 인터페이스(Functional Interface)이다.
(참고) 함수형 인터페이스
Functional Interface란
Java8부터 함수형 프로그래밍을 지원한다. 함수를 일급객체처럼 다룰 수 있게 제공하는 Functional Interface에 대해 알아볼 것이다. Functional Interface란? 단 하나의 추상 메서드를 가지는 인터페이스. - J
tecoble.techcourse.co.kr
따라서 이동 전략이 "이동 가능하다" 또는 "이동 가능하지 않다"라는 결과값을 반환했을 때의 테스트 케이스를 다음과 같은 람다식을 사용해서 작성할 수 있다.
CarTest.java
package step3;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class CarTest {
Car car;
@BeforeEach
void generate_car() {
car = new Car();
}
@Test
void move_when_movable() {
car.move(() -> true);
assertThat(car.showDistance()).isEqualTo(1);
}
@Test
void stop_when_not_movable() {
car.move(() -> false);
assertThat(car.showDistance()).isEqualTo(0);
}
}
(참고) MoveStrategy가 함수형 인터페이스가 아닐 때에는 람다식을 사용할 수 없고 다음과 같이 익명 클래스를 사용해서 작성해야 한다.
car.move(new MoveStrategy() {
@Override
public boolean isMovable() {
return true;
}
});
3. 일급 컬렉션
Game 객체에서 경주에 참여하는 자동차 목록이나 게임 결과를 배열로 담고 있는데 이 부분에 대한 피드백도 받았다.
일급 컬렉션을 사용하면 객체들 간의 역할과 구조 관점에서 더 유연한 구조를 만들 수 있다고 개선 요청을 해주셨다.
(참고) 일급 컬렉션(First Class Collection)
일급 컬렉션 (First Class Collection)의 소개와 써야할 이유
최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코
jojoldu.tistory.com
일급 컬렉션은 Collection을 wrapping하면서 그 외 다른 멤버 변수가 없는 클래스이다.
일급 컬렉션의 장점
- 비즈니스에 종속적인 자료구조를 만들 수 있다.
- Collection의 불변성을 보장할 수 있다.
- 상태와 행위를 한 곳에서 관리할 수 있다.
- 이름이 있는 컬렉션을 만들 수 있다.
1. Cars
경주에 참여하는 자동차 목록인 List<Car>를 wrapping하는 일급 컬렉션 Cars를 만들고, Car를 관리하는 역할과 책임을 부여했다.
2. RoundResult
게임 한 라운드 결과인 List<Integer>를 wrapping하는 일급 컬렉션 RoundResult를 만들었다.
3. GameResults
게임 전체 결과인 List<RoundResult>를 wrapping하는 일급 컬렉션 GameResults를 만들었다.
10/9 피드백 받아서 수정 예정
일급 컬렉션을 적용하면서 좋아진 점
먼저 Service 관점에서 좋아졌다.
즉 이 객체를 사용하는 다른 객체들에게List<RoundResult> gameResult를 사용하는 다양한 method를 제공함으로써 복잡한 자료구조까지 알 필요 없이 사용 및 제어가 가능하다.
같이 협업하고 있는 동료 개발자들은 이제는 GameResults만 알면 경기 결과도 저장할 수 있고, 가져올 수도 있다.
일급 컬렉션을 사용할 때 주의할 점
다만 일급컬렉션에서 객체를 반환시키는 경우 (여기에서는 gameResult 값) 불변객체로 반환해야 한다.
왜냐하면 데이터의 위변조 위험이 있기 때문이다.
지금 GameResults에서 가장 중요한 것은 gameResult, 즉 경기 결과이다.
이 값이 외부에 제공될 때는 read-only형태로만 가능해야 하며 혹시 변경이 된다고 해도 GameResults가 제공하는 method에 의해서만 변경 되도록 해야 한다.
그래야 중요한 객체가 가진 데이터가 보호되고 일관성이 확보 될 수 있다.
Cars도 마찬가지로 Cars를 통해서 Car들을 이동시키고, 조회 시에는 read-only로 해야 외부에서 수정이 불가능하다.
'☕️ Java > TDD, 클린 코드 with Java' 카테고리의 다른 글
[TDD, 클린 코드 with Java] 자바 스트림(Stream), 경계값 테스트, 클래스 분리, 원시값과 문자열 포장 (1) | 2024.10.23 |
---|---|
[TDD, 클린 코드 with Java] TDD 연습, Test Fixture, 생성자 활용, Getter를 쓰지 말고 객체에게 메시지를 보내라, 테스트 가능한 구조로 개선 (1) | 2024.10.22 |
[TDD, 클린 코드 with Java] 자동차 경주 미션 피드백 : 예외 처리, 매직넘버 치환 (0) | 2024.10.05 |
[TDD, 클린 코드 with Java] 미션과 상관없는 이야기 (2) | 2024.10.01 |
[TDD, 클린 코드 with Java] 과정 소개, 학습 테스트 구현, 메서드 분리 (3) | 2024.09.26 |
블로그의 정보
Claire's Study Note
Hi.Claire