우테코 최종 코딩테스트를 준비했을 때 한 번 풀어본 문제였기 때문에 기능 요구 사항을 이해하는데 문제가 없었다. 기능이 어렵지 않아서인지 페어와 나는 프로그램의 실행 순서에 맞게 기능 목록을 작성했다. 지금 생각해보면 프로그램 규모가 작고 머릿속으로 설계할 수 있는 정도의 크기이기 때문에 프로그램 실행 순서대로 코드를 작성할 수 있었던 것 같다.
하지만, 만약 프로그램 규모가 커져 카트라이더 같은 게임이 되면 같은 방법으로 설계해도 될까? 의문이 들었다. 카트라이더 정도의 규모는 머리로 기능을 순서대로 나열하는 것이 불가능하다. 내 방식대로 설계하면 무조건 놓치는 기능이 생길 것이다.
회고를 작성하는 이 시점에서 내가 생각하는 좋은 방식은 도메인 별 기능 목록 작성이다. 비슷한 기능들을 모아서 도메인의 역할로 미리 정의해주고, 도메인 별로 코드를 작성하는 방식이다. 단위 테스트를 작성할 때도 편할 뿐더러, 기능이 추가되거나 삭제되더라도 기능 목록 수정이 편하겠다는 생각이 들었다. 그리고 놓치는 부분을 인지하고 기능 목록을 계속해서 업데이트해가는 것이 우테코에서 말하는 살아있는 기능 목록을 만드는 것이 아닐까 생각한다. 실제로 두 번째 미션인 "사다리 타기"를 진행할 때, 리뷰어분께서 도메인 별로 정리하는 것이 더 좋을 것 같다는 피드백도 받았다.
Car
객체에 자동차 이름을 저장하는 name
과 위치를 저장하는 currentPosition
를 필드를 두었다. 아래의 코드를 보면 currentPosition
의 자료형이 Position
인 것을 확인할 수 있는데, Car
에서 현재 위치를 관리하는 것은 자동차의 역할이라기에 무겁다는 느낌이 들어 Position
이라는 도메인을 만들어 관리하도록 했다.private final String name;
private final Position currentPosition;
public Car(String name, int startPoint) {
validateName(name);
this.name = name;
this.currentPosition = new Position(startPoint);
}
Cars
를 만들어 모든 Car
를 관리하는 리스트를 객체화시켜 한 번 감싸주었다. 일급 컬렉션으로 감싸준 이유는 링크를 참고하면 된다.private final List<Car> cars;
public Cars(List<String> carNames) {
validateDuplicatedNames(carNames);
validateCarCount(carNames.size());
this.cars = createCarsByNames(carNames);
}
Car
와 Cars
인스턴스를 생성하도록 했고, 만들어진 Cars
내부에서 자동차가 움직이는 로직을 구현했다. 코드에서 NumberGenerator
는 숫자를 발행하는 메서드가 들어있는 인터페이스이다. 전략 패턴을 통해 랜덤 숫자 발행기와 원하는 숫자 발행기를 만들어 테스트가 쉽도록 했다. 전략 패턴에 대한 자세한 내용은 링크를 참고하길 바란다.// Cars
public List<Car> moveCars(NumberGenerator numberGenerator) {
cars.forEach(car -> car.move(numberGenerator));
return Collections.unmodifiableList(cars);
}
// Car
public void move(NumberGenerator numberGenerator) {
int randomNumber = numberGenerator.generate();
if (isMovable(randomNumber)) {
currentPosition.move();
}
}
WinnerMaker
객체가 담당하도록 했다. 자동차들 중에 가장 위치값이 큰 자동차 하나를 가려낸 후에 그 자동차와 위치값이 같은 자동차들을 반환하도록 했다. 코드를 작성할 당시에는 가장 앞에 있는 자동차를 받아오는 것이 최선이라고 생각했지만 지금은 생각이 다르다. 자동차끼리 비교하는 것이 아닌 위치값끼리 비교하는 로직을 만들 것 같다. 결국 비교해야하는 대상은 위치값이기 때문이다.public class WinnerMaker {
public static List<String> getWinnerCarsName(List<Car> cars) {
Car winner = getWinner(cars);
return cars.stream()
.filter(car -> car.isSamePosition(winner))
.map(Car::getName)
.collect(Collectors.toUnmodifiableList());
}
private static Car getWinner(List<Car> cars) {
return cars.stream()
.max(Car::compareTo)
.orElseThrow(() -> new IllegalArgumentException(ErrorConstant.ERROR_PREFIX + "비교할 자동차가 없습니다."));
}
}
나는 결과값 계산에 필요한 자동차 이름
과 자동차의 현재 위치
를 DTO를 통해 View에 전달했다. 문제는 Car에서부터 dto를 만들어 view로 전달했다는 것이다. 코드를 작성할 당시 내가 생각한 dto의 역할은 아래와 같다.
2가지 장점을 모두 살려 사용했는데, 그렇다면 무엇이 문제일까? 나는 dto 이전에 생각해야할 가장 큰 원칙인 mvc를 무시하고 dto를 사용했다. domain이 마치 view를 인식하고 데이터를 전달하는 듯한 느낌을 준다. domain은 view를 몰라야하는데, dto를 domain에서 사용함으로써 마치 view를 아는 것처럼 되어버렸다. 그로 인해 dto에 대해 더 알아보는 것이 좋을 것 같다는 리뷰를 받았고 더 깊게 공부할 수 있었다. 공부한 후에 controller에서 dto를 생성하도록 로직을 바꾸었고 dto가 dto답게 쓰일 수 있게 되었다.
처음에 Cars
라는 객체 이름 대신 CarRepository
라는 이름을 사용했다. DB와 연결되어 쿼리로 원하는 데이터를 들고오는 역할이 Repository의 역할이다. 하지만 자동차의 일급 컬렉션의 역할을 하는 객체 이름을 CarRepository
로 만들었으니 언급 받았다. 처음에는 자동차 데이터들을 관리하는 역할이기 때문에 Repository로 이름을 정해야 좋지 않을까 생각했지만, Repository와 일급 컬렉션을 제대로 이해하지 못한 내 불찰이었다. 리뷰어께서 언급해주신 덕분에 교정할 수 있었다.
첫 미션 자신만만하게 풀었지만 다시 돌아보니 부족한 점이 많았다. 1단계 미션의 코드는 부끄러운 정도이다. 우테코 기간동안 부족한 부분들을 이렇게 기록하며 차곡차곡 쌓아가며 내 것으로 만들 예정이다. 1단계에서 미처 해결하지 못한 부분들은 2단계 로그에 업로드할 계획이다.
역시 내 페어 갱장하네요