이번 주차의 미션은 자동차 경주였다.
랜덤 숫자를 생성하고, 그 값에 따라 자동차가 전진하거나 멈출 수 있다. 사용자로부터 입력 받은 시도 횟수만큼 전진을 반복하고나서, 가장 많이 전진한 자동차를 선정하는 내용이다.
2주 차 미션 저장소
https://github.com/woowacourse-precourse/java-racingcar-6
내가 제출한 PR
https://github.com/woowacourse-precourse/java-racingcar-6/pull/500
1주 차 공통 피드백에는 "Java API를 적극적으로 활용한다"라는 말이 담겨 있었다. 아마 코드에 대한 신뢰성, 생산성 향상 등등 다양한 이유로 인해 이런 피드백을 주신 것 같다.
나는 피드백을 반영하기 위해, 내가 구현할 기능을 Java에서 이미 제공하지는 않는지 열심히 찾아봤다. 그렇게 찾은 것들은 String.join()
, String.repeat()
등등 생각보다 많았다.
확실히 Java에서 제공하는 API를 이런 방식으로 학습하면 코드를 작성하는 에너지가 감소할 듯 하다😀
for문은 가독성이 좋지 않다고 생각하기도 하고, 공통 피드백에서도 Java API를 활용하라 해서 최대한 Stram을 활용하는 것을 목표로 했다.
리팩토링 단계에서 많은 부분을 Stream을 사용해서 수정했는데, 그중에서 눈에 띄게 수정된 부분은 다음과 같다.
// for 문 사용
public MoveResult handleCarMovement(NumberGenerator numberGenerator) {
List<Integer> forwardCounts = new ArrayList<>();
cars.forEach(car -> forwardCounts.add(car.move(numberGenerator.generate())));
for (Car car : cars) {
int forwardCount = car.move(numberGenerator.generate());
forwardCounts.add(forwardCount);
}
return new MoveResult(toCarNames(cars), forwardCounts);
}
// Stream 사용
public MoveResult handleCarMovement(NumberGenerator numberGenerator) {
List<Integer> forwardCounts = cars.stream()
.map(car -> car.move(numberGenerator.generate()))
.toList();
return new MoveResult(getCarNames(cars), forwardCounts);
}
처음에는 enhanced for문으로 각각의 Car가 이동한 전진 횟수를 List<Integer>
타입의 forwardCounts에 add()
하는 방식으로 작성했다.
하지만 Stream을 사용하니 굳이 add()
를 사용하지 않고도 간결하게 반복할 수 있었다.
이번 주차에 추가된 요구사항 중에 "함수는 한 가지 일만 한다"라는 문장이 있었다.
하지만 나는 함수가 한 가지 일만 해야 한다는 것이 잘 와닿지 않았고, 이를 판단할 기준이 필요했다.
그래서 메서드의 이름과 실제로 그 메서드가 하는 일을 비교해서 일이 더 많아 보이면 최대한 분리했으며, 자체적으로 코드 길이를 최대 15줄로 제한하고 파라미터 개수는 최대 2개까지 제한해봤다.
이렇게 스스로 모래주머니를 달아보니까 대부분의 메서드가 자연스럽게 한 가지 일만 할 수 있었던 것 같다.
저번 주차에는 랜덤 값에 대한 단위 테스트를 수행하지 못해서 한이 맺혀 있었다...
그래서 이번에는 꼭 테스트 코드를 작성해보고자 ApplicationTest.class에 있는 assertRandomNumberInRangeTest()
가 어떤 방식으로 동작하는지 알아봤다.
이 과정에서 Mockito 라이브러리의 anyInt()
같은 메서드를 사용해 값을 주입해줄 수 있다는 것을 알 수 있었다.
지금까지는 입출력이 정상 동작하는지 확인하기 위해 Application.class의 main()
을 실행시켰는데, 이게 너무너무 귀찮고 비효율적이라는 생각이 들었다. 테스트하는 더 좋은 방법이 분명 있을 거라 생각했고, 역시 많은 사람이 이에 대한 글을 작성해놨다.
여러 글들을 보면서 System.in과 Scanner의 세세한 동작까지 알게 되었고, 콘솔 입출력에 대한 테스트 코드를 작성할 수 있게 되었다.
테스트 코드를 작성하는 과정에서 한 가지 예외가 발생하여 해결해보기도 했는데, 이에 대한 내용을 블로그에 글로 남겨놓았다.
이번 주차 미션의 구현 난이도는 저번주보다 어렵지 않았지만, 요구 사항이 명확하지 않아 애매한 부분이 많았다.
아마 현업에서는 요구 사항이 모호하다는 점에서 우테코가 이를 의도한 것이 아닐까 싶다.
나한테는 오히려 이러한 부분이 재밌게 다가왔다. 사용자의 입장에서 게임을 실행했을 때, 어떤 오류를 낼 수 있을까를 생각해보면서 예외 상황을 찾는 것이 나름 재밌었다.
예외 처리를 하면서 많은 고민이 있었는데, 그중 가장 기억에 남는 것은 시도 횟수의 최댓값이었다.
요구 사항에는 시도 횟수에 대한 최소, 최댓값이 명시되어 있지 않았지만 분명 음수를 입력하거나 엄청 큰 수를 입력하여 오류가 발생할 일이 있을 것이라 생각했다.
최솟값의 경우 그래도 경주를 한 번은 시도해야 하지 않을까 싶어 1부터 가능하도록 설정했으며, 최댓값은 응답 시간을 고려했다.
API의 경우 300ms, 게임의 경우 이것보다 더 적은 응답 시간을 가져야 이상적이라는 말을 들었던 적이 있다. 여기서 이상적이라는 건 사용자가 기다리느라 불편함을 느끼지 않을 정도의 응답 시간을 말한다.
그래서 나는 시도 횟수를 3000부터 조금씩 감소시키면서 응답 시간을 측정해봤다. 이 과정에서 현재 시간을 받아오는 System.currentTimeMillis()
를 알게 되었고, 이를 활용해서 각각의 횟수에 따른 응답 시간을 측정할 수 있었다.
2000, 1000, 500, 300, 100 순으로 응답 시간을 측정해봤고, 그 결과 100회가 가장 이상적이라 판단하여 이를 최댓값으로 설정해주었다.
객체지향의 사실과 오해 책을 한 번 읽어봤는데, 반복해서 읽어보면 더 많은 것을 깨달을 수 있지 않을까 해서 2주 차부터 스터디를 시작하게 되었다.
스터디는 책을 읽으면서 학습한 내용을 미션에 적용해보고, 이 과정에서 느낀점을 팀원들과 공유하는 방식으로 진행했다.
생각보다 많은 분들이 참여해주셨고, 많은 것들을 알 수 있게 해주셔서 감사했다.
혼자 읽으려 했다면 아마 계속해서 미뤘을텐데, 스터디가 내 의지를 잡아주는 것 같아 다행이다😄
한 가지 아쉬운 점이 있다면, 생각보다 요구 사항만 만족해도 시간이 빨리 가서 static 메서드 사용에 대한 학습을 제대로 하지 못했다는 것이다. 이번 주차에는 static 메서드를 언제 사용할지에 대한 나만의 기준을 세워보고 싶다.
"프리코스가 시작되면 다양한 디자인 패턴과 TDD, DDD 등등 고급 기술을 학습해서 도입해야지!"라는 생각을 가지고 있었지만, 이는 내가 깨닫지 못한 착각이었다. 처음에는 다양한 고급 기술들을 적용하려는 열망이 있었지만, 실제로 미션을 수행하면서 주어진 요구사항을 충족시키는 것만으로도 많은 시간이 소요됨을 느꼈다. 그 과정에서 기본적인 원칙을 지키는 것의 중요성을 깨달았고, 내가 그동안 얼마나 이 기본적인 부분에 소홀했는지를 깨닫게 되었다.