프리코스 2주차 미션이 시작됐다.
2번째 미션은 자동차 경주 미션이었다
https://github.com/woowacourse-precourse/java-racingcar-7
이번에도 역시 설계부터 진행하고 테스트를 먼저 짜 놓고 개발을 시작했다
그리고 저번 1주차 미션에서 코드리뷰 받은것을 바탕으로 개선해야할 점을 정리해봤다
- 매직넘버 상수화
- 테스트 클래스 분리
- 의미없는 멤버 변수 삭제
- 변수면 자료형 명시 X
- 메서드명 의미 확실하게
- 어떤 테스트인지 확실하게 명시
- 의미있는 개행
대략적으로 이런 개선점이 나왔는데 이번 미션을 구현하면서 지켜야겠다는 생각을 갖고 개발을 시작하게 되었다!
기본적인 요구사항은 이랬는데 클래스 분리를 위해 위의 기능 요구 사항을 보고 간단하게 역할을 정리해봤다.
- 자동차
- [ ] 모든 자동차는 위치를 가진다
- [ ] 4 이상일 경우 전진할 수 있다
- [ ] 3 이하일 경우 멈춰있는다
- [ ] 이름은 5자 이하만 가능하다
- [ ] 이름이 5자 초과일 경우 예외
- [ ] 이름이 공백인 경우 예외
- 자동차 경주
- [ ] 횟수가 주어진다
- [ ] n대의 자동차가 경주를 한다
- [ ] 매 차수마다 자동차의 위치를 알아야 한다
- 자동차 공장
- [ ] 자동차 이름 리스트를 가진다
- [ ] 자동차 이름으로 자동차 객체 생성
- 랜덤 숫자 생성기
- [ ] 0에서 9 사이의 무작위 값이 생성된다
- 승자 판별
- [ ] 자동차 리스트를 가진다
- [ ] 자동차들의 위치를 비교하여 우승자를 가린다
- [ ] 우승자는 한 명 이상일 수 있다
- 입력 숫자 검증
- [ ] 숫자가 아닌 경우 예외
- [ ] 자연수가 아닌 경우 예외
- 입력
- [ ] 경주할 자동차 이름이 `,`로 구분되어 입력된다
- [ ] 구분자가 `,`이 아닐경우 예외
- [ ] 시도할 횟수를 입력 받는다
- 출력
- [ ] 차수별로 실행 결과를 출력한다
- [ ] 우승자를 출력한다
- [ ] 단독 우승자인 경우
- [ ] 공동 우승자인 경우
초기의 설계는 이렇다
개발을 하면서 조금씩 변경된 부분이 있긴 하지만 초기 클래스들은 거의 다 가져간 것 같다
그리고 흐름은 이렇다
위의 설계에서 의문이 생길수도 있는 부분은 아마 랜덤 숫자 생성기
일 것 같다
이 랜덤 숫자 생성기
라는 것을 생각하게 된 이유는 Random.pickNumberInRange(0, 9)
때문이다!
이에 대한 자세한 내용은 뒤에 서술합니다
이번주에도 역시 TDD(Test Driven Development)
를 위해 구현전에 다양한 테스트 케이스들도 생각해도고 테스트코드를 먼저 작성해봤다!
- 테스트
- [ ] 자동차 테스트
- [ ] 4이상일 경우 전진 확인
- [ ] 3이하일 경우 정지 확인
- [ ] 이름이 5자 초과일 경우 예외
- [ ] 이름에 특수문자가 들어갈 경우 예외
- [ ] 우승자 테스트
- [ ] 단독 우승자일 경우 확인
- [ ] 공동 우승자일 경우 확인
- [ ] 입력 예외 테스트
- [ ] 구분자가 `,` 가 아닌 경우 예외
- [ ] 시도할 횟수가 음수일 경우 예외
- [ ] 시도할 횟수가 숫자가 아닐 경우 예외
처음에 생각한 테스트 케이스들은 이렇고, 지금은 더 떠오른 케이스들이 있어서 그에 대한 테스트들을 좀 더 만들었다
TDD
를 하려면 먼저 통과하지 않는 테스트들을 만들어야 한다.
지난 미션에서 테스트의 클래스를 분리해보는게 어떻냐는 리뷰를 받아서 한번 기능?에 따라 클래스를 분리해봤다.
이렇게 분리해봤는데 나름 괜찮았던 것 같다
그리고 단위 테스트
를 시도해봤는데.... 완벽하게 하지는 못하고, Car
클래스에 대해서만 시도를 했다!
@Test
void 전진_테스트() {
assertSimpleTest(() -> {
Car car = new Car("junki", new Num4Generator());
car.move();
assertThat(car.getPosition()).isEqualTo(1);
});
}
@Test
void 정지_테스트() {
assertSimpleTest(() -> {
Car car = new Car("junki", new Num0Generator());
car.move();
assertThat(car.getPosition()).isZero();
});
}
@Test
void 이름에_특수문자_예외_테스트() {
assertThatThrownBy(() -> {
Car car = new Car("jk@", new Num0Generator());
}).isInstanceOf(IllegalArgumentException.class);
}
@Test
void 이름_5글자_초과_예외_테스트() {
assertThatThrownBy(() -> {
Car car = new Car("junkiHeo", new Num0Generator());
}).isInstanceOf(IllegalArgumentException.class);
}
이런식으로 테스트 코드를 구성해봤는데 처음에는 Car
클래스조차 없고 진짜 테스트 코드만 있어서 아래 사진처럼 돌아가지도 않는 상황을 볼 수 있다
Car
클래스에 대한 단위 테스트 때문에 NumberGenerator
를 만들었는데 이것도 뒤에 서술합니다
개발을 하며 미리 만들어놓은 테스트들이 통과하는걸 보면 퀘스트를 깨는 것 같고 개발이 좀 더 재밌어지는 것 같다
구현전 사진(전부 실패)에서 마지막에 코드를 다 짜고 테스트를 돌렸을 때 성공 표시가 전부 떴을 때의 쾌감은 잊지 못한다
커밋 기록은 이렇다!
아래에서 위로 보면 된다
이번 구현은 중간에 밥 먹고 온 시간 빼면 약 4시간 정도 걸린 것 같다
1주차 회고에도 썼듯이 나의 프리코스 목표는 깔끔한 코드
, TDD
, 짧은 시간 내에 개발
이었다.
그래서 4~5시간을 잡고 최소한의 요구사항을 만족하는 코드를 만들고 이후에 리팩토링을 하는 방법으로 진행하고 있는데 만족스러운 것 같다. 시험 보는 느낌으로 진행하니까 집중력도 올라가고 재미있다! 다른 분들도 이렇게 한 번 해보세요
암튼 이번에도 코드의 구조를 많이 신경썼다. 구현 상 어려운 부분은 딱히 없었다.
아 이제 위에 나온 NumberGenerator
를 만든 이유를 말해봐야겠다
전략패턴
이란 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 이다.
이번 미션에서는 Random.pickNumberInRange(0,9)
를 통해서 랜덤한 숫자를 생성하는 방식을 사용한다.
그래서 이를 테스트 하기 위해서는 컴퓨터와의 원만한 합의가 필요하다. 하지만 우리의 컴퓨터들은 합의를 해주지 않는다...
Car
클래스의 동작에 대한 테스트가 필요한 상황이라고 생각해보자. 나의 코드를 예시로 들어보자면, Car
객체가 움직이기 위해서는 0에서 9 사이의 수가 필요하다. 이 때 만약 Car
클래스 내에 Random.pickNumberInRange(0,9)
를 넣어놓는다면 내가 원하는대로 자동차의 동작을 제어할 수 없게 된다
내가 짠 코드가 나의 통제를 벗어나는 일이 발생하는 것이다!
앞으로 한 칸 움직이는 동작을 테스트 하고 싶어 car.move()
를 실행시키면 랜덤한 값으로 인하여 항상 같은 결과가 나오지 않는다.
이를 막고자 NumberGenerator
라는 클래스를 사용하여 구현하게 되었다. 해당 클래스를 통하여 내가 원하는 숫자를 전달할 수 있도록 구성해주었다.
public class Car {
private final String name;
private int position = 0;
private final NumberGenerator numberGenerator;
public Car(String name, NumberGenerator numberGenerator) {
this.name = new CarName(name).getName();
this.numberGenerator = numberGenerator;
}
public void move() {
if (numberGenerator.generateNum() >= 4) {
position += 1;
}
}
public int getPosition() {
return position;
}
public String getName() {
return name;
}
}
이렇게 해주면 NumberGenerator
를 받아와서 원하는 숫자를 생성해줄 수 있다!
가장 상위에 존재하는 NumberGenerator
라는 인터페이스를 만들어주고,
public interface NumberGenerator {
int generateNum();
}
위의 인터페이스를 구현하는 하위 클래스를 만들어준다
아래의 RandomNumberGenerator
는 실제 로직에서 사용되는 랜덤한 숫자를 생성해주는 클래스이다.
public class RandomNumberGenerator implements NumberGenerator {
@Override
public int generateNum() {
return Randoms.pickNumberInRange(0, 9);
}
}
그리고 아래의 두 클래스는 단위 테스트를 위한 항상 동일한 값이 나오는 NumberGenerator
들이다.
public class Num0Generator implements NumberGenerator {
@Override
public int generateNum() {
return 0;
}
}
public class Num4Generator implements NumberGenerator {
@Override
public int generateNum() {
return 4;
}
}
이런식으로 구현하면
void 전진_테스트() {
assertSimpleTest(() -> {
Car car = new Car("junki", new Num4Generator());
car.move();
assertThat(car.getPosition()).isEqualTo(1);
});
}
@Test
void 정지_테스트() {
assertSimpleTest(() -> {
Car car = new Car("junki", new Num0Generator());
car.move();
assertThat(car.getPosition()).isZero();
});
}
이렇게 원하는 Car
의 동작을 테스트 할 수 있다
위의 방식을 전략 패턴
이라고 한다.
이렇게 원하는 내가 제어할 수 있는 클래스를 만들고 또 하나의 고민이 생겼다
바로 자동차 이름
에 대한 검증이었다. 저번 주차에도 검증에 대한 고민을 많이 했는데 이번에도 역시 검증이었다.
기존에는 자동차 이름
을 Car
클래스 내부에서 String
값으로 가지고 있고 검증도 Car
클래스 내부에서 진행을 해줬다. 그렇게 되니 Car
클래스의 코드가 지저분해지고 책임이 늘어난다는 생각이 들었다
public class Car {
private final String carName;
private int position = 0;
private final NumberGenerator numberGenerator;
private static final int NAME_LIMIT_LENGTH = 5;
private static final String engilshRegex = "[^a-zA-Z]";
public Car(String name, NumberGenerator numberGenerator) {
this.carName = name;
this.numberGenerator = numberGenerator;
}
public void move() {
if (numberGenerator.generateNum() >= 4) {
position += 1;
}
}
private void validateName() {
validateNameSize();
validateNameNonEnglish();
}
private void validateNameSize() {
if (carName.length() > NAME_LIMIT_LENGTH) {
throw new IllegalArgumentException("자동차의 이름은 5글자를 초과할 수 없습니다!");
}
}
private void validateNameNonEnglish() {
Pattern pattern = Pattern.compile(engilshRegex);
Matcher matcher = pattern.matcher(carName);
if (matcher.find()) {
throw new IllegalArgumentException("이름에는 영어를 제외한 다른 문자가 들어갈 수 없습니다!");
}
}
public int getPosition() {
return position;
}
public String getName() {
return carName;
}
}
자동차 이름
에 대한 검증도 물론 Car
클래스의 책임이라고 생각할 수 있지만, 이름에 대한 검증을 넣으면 코드가 지저분해지고, 뭔가 Car
클래스에는 자동차의 움직임에 대한 역할만 부여하고 싶었다. 그래서 CarName
이라는 클래스를 추가로 만들어서 검증하는 방식으로 진행하게 되었다.
Car
과 CarName
로 구분한 코드는 이렇다
public class Car {
private final CarName carName;
private int position = 0;
private final NumberGenerator numberGenerator;
public Car(String name, NumberGenerator numberGenerator) {
this.carName = new CarName(name);
this.numberGenerator = numberGenerator;
}
public void move() {
if (numberGenerator.generateNum() >= 4) {
position += 1;
}
}
public int getPosition() {
return position;
}
public String getName() {
return carName.getName();
}
}
public class CarName {
private final String name;
private static final int NAME_LIMIT_LENGTH = 5;
private static final String engilshRegex = "[^a-zA-Z]";
public CarName(String name) {
this.name = name;
validateName();
}
private void validateName() {
validateNameSize();
validateNameNonEnglish();
}
private void validateNameSize() {
if (name.length() > NAME_LIMIT_LENGTH) {
throw new IllegalArgumentException("자동차의 이름은 5글자를 초과할 수 없습니다!");
}
}
private void validateNameNonEnglish() {
Pattern pattern = Pattern.compile(engilshRegex);
Matcher matcher = pattern.matcher(name);
if (matcher.find()) {
throw new IllegalArgumentException("이름에는 영어를 제외한 다른 문자가 들어갈 수 없습니다!");
}
}
public String getName() {
return name;
}
}
이렇게 클래스를 구분하고 CarName
에 대한 검증은 해당 클래스 내에서 진행하고, Car
의 멤버 변수로 CarName
을 가지게 되니 코드가 훨씬 깔끔해지고 역할이 분리됐다.
이 과정에서 고민이 든 부분이 하나 있었다.
바로 일급 객체
에 대한 고민이었다! CarName
으로 분리하려고 결정을 짓고 비슷하게 코드를 짠사람들이 있는지 검색을 해보다가 일급 객체
라는 키워드를 알게 되었다.
내가 이해한 일급 객체
의 의미는 이렇다
일급 객체
: 하나의 자료형을 감싸면서 그 외의멤버 변수
가 없는 상태
이렇게 이해를 하니 CarName
에서 걸리는 부분이 있었다
private final String name;
private final int NAME_LIMIT_LENGTH = 5;
private final String engilshRegex = "[^a-zA-Z]";
바로 이 부분이었다.
String name
을 감싸기 위해서 해당값을 멤버변수로 갖는 클래스를 만든건데 다른 상수들이 있으니 이 클래스는 일급 객체
에 해당하지 않는가? 라는 의문이 들었다
다행히도 이 의문은 문제를 다시 곱씹어보니 해결됐다!
멤버변수
와 상수
의 차이는 뭘까?
멤버 변수
: 클래스 내부에서 가지고 있는 변수상수
: 계속 사용하는 고정적인 변수
이렇게 생각하니 위의 코드에서 잘못된 점을 발견했다
바로 static
을 안붙인점... 계속해서 사용되는 변수에 대해서 객체가 생성될 때마다 할당해줄 필요가 없어서 static
을 붙여주는게 좋다.
이렇게 되면 일급 객체
에 대한 의구심이 해결됐다!
멤버 변수
는 하나니까 일급 객체
에 대해서는 만족한다!!
그렇다면 static
은 뭘까!?
다시 정확하게 알아보고 싶어서 한 번 찾아봤다
static
키워드를 통해 생성된 것들은Heap
영역이 아닌static
영역에 할당된다
static
영역에 할당된 메모리는 모든 객체가 공유하여 하나의 멤버를 어디서든지 참조 가능!
이렇게 돼서 상수는 static
으로 선언하고 사용하는것이다
이렇게 이번 구현 과정에서의 가장 큰 고민이 해결 됐다!!
이렇게 2주차 미션이 끝이 났다
1주차 미션의 코드 리뷰 과정에서 얻어가는 것이 많았는데 이번 코드리뷰 과정에서도 많은 것들을 얻었으면 좋겠다
확실히 프리코스를 진행하면서 많은 것들을 얻어가는것 같다. 혼자서만 개발을 하다보면 피드백을 받는일이 많지 않고 개발을 하는 방식이 고착화되기 쉬운데 다른 사람들과의 리뷰를 통해 많이 성장하는 것 같다.
그리고 여전히 검증에 대한 클래스를 만드는 것이 객체지향에 적합할까? 라는 의문이 있는데 이건 다른 사람들이랑 한 번 이야기해보고 싶다. 디스코드에 올려봐야겠다!
다양한 고민을 하면서 개발을 하니까 좀 더 재밌고 의욕이 생기는 것 같다
다음미션도 잘 해봐야지
https://github.com/woowacourse-precourse/java-racingcar-7/pull/61