[우아한테크코스 6기] 2주차 미션 회고 - 자동차 경주(feat.DTO)

Jinny·2023년 11월 1일
1

💬 들어가며

이번주차 미션은 자동차 경주 게임이었다.
미션 요구사항과 필자가 작성한 코드는 밑에 링크에서 확인할 수 있다.

이번주 미션을 진행하면서 구조 설계에 대해 많은 고민을 했던 것 같다.

구현하다보니 ServiceController는 비슷한 역할을 했고 역할을 나누는 것이
오히려 오버 프로그래밍인 것 같다는 생각이 들어서 이번 미션에는 Service없이 domaincontroller로 구성하게 되었다.

그럼 회고를 시작해보자!!




🔍 공부한 내용과 고민

테스트 코드

테스트 라이브러리의 존재는 알고 있었지만
그동안 이런저런 이유로 학습을 미뤄왔었는데 이참에 기술부채를 좀 해결해보았다.

  • 일단은 JunitAssertJ에 대해 공부를 했고,
    많은 사람들이 가독성 측면에서 AssertJ를 더 선호한다는 것을 알게되었다.

  • 여러 개의 Assertion이 있을 경우, 테스트 코드가 실패해도 모두 검증하고 결과를 보여줄 수 있는 방법에 대해 찾아보다가 assertAll()SoftAssertions의 차이점의 대해 공부했다.




결과 출력을 위한 Dto 사용

우선 Dto 사용 이유에 대해 이해를 돕기 위해 내가 했던 고민과 프로젝트 구조에 대해 간단하게 설명을 해보겠다.

고민

사실 별거 아닌 고민일 수도 있지만,
회차별 결과를 바로바로 출력할지 경주가 끝날 때까지 결과를 모아두었다 한꺼번에 출력할지 고민했다.

실행 결과 예시

실행 결과
pobi : -
woni : 
jun : -

pobi : --
woni : -
jun : --

pobi : ---
woni : --
jun : ---

게임이 좀 더 확장된다면 유저가 쫄깃한(?) 게임을 위해 라운드를 한번씩 누르며 게임을 진행할 수도 있겠지만
그런 요구사항은 없었기에 일단 주어진 그 자체로 생각하기로 했다.

한번씩 출력을 한다해도 사람이 시간 간극을 느끼지 못할정도로 동시에 출력이 될 것으로 생각했다.
그리고 System.out.println()은 무겁기 때문에 호출 횟수를 줄이고 싶었다.

그래서 List에 결과를 담아두었다가 결과를 한꺼번에 출력하게 되었는데,
그 과정에서 여러가지 문제점이 발생했다. 😅


프로젝트 구조

최종 프로젝트 구조는 다음과 같다.

  • 도메인:
    • Car.java: 경주 게임에 참여하는 자동차 1대를 의미하는 클래스
    • Cars.java: 경주 게임에 참여하는 자동차 여러 대를 담당하는 클래스
    • Game.java: 경주 게임을 담당하는 클래스
  • Dto
    • RoundResult.java: 경주 라운드별 결과를 담당하는 클래스

Game.java

  • Cars와 경주 횟수가 정해져야 Game 객체가 생성되고, List<RoundResult>에 모든 게임 결과가 저장된다.
  • 결과를 출력할 때는 List<RoundResult>StringBuilder로 붙여서 한번에 출력한다.

(List<RoundResult>Repository로 따로 분리할까하다가 오버프로그래밍인 것 같아서 Game 도메인 안에 두었다.)

public class Game { // 이해를 돕기 위해 첨부한 코드로, 생략된 부분이 있다.

    private final Cars cars;
    private final int numberOfAttempts;
    private final List<RoundResult> results;

    public Game(Cars cars, int numberOfAttempts) {
        this.cars = cars;
        this.numberOfAttempts = numberOfAttempts;
        this.results = new ArrayList<>();
    }
    
    public void moveCars() {
        for (int i = 0; i < numberOfAttempts; i++) {
            cars.moveAll(getMovingNumbers());
            saveResult();
        }
    }

