우아한테크코스 4기 프리코스 2주차 미션 자동차 경주 게임을 진행하면서 고민했던 것을 기록으로 남깁니다.
2주차 미션은 자동차 경주 게임이다. 사용자가 자동차의 이름과 시도 횟수를 입력하면 경주 결과와 최종 우승자를 출력하는 간단한 경주 게임이다.
이번 미션도 지켜야 할 요구사항은 기능 요구사항, 프로그래밍 요구사항, 과제 진행 요구사항 세 가지로 구성되어있었다.
주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
사용자가 잘못된 값을 입력할 경우 IllegalArgumentException
를 발생시키고, [ERROR]
로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
아래의 프로그래밍 실행 결과 예시와 동일하게 입력과 출력이 이루어져야 한다.
입/출력 예시
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 회수는 몇회인가요?
5실행 결과
pobi : -
woni :
jun : -pobi : --
woni : -
jun : --pobi : ---
woni : --
jun : ---pobi : ----
woni : ---
jun : ----pobi : -----
woni : ----
jun : -----최종 우승자 : pobi, jun
Application
의 main()
이다.public class Car {
private final String name;
private int position = 0;
public Car(String name) {
this.name = name;
}
// 추가 기능 구현
}
camp.nextstep.edu.missionutils
에서 제공하는 Randoms
, Console
API를 활용해 구현해야 한다.camp.nextstep.edu.missionutils.Randoms
의 pickNumberInRange()
를 활용한다.camp.nextstep.edu.missionutils.Console
의 readLine()
을 활용한다.src/test/java
디렉터리의 ApplicationTest
에 있는 모든 테스트 케이스가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.이전 미션보다 프로그래밍 요구사항이 더 많아졌다. 특히 특정 클래스를 사용하여 구현한다는 것은 객체 활용이 많아진다는 의미 같았다. 다른 추가 요구사항도 적용하기 어렵지는 않았다. 하나의 메소드의 기능을 줄일수록 코드가 짧아졌기 때문에 대부분 15라인 이하로 구현이 되었고, else를 사용하지 않고 바로바로 리턴하는 방식은 저번 미션부터 연습을 해봤기 때문에 어렵지는 않았다.
미션 시작과 함께 받은 메일에서 1주차 미션의 목표가 메소드를 분리하는 것이라고 한 것을 봤다. 1주차 미션에서 indent depth를 2 이하로 제한하고 메소드의 기능을 최대한 줄이라는 것이 힌트였나? 하고 생각했다. 이번 미션의 가장 목표는 클래스를 분리하며 개발하기다. 그렇다면 클래스를 분리하는 것은 무엇일까...? 라는 생각으로 미션을 시작한 것 같다.
racingcar
├ controller
│ └ RacingCarGameController
├ domain
│ ├ Car
│ ├ CarName
│ ├ CarNames
│ ├ Cars
│ └ RacingRound
├ enums
│ └ DomainConditions
├ utils
│ └ CustomNsTest
├ view
│ ├ InputView
│ └ OutputView
└ Application
각 클래스에서 하는 일
List<Car>
에 대한 일급 컬렉션이번 미션은 최대한 목표인 클래스 분리에 신경 쓰며 구현했다. 이번 미션은 여러번 초기화를 하면서 새로 구현했었다. 같은 것을 구현하면서 점점 더 코드가 나아졌지만 그만큼 부족한 점도 많다고 느껴서 많이 최종 테스트까지 많이 준비해야 할 것 같다.
❗ 잘못된 정보가 혹시라도 있다면 알려주세요 !!
이번 미션도 기능 요구사항만을 만족하도록 구현한다면 난이도는 크게 어렵지 않았다고 생각했다. 중요한 것은 이번 미션을 빨리 끝내는 것이 아닌 어느 것을 배우고 느껴서 최종 테스트에서 좋은 결과를 낼 것인가라고 생각했다. 이번 미션은 구현을 완료한 뒤 초기화하고 다시 구현하는 과정을 3번 반복하여 구현했다. 단순하게 구현하는 게 아닌 클래스를 분리하여 개발하는 것에 대해 이해하고, 평소에 해보고 싶었던 것들을 이 미션으로 실험해보고 싶었기 때문이다. 이번 미션에서 평소에 해보지 못한 것들을 시도해보는 데 집중했던 것 같다.
클래스를 분리한다는 것이 뭘까? 말 그대로 클래스를 나눈다는 말이겠지만 처음 들었을 때 감이 오지는 않았다. 이번 미션에서 제시한 요구사항에서도 직접적인 힌트는 없어 보였다. 클래스의 분리에 대해 검색하고 공부하며 내가 찾은 키워드는 "단일 책임 원칙", "일급 컬렉션" 두 가지다.
객체지향의 설계 방법의 수많은 글에서 늘 보던 하나의 클래스는 하나의 책임을 가진다는 그 단일 책임 원칙이다. 즉 책임에 따라 클래스를 분리하면 될 것이다. 말은 쉽고 간단하지만 실제로는 그렇지 않았었다. 공부하면서 내가 정리한 단일 책임 원칙은 다음과 같다.
난 이 부분을 이전 미션인 숫자 야구 게임을 구현하면서 했던 고민을 통해 이해할 수 있었다.
서로 다른 클래스 C1과 C2에서 Valid의 메소드 V를 통해 검증을 하는 상황에서 C1에 의해 V가 수정되야 한다면 C2에 영향을 미칠 수 있다. 그렇기 때문에 유효성 검증하는 클래스를 삭제하고 두 개의 클래스에서 따로따로 검증하는 메소드를 만들 것이라고 했었다. 여기서 C1에 새로 만든 V1이라는 메소드는 서로 같은 책임이고, C2에 새로 만든 V2라는 메소드는 같은 책임을 가질것이다. 사실 이게 상황에 따라 틀릴 수도 있겠지만 어느 정도는 변경으로 영향을 받는 것들을 같은 책임으로 본다고 이해했다.
일급 컬렉션은 기억보다 기록을 을 참고했습니다. 감사합니다.
일급 컬렉션은 컬렉션 변수를 Wrapping 하면서 다른 멤버 변수가 없는 클래스를 의미한다. 일급 컬렉션은 다음과 같은 장점이 있다고 한다.
네 가지 모두가 일급 컬렉션을 사용해야 할 이유이다. 여기서 비지니스에 종속적인 자료구조는 클래스를 분리하여 개발하는 이번 미션를 위해 필요한 장점이다. 모든 비즈니스 로직이 일급 컬렉션 클래스 안에서 이루어진다는 말이다. 이 말이 처음엔 조금 어려웠는데, 한 클래스에 해당 콜렉션을 사용하는 메소드를 모아놨다고 생각하니 조금 이해하기 쉬웠다. 일단 일급 컬렉션 사용 에시를 보자.
이번 미션에서 자동차는 여러 대 설정할 수 있다. 그리고 이 자동차들을 이용해 경주를 해야 하고, 우승자를 찾아내야 한다. 그렇다면 예를 들어 다음과 같이 구현할 수 있을 것이다.
class Game {
// ...
public void play() {
String[] names = readLine().split(",");
List<Car> cars = new ArrayList<>();
for (String name : names) {
cars.add(new Car(name));
}
racing(cars);
List<Car> winners = findWinners(cars);
}
private void racing(List<Car> cars) {
// 경주하는 기능
}
private List<Car> findWinners(List<Car> cars) {
// 우승자를 찾는 기능
}
// ...
}
여기서 List<Car> cars
를 일급 컬렉션으로 만든다면 다음처럼 만들 수 있다.
class Game {
// ...
public void play() {
String[] names = readLine().split(",");
Cars cars = new Cars(names);
cars.racing();
List<Car> winners = cars.findWinners();
}
// ...
}
class Cars {
private final List<Car> cars = new ArrayList<>();
public Cars(String[] names) {
for (String name : names) {
cars.add(new Car(name));
}
}
public void racing() {
// 경주하는 기능
}
public List<Car> findWinners() {
// 우승자를 찾는 기능
}
}
그럼 이렇게 되는 것이 왜 장점인지 알아야 한다. 비즈니스에 종속적인 자료구조라는 관점에서 내가 생각한 일급 컬렉션의 장점은 다음과 같다.
일급 컬렉션에서 모든 로직이 있다 보니 다른 사람이 코드를 보거나 수정할 일이 있어도 변경할 부분을 더 빠르게 찾고 더 명확하게 변경할 수 있다. 또 일급 컬렉션을 사용하는 상위 클래스의 코드가 더 깔끔해진다. 그리고 컬렉션이 해야 하는 일을 일급 컬렉션으로 분리되기 때문에 책임이 나뉘어 단일 책임 원칙을 만족할 수 있다. 즉 깔끔하고 이해하기 쉬운 객체지향적 코드가 된다! 고 생각한다.
나는 이번 미션에서 자동차를 모아둔 일급 컬렉션 Cars를 만들었는데 결국 List<Car>
가 사용하는 메소드는 너가 다 가져가서 알아서 처리해! 하고 클래스를 만들어준 것이었다. 여기서 내가 든 생각은 이게 꼭 컬렉션이어야 하나? 그냥 저렇게 책임이 여러 개면 다 분리하면 안 되나? 라는 것이었다. 자동차 이름과 시도 횟수가 그 예시다. 그 이전에는 입력받는 클래스에서 이름과 시도 횟수의 유효성을 검증하는 메소드가 있었다. 아래와 같이 한 클래스에서 여러 책임을 가지고 있는 경우의 상황이다.
class InputView {
// 상수
// 생성자
// 입력에 관련된 메소드
...
자동차_이름을_받는_메소드() {
String input = readLine();
String[] names = input.split(",");
자동차_이름을_검증하는_메소드(names);
}
자동차_이름을_검증하는_메소드(String[] names) {
// 검증
}
시도_횟수를_받는_메소드() {...}
시도_횟수를_검증하는_메소드() {...}
...
}
여기서 InputView는 입력받는 역할만 하면 될 것이다. 입력값을 검증하는 것은 InputView에 있을 필요가 없다. InputView는 입력만 받고, 자동차 이름은 (CarName, CarNames) 객체를 만들어 검증만 하고 돌려주고, 시도 횟수도 RacingRound에서 검증만 하고 돌려주는 방식으로 구현한다면 클래스를 분리할 수 있을 것 같았다.
class InputView {
...
자동차_이름을_받는_메소드() {
String input = readLine();
String[] names = input.split(",");
names = new CarNames(names).get();
}
시도_횟수를_받는_메소드() {...}
...
}
class CarNames {
private String[] carNames;
public CarNames(String[] carNames) {
validate(carNames);
this.carNames = carNames;
}
public String[] get() {
return carNames;
}
private void validate(String[] carNames) {
// 검증
}
}
class CarName {...}
class RacingRound {...}
이것은 단순히 클래스를 분리한 것이 아닌 각자의 역할을 나누어 분리했다는 게 의미가 있는 것 같다. 지금은 간단한 이름, 시도 횟수를 검증하는 기능만 있지만 이후 기능이 추가되는 등 다른 기능이 필요할 때 수정이 더 쉬울 것이다.
이번 미션에서는 setter를 쓰지 말라는 요구사항이 있다. 내가 대학생 때 java를 배울 때 캡슐화를 위해서 우리는 getter와 setter를 써야 한다고 배웠다. 내부 속성을 알거나 직접 접근하지 못하게 하기 위함이다. setter를 쓰지 말라는 요구사항을 보자마자 그럼 getter는 써도 되나? 라는 생각을 했다. getter와 setter도 결국 그 값을 그대로 넣어주고 받아오는 것이기 때문이다. 찾다 보니 getter도 되도록 사용하지 말라는 것을 알게 되었다. 그럼 은닉화와 캡슐화를 지키면서도 내가 원하는 작업을 하려면 어떻게 해야 할까?
캡슐화에 대해 공부하면서 묻지말고 시켜라라는 말을 봤고, 이 모든 궁금증을 해결해 주었다. 처음에는 조금 어려웠다. 나는 getter로 값을 받아와서 메소드에 파라미터로 보내고 원하는 정보를 받아온 뒤 setter로 저장하는 방식에 익숙해져 있었기 때문이다. 그러나 이런 방식은 객체지향이 아닌 절차지향에 가깝다고 한다. 객체지향에서는 객체의 상태를 묻지 않고 시킨다고 한다. 예를 들면 이런 것이다. 자동차라는 객체가 있다. 이 자동차가 앞으로 한 칸 이동하고 싶다면 방법은 두 가지다.
지금 앞에 장애물이 막고 있는지, 연료가 있는지, 엔진이 고장났는지 그건 내가 모르겠고 그냥 넌 앞으로 가! 문제있으면 니가 예외 처리 해! 이게 객체지향적인 방식이라는 것이다. 그렇기 때문에 메소드의 이름을 통해 이 작업의 의도를 나타내는 것이 중요한 것이었다. 이것을 이번 미션에서 최대한 적용하면서 구현해보려 노력했었다. 그런데 의외로 적용이 어렵지는 않았던것 같다. 나도 모르게 이런식으로 구현한 적도 있었던것 같고 무엇보다 코드가 깔끔해지다 보니 자연스럽게 적용하게 되었다.
1주차 과제를 하면서 아직도 객체지향적인 코드가 뭔지 막연하다고 했었던게 기억난다. 그러나 지금은 적어도 그때보다는 더 나아진 것 같다. 객체지향이 뭔지에 대해 검색하면 가장 먼저 나오는 것들이 객체지향의 설계 5원칙, 객체지향의 4가지 특징과 같은 것들이 나온다. 이 개념은 객체지향언어를 공부한 사람들이라면 대부분이 알고 있을 것이다. 나도 이런 것들을 들어봤고 알고 있지만, 이해하고 적용하지 못하고 있던 것 같다. 이런 것들을 일기 위해 이런 미션을 진행해보는 것은 너무나 큰 경험인 것 같다. 내가 숫자 야구 게임에서 검증 클래스를 만들어보지 않았다면, 일급 컬렉션의 존재를 몰랐다면 나는 아직도 책임에 대해서 몰랐을 것이고 하나의 클래스에 여러 메소드를 넣어두고 있을 것이다. 이런식으로 이론이 아닌 실제로 무언가 해보는 것이 큰 도움이 된다는 점에서 현장 중심의 교육이 얼마나 중요할지 기대도 된다.
TDD(Test Driven Development)는 테스트 주도 개발이라고도 한다. TDD라는 것은 이전에 토이 프로젝트를 하면서 처음 알게 되었다. 처음 테스트 코드를 보고 내가 든 생각은 이제 더 이상 코드를 바꿀 때마다 확인을 위해 System.out.println()를 찍어보지 않아도 되는것인가??? 와 이거 대박이다! 라는 생각을 했었다. 그러나 실제 적용이 참 어려웠다. 구현 초기 단계에서 요구사항이 계속 변하다 보니 그때마다 테스트 코드를 함께 변경해줬어야 했다. 또 시간이 추가로 걸리다보니 결국 구현이 다 마무리된 후 테스트 코드를 작성하게 되었고 내가 작성한 코드에 테스트를 맞추려는 것 같은 느낌을 받기도 했기 때문이다. 이전 프로젝트에서 테스트 코드를 작성하기 어려운 가장 큰 원인은 요구사항과 구조가 자꾸 변했기 때문이다.
이번 미션은 요구사항과 기능이 명확하기 때문에 TDD를 실습해보기 쉬울 것 같다고 생각했다. 그리고 적용을 해보면서 역시 쉽지는 않다고 느꼈다. 개발 속도가 느려진다는 것이 가장 불편한 점이었다. 그리고 테스트 코드를 작성하는것을 자주 잊기도 했다. 하지만 기능이 잘 동작하는지 확인할 수 있다는 점, 내가 변경한 코드가 시스템 전체에 의도하지 않은 영향을 주는지 확인할 수 있다는 점은 좋다고 느꼈다. 짧게 보면 개발 속도가 느려지는 것이 너무 불편하고 손해 같다고 생각한다. 하지만 더 길게 본다면 코드가 쌓여갈수록 장점이 더 커지지 않을까 생각한다. TDD는 앞으로도 더 적용해보면서 공부해봐야 할 것 같다.
나는 대부분의 개발을 데스크톱을 이용했다. 두 개의 모니터를 사용하는 게 너무 익숙했기 때문이다. 하지만 우아한테크코스에 합격하게 된다면 매일 노트북을 사용하게 될 것이다. 당장 최종 테스트도 오프라인으로 본다면 노트북을 사용해야 한다. 그래서 노트북과 친해지기 위해 이번 미션은 모든 과정을 노트북으로 진행했다.
노트북을 사용하면서 가장 큰 불편함은 마우스가 없다는 것이었다. 터치패드가 있지만 수년간 사용한 마우스보다 불편했다. 하지만 오히려 이 불편함 때문에 수많은 단축키를 사용하게 되었다. 단축키는 개발 속도를 향상시켜줬고 익숙해지고 나니 마우스를 쓰는 것 보다 편해졌다. 특히 IntelliJ에서는 적어도 이번 미션을 진행하는 동안 마우스가 없이도 모든 작업을 할 수 있었다. 다음은 IntelliJ에서 내가 가장 유용하게 사용했던 단축키들이다. 진짜 너무 편하다. (윈도우 기준이다.)
진짜 하나같이 너무 소중한 단축키들이다. 지금은 오히려 하나의 화면으로 개발에만 집중할 수 있어서 노트북이 더 편해지는 것 같다. 합격과 별개로 앞으로 노트북을 많이 사용하게 될 텐데 잘 맞는 것 같아서 다행이다.
1주차 미션이 끝난 뒤 받은 공통 피드백에서 처음부터 완벽하게 기능 목록을 정리하고 클래스, 메소드 등을 설계하지 말고 README.md를 활용하여 구현해야 할 기능을 정리하는데 집중하라는 것이 가장 기억에 남는다. 왜냐하면 이것은 평소의 나의 코딩 스타일과 정반대였기 때문이다. 이렇게 구현하기 위해 평소와 전혀 다른 방식으로 구현을 시도했다. 평소에 나는 코드를 작성하기 전 전체적인 구조를 그리고 클래스나 메소드를 간단하게 정리한 뒤 구현을 시작했었다. 하지만 이번 과제에서는 구현할 기능만을 정리하고 구현을 시작했다. 생각해보면 계획을 완벽하게 세워도 어차피 구현하면서 수정된다. 오히려 기능만을 정리하고 그것에 집중해 구현하니 더 빨랐다. 또 커밋 단위를 기능 단위로 하라는 요구사항이 있었는데 어떤 기준에서 커밋을 할지가 명확해지다보니 이것을 만족하기에도 좋았다. 큰 것을 한 번에 구현하려 하기보다 작은 것부터 구현해나가는 것이 더 쉽고 간단하게 구현할 수 있는 방법이라고 느꼈다.
그리고 난 원래 구현한 것에 대해 기록을 잘 하지 않았다. 구현하면서 느낀점이나 새로 알게 된 것들은 노션에 메모하면서 남기지만 내가 구현한 코드에 대해 기록하지 않는다는 의미이다. 늘 혼자 개발하다보니 필요성을 느끼지 못했었다. 하지만 다른 사람들과 함께 일하게 된다면 기록은 중요하다고 생각하게 되었다. 나는 기록을 하기위해 README.md에 프로젝트에 대해 나름대로 상세하게 기록했다. 어떤 프로젝트이고 어떻게 동작하고 구현 환경, 요구사항 등을 정리했다. 하지만 코드에 대한 주석은 추가하지 않았다. 코드에서는 주석을 통한 소통보다 최대한 이름으로 의도를 나타내기 위해 노력했다.
미션을 하다보면 README.md에서 기능을 구현해나가면서 구현한 기능을 체크를 해야 한다. 처음엔 완성된 기능 앞이나 뒤에 ✔️를 추가했었다. 하지만 통일된 느낌이 없는 게 아쉬웠었다. 찾아보니 마크다운에서 체크박스가 있었다. - [ ]
를 하면 빈 체크박스가 되고 - [x]
를 하면 찬 체크박스가 된다. 이것은 github에서도 많이 사용되기 때문에 README.md에서도 많이 사용된다고 한다. 그리고 PR 메시지에 체크박스를 적어서 보냈더니 다음처럼 tasks done이라고 나오면서 체크박스가 적용되기도 했다. 잘 활용하면 앞으로 유용하게 사용할 수 있을 것 같다.
이전 미션에서 상수 클래스를 만든 지원자들이 많았었다. 나는 상수를 모아둔 클래스를 만들 필요가 없다고 생각해서 만들지 않았다. 이럴경우 private 접근자를 사용하기 때문에 외부에서 사용하지 못한다. 여기서 한가지 문제가 발생했다.
private static final String LONG_LENGTH_ERROR_MESSAGE = "[ERROR] 5자 이하로 입력해주세요.";
위 상수는 자동차 이름의 길이가 6자 이상이 되면 출력하는 상수이다. 매직넘버를 사용하지 않기 위해 상수를 썼는데 상수에 매직 넘버가 있다. 이 경우 아래와 같이 수정을 할 수 있다. 이번 경우는 문제가 발생하지 않지만 MAX_CAR_NAME_LENGTH를 다른 곳에서 사용한다면 그곳에 또 상수를 생성해야 한다.
private static final int MAX_CAR_NAME_LENGTH = 5;
private static final String LONG_LENGTH_ERROR_MESSAGE = "[ERROR] "
+ MAX_CAR_NAME_LENGTH
+ "자 이하로 입력해주세요.";
물론 책임을 잘 분리해서 클래스를 나누고 사용하면 문제가 없을 수도 있다. 하지만 하나의 상수를 여러 곳에서 사용하는 경우가 있을 것이다. 예를들어 지금의 코드에서 문제가 발생했을 때 에러 코드의 시작을 [ERROR]
가 아닌 [WARNING]
으로 바꾼다면 모든 곳의 상수에서 값을 바꿔줘야 한다. 이럴 경우 [WARNING]
을 한곳에서 가지고 있고 모든 에러 메시지에서 그 값을 가져와서 사용하면 문제를 해결할 수 있다. 이럴 때 Enum을 사용할 수 있을 것 같았다. 실제 코드에서는 여러가지 조건에 대한 부분을 Enum으로 만들어 사용해보았다.
public enum DomainConditions {
MAX_CAR_NAME_LENGTH(5),
EMPTY_CAR_NAME_LENGTH(0),
EMPTY_RACING_ROUND(0),
RANDOM_START_NUMBER(0),
RANDOM_END_NUMBER(9),
MOVING_POINT_NUMBER(4);
private final int value;
DomainConditions(int value) {
this.value = value;
}
public int get() {
return value;
}
}
////
private static final int MAX_CAR_NAME_LENGTH = DomainConditions.MAX_CAR_NAME_LENGTH.get();
private static final String LONG_LENGTH_ERROR_MESSAGE = "[ERROR] "
+ MAX_CAR_NAME_LENGTH
+ "자 이하로 입력해주세요.";
...
private void validateRange(String name) {
int nameLength = name.length();
if (nameLength > MAX_CAR_NAME_LENGTH) {
throw new IllegalArgumentException(LONG_LENGTH_ERROR_MESSAGE);
}
}
그 결과 이런 식으로 코드를 수정하였다. 이 코드를 예로들면 현재 자동차의 최대는 5이지만 최대 10자의 자동차 이름을 받고 싶다면 Enum의 값만 수정해주면 된다. 자동으로 유효성 검증 메소드와 에러 메시지가 수정된다. Enum을 처음 사용해봤는데 분명 좋아 보이고 편해 보이지만 아직은 좀 어설프게 적용해본 것 같아서 앞으로도 연습을 해봐야 할 것 같다.
람다식은 익명 함수를 말하는 것이라고 한다. 쉽게 함수지만 내가 따로 메소드를 만들어 사용하는 것이 아닌 그자리에서 슥 만들어서 쓰는 것이다. 나는 원래 람다식을 잘 쓰지 않았다. 알고리즘 문제를 풀다보면 람다식으로 문제를 풀면 속도가 더 느려지기 때문에 안좋은 인식이 있었기 때문이다. 람다식은 실제로 stream을 사용하면 느리다는 단점이 있긴 하지만 코드가 간결해지고 개발자의 의도를 나타내기 쉽다는 장점이 있다고 한다. 오히려 병렬처리를 하면 속도가 더 빠르기도 하다고 한다. 이번 미션에서는 단순하게 느리다는 단점보다 코드의 가독성을 향상한다는 장점만을 보고 적용해보기로 했다.
public List<Car> findWinners() {
Car maxPositionCar = cars.stream()
.max(Car::compareTo)
.orElseThrow(() -> new IllegalArgumentException(CARS_IS_EMPTY_ERROR_MESSAGE));
return cars.stream()
.filter(car -> car.isSamePosition(maxPositionCar))
.collect(Collectors.toList());
}
이 코드를 일반적인 코드로 수정하면 다음처럼 된다. 딱 봐도 가독성이 안 좋아진 것은 물론이고 당장 이 코드가 뭘 하겠다는 것인지 알기 어려워진다.
public List<Car> findWinners() {
if (cars.isEmpty()) {
throw new IllegalArgumentException(CARS_IS_EMPTY_ERROR_MESSAGE);
}
Car maxPositionCar = cars.get(0);
for (Car car : cars) {
if (maxPositionCar.compareTo(car) < 0) {
maxPositionCar = car;
}
}
List<Car> winners = new ArrayList<>();
for (Car car : cars) {
if (car.isSamePosition(maxPositionCar)) {
winners.add(car);
}
}
return winners;
}
공부하면서 본 글에서 "컴퓨터의 성능은 올릴 수 있지만, 가독성이 떨어지는 코드는 이후에 모든 개발자가 고통받는다" 이런 느낌의 말을 본 적이 있다. 람다식을 사용하는 이유가 딱 맞을 것 같다. 람다식을 이용하면 좀 더 느려질 수도 있다. 하지만 이후 이 코드를 보는 개발자가 편해질 것이다. 물론 람다식을 남용하면 오히려 가독성이 해칠 수 있고 코드의 재사용성이 떨어질 수 있다. 그렇기 때문에 필요한 부분에 람다식을 적절하게 사용할 수 있도록 연습해야 할 것 같다.
사실 이번에 람다식을 적용하면서 검색을 거의 하지 않았다. IntelliJ에서 추천해주는 Ctrl + space
를 기능을 많이 사용하는데 이곳에서 추천하는 함수를 보면 이름, 파라미터, 반환형을 보여준다. 이름, 파라미터, 반환형만 봐도 이 함수가 무엇을 하는 함수인지 알 수 있을 만큼 쉽고 명확하다. 아래 예시를 보면 max는 최댓값인 녀석을 찾는구나, orElseThrow는 또는 아니면 throw를 하라는 거구나, filter는 조건에 의해 맞는 것만 리턴하는거구나 하는 식으로 이름만 보고도 람다식을 사용할 수 있었다. 물론 코드를 작성하고 나서 맞는지 확인하기 위해 검색을 해보았지만 전부 내가 생각한 것과 같았다. 이름에서 의도를 나타내라는 것이 얼마나 다른 개발자에게 얼마나 중요한지 알게 되었다. 아 그리고 IntelliJ의 추천 기능은 대박이다!
public List<Car> findWinners() {
Car maxPositionCar = cars.stream()
.max(Car::compareTo)
.orElseThrow(() -> new IllegalArgumentException(CARS_IS_EMPTY_ERROR_MESSAGE));
return cars.stream()
.filter(car -> car.isSamePosition(maxPositionCar))
.collect(Collectors.toList());
}
요구사항을 지키는 것은 기본 중 기본일 것이다. 이번 미션을 하면서 내가 실수한 부분이 하나 있다. README.md의 위치를 /docs/README.md로 이동하지 않고 작업한 것이다. 제출 직전 하나하나 확인하면서 이것을 알게 되었고 README.md를 이동시켰다. 이동시키고 나니 이전 히스토리가 다 사라져 버렸다. 나름대로 체크박스를 이용하면서 계속 수정하며 살아있는 문서로 만들기 위한 노력이 다 사라진 느낌이었다. 하지만 나의 실수였기 때문에 할 말이 없다. 다음 미션에서는 요구사항을 더 신경써서 확인해야겠다.
3주차부터는 난이도가 많이 오른다고 하는데 열심히 준비해야겠다.
👍
정성글 잘봤습니다,, ^^ 항상 노력하시는 모습이 아름다우세요.