[우아한테크코스 6기 BE] 2주차 회고 - 자동차 경주 🚘

반응형

2주차 미션

2주차 미션은 자동차 경주를 받았다.

2주차인 만큼 1주차에 비해 요구사항이 더 많아지긴 했지만

이번 미션은 생각보다 1주차 미션에 비해 구현이 쉬웠다는 느낌을 받았다.

그래서 이번 미션에는 1주차 공통 피드백에 대한 내용과 1주차 미션 코드 리뷰를 통해 내 코드에 대해 아쉬웠던점

적극적으로 반영하기 위해 많은 노력을 했다. (1주차 회고 보러가기)

이 과정에서 많은 수정이 있었고 그 결과 107개라는 너무 많은 커밋 수가 나왔다.. 

 

 

이번 2주차도 주어진 요구사항들에 추가로 나 자신에게 새로운 요구사항을 던져주었다.

나 자신에게 준 새로운 요구사항은 다음과 같이 리뷰를 통해 받은 피드백 반영과 추가로 더 신경쓰고 싶은 부분을 반영하고자 하였다.

  1. 각 클래스 별로 역할에 충실할 수 있도록 하기
  2. 메서드가 한가지만 기능만 하도록 잘게 쪼개기
  3. 메서드 이름을 통해 의도를 파악 할 수 있게 하기
  4. Enum과 매직넘버를 적절히 분리해 사용해보기
  5. 발생할 수 있는 예외 상항에 대해 꼼꼼히 생각하기
  6. 구현한 기능 별 꼼꼼한 단위 테스트 작성
  7. 불필요한 객체 생성 피하기 (ex : Pattern 객체 생성 캐시화)
제출 코드
 

GitHub - Hyeon0208/java-racingcar-6

Contribute to Hyeon0208/java-racingcar-6 development by creating an account on GitHub.

github.com


무엇을 배웠나?

 

Stream 최종 연산의 toList()

JDK 16부터 스트림 최종연산에 toList()라는 메서드 생겼다. (미션은 JDK 17을 사용)

해당 메서드는 불변리스트를 반환 해줌으로써 아래와 같은 코드가

        List<CarStatusDto> carStatuses = cars.stream()
                .map(car -> new CarStatusDto(car.getName(), car.getPosition()))
                .collect(Collectors.toList());
        return Collections.unmodifiableList(carStatuses);

 

이렇게 간단하게 바뀔 수 있다.

return cars.stream()
                .map(car -> new CarStatusDto(car.getName(), car.getPosition()))
                .toList()

 

 

다양한 JUnit5의 메서드

클래스 별 기능에 대한 단위 테스트를 작성하면서 새로운 JUnit5 메서드들에 학습할 수 있었다.

내가 새롭게 알게된 메서드들에 대해 간단히 설명하면 다음과 같다.

 

  • @ParameterizedTest

하나의 테스트를 다양한 매겨변수로 여러 번 실행할 수 있게 해주는 테스트 어노테이션이다.

해당 어노테이션과 함께 사용되는 어노테이션으로는 @CsvSource, @ValueSource, @EnumSource 등이 있다.

@ParameterizedTest
@ValueSource(ints = {1, 2, 3}) // 여기서 정의한 값들을 파라미터로 넘겨 테스트한다.
void ValueSource_어노테이션_사용_테스트1(int number) {
    assertTrue(number > 0);
}

@ParameterizedTest
@ValueSource(strings = {"apple", "banana"}) // 여기서 정의한 값들을 파라미터로 넘겨 테스트한다.
void ValueSource_어노테이션_사용_테스트2(String fruit) {
    assertNotNull(fruit);
}

@ParameterizedTest
@CsvSource({"123, 123", "01, 1"}) // CSV 형식의 값을 파라미터로 넘겨 테스트 한다.
void CsvSource_어노테이션_사용_테스트(int value, int expected) {
    assertEquals(expected, number);
}

@ParameterizedTest
@EnumSource(Day.class) // Enum클래스의 모든 상수를 파라미터로 넘겨 테스트한다. 
void EnumSource_어노테이션_사용_테스트(Day day) {
    assertNotNull(day);
}

enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

 

  • assertDoesNotThrow

해당 코드가 예외를 발생시키기 않는다는 것을 검증한다.

@Test
void 예외가_발생하지_않는다() {
    String input = "ping,pong,gong";

    assertDoesNotThrow(() -> CarNameValidator.validate(input));
}

 

  • extracting

객체의 특정 필드를 추출하여 검증할 수 있도록 도와준다.

@Test
void getCarStatuses() {
    // given
    List<Car> cars = Arrays.asList(new Car("pony"), new Car("zon"));

    assertThat(cars).extracting(Car::getName)
        .containsExactly("pony", "zon");
}

 

 

인터페이스 활용한 랜덤 번호 테스트

이미 요구사항이 정해진 과제 미션 특성상 확장성을 고려해 인터페이스를 도입하는 부분에 대해 많이 의아했었다.

왜냐면 오히려 복잡성만 증가되고 코드 가독성이 떨어진다고 생각했기 때문이다.

하지만 이번 코드리뷰를 통해 이 부분에 대한 생각이 바뀌게 되었다.

랜덤하게 생성된 번호를 어떻게 테스트 할지가 고민이였는데, 이때 인터페이스를 활용함으로써 결합도를 낮춰 랜덤 번호 테스트를 할 수 있다는 것을 배울 수 있었다.

번호 생성 인터페이스를 구현하는 구현체가 랜덤 번호 생성 로직을 갖도록 하고

랜덤한 번호 값을 통해 특정 기능을 수행하는 메서드가 해당 인터페이스를 파라미터로 받아 기능을 수행하도록 구현한다면

해당 기능을 테스트할 때 테스트용 인터페이스 구현체(Mock 같이)를 만들어 특정 값을 주었을 때 원하는 기능을 수행하는 지 테스트 할 수 있었다.

 


아쉬웠던 점

 

검증의 책임 분리

2주차는 정말 많은 예외 상황을 고려해 검증하도록 했다.

하지만 이 많은 예외 검증을 전부 InputView 클래스가 입력 시점에 전부 담당하고 있어,

도메인은 어떠한 정책이 있는지 알 수가 없었다.

3주차 미션에는 이 검증에 대한 책임을 입력값 형식 검증은 입력 시점에 검증

도메인 객체에 대한 정책은 도메인 객체 생성 시점에 검증 하도록 분리해보려 한다.

 

객체로 관리할 수 있는 로직은 객체를 만들어 관리

나름대로 각 클래스들이 자신만의 역할에 충실한지, 하나의 메서드는 한 가지 기능만을 담당하는지에 대해

다시 한번 훑어 보면서 검토해보았었다.

하지만 객체가 담당할 수 있는 로직을 특정 객체로 만들어 관리할 생각은 해보지 못했던것 같다.

이번 미션에서는 객체가 관리할 수 있는 로직을 객체가 관리하도록 해 세세하게 클래스를 분리해보려 한다. 

 

메서드 네이밍

번역기를 돌려가며 어떻게 하면 이름을 통해 의도를 전달할 수 있을까에 대해 많이 신경을 썼었다.

하지만 일부 메서드들은 이름을 통해 의도가 전달 되기 보다는 의도가 혼동될 수 있었던 메서드들이 있었다.

참 적절한 메서드 이름을 정한다는 건 쉬운일이 아닌것 같다.. 😂

전체 리팩토링 후에 다시 한번 꼼꼼히 검토해 의도가 잘 전달되는지 검토해보아야겠다.