    private void saveResult() {
        results.add(RoundResult.from(cars));
    }
}

Dto가 만들어지기 까지 다음과 같은 시도가 있었다.

첫번째 시도: List<Map<String, Integer>>

Dto를 만들기 전에는 자동차 이름과, 이동한 자리를 Map<String, Integer>에 저장하고,
회차별 구분을 위해 Map<String, Integer>를 랩핑한 클래스를 만들었다.

그런데 코드 짤 때는 생각치도 못했던 문제를 테스트를 하면서 발견했는데,
HashMap이라서 자동차들의 출력 순서가 보장되지 않는 문제가 있었다.

RoundResult.java

public record RoundResult(Map<String, Integer> carsPositions) {

    public static RoundResult from(Cars cars) {
        return new RoundResult(cars.getCars().stream()
                .collect(Collectors.toUnmodifiableMap(
                        Car::getName,
                        Car::getPosition
                )));
    }
}

두번째 시도: List<LinkedHashMap<String, Integer>>

출력 순서를 보장하기 위해 LinkedHashMap으로 변경했는데, Car로부터 변환을 위해 스트림의 merge function을 사용해야 했다.

merge function에 대해 아직 완벽하게 이해가 되지 않아서 이해하지 못하는 코드를 쓰고 싶지 않았다.
(회고를 하면서 다시 보니 스트림이 오히려 for문 보다 가독성이 떨어지는 것 같아서 for문을 사용해도 좋았을 것 같다.)

그리고 외부에서 볼 때 Map의 String, Integer가 각각 무엇을 의미하는지 안보이는 것도 가독성이 떨어진다고 생각이 들었다.

public record RoundResult(LinkedHashMap<String, Integer> carsPositions) {

    public static RoundResult from(Cars cars) {
        LinkedHashMap<String, Integer> carsPositions = cars.getCars().stream()
                .collect(Collectors.toMap(
                        Car::getName,
                        Car::getPosition,
                        (existing, replacement) -> existing,
                        LinkedHashMap::new
                ));
        return new RoundResult(carsPositions);
    }
}

세번째 시도: Dto 생성

앞선 두가지 문제점을 해결하기 위한 방법을 고민하다가
문득 최근에 공부했던 inner class를 활용한 Dto를 만들어보면 어떨까하는 생각이 들었다.

[Spring] DTO 관리 - Inner Class

Dto 구현을 통해 앞서 말한 두가지 문제점을 해결해보았다.

  • 출력 시 순서 보장
  • 각 필드가 무엇을 의미하는지 표현

public record RoundResult(CarsDto carsDto) {

    public static RoundResult from(Cars cars) {
        return new RoundResult(CarsDto.from(cars));
    }

    public record CarsDto(List<CarDto> carDtos) {

        private static CarsDto from(Cars cars) {
            return new CarsDto(cars.getCars().stream()
                    .map(CarDto::from)
                    .toList());
        }
    }

    public record CarDto(String name, int position) {

        private static CarDto from(Car car) {
            return new CarDto(car.getName(), car.getPosition());
        }
    }
}



👋 마치며

자동차 경주 미션을 어느정도 마무리하고 우테코 커뮤니티에서 서로 PR 리뷰를 하였는데,
모든 사람들이 정말 열정이 가득했다.!

그 열정 속에서 남들 코드 보면서 내가 생각치 못한 부분도 배울 수 있었고,
내 코드에 대한 피드백을 통해 내가 작성한 코드에 대해 한번 더 돌아볼 수 있어서 좋았다!

그리고 점점 체력이 떨어지는 것을 느끼는데,
이번주 회고도 떨어진 체력만큼 퀄리티가 좋지 않다고 느껴졌다..🥲

앞으로 꾸준히 코딩도 하고 글도 잘 쓰려면 체력 관리를 잘 해야겠다!

profile
공부는 마라톤이다. 한꺼번에 많은 것을 하다 지치지 말고 조금씩, 꾸준히, 자주하자.

0개의 댓글