[우테코 프리코스] 2주차 미션 - 자동차 경주

haaaalin·2023년 11월 1일

우아한 테크코스

목록 보기
1/1
post-thumbnail

2주차 미션

https://github.com/woowacourse-precourse/java-racingcar-6

🍀 이번 미션 목표
1주 차에서 학습한 것에 더해, 함수를 분리하고, 각 함수별로 테스트를 작성하는 것에 익숙해지는 것이 목표

저번 1주차 미션은 감을 잡는 미션이었다면, 이번엔 더 깊이 본격적으로 설계해보는 것을 권장하는 미션이었다. 이번엔 테스트 코드도 추가로 작성해야 했기 때문에 여러 가지 신경 쓸 것도 많았다.

미션 수행

2주차까지 수행해보니, 우아한 테크코스는 단순히 과제 요구 사항을 해결하기 위한 코드 작성을 원하는 것이 아닌, 개발에 필요한 뿌리를 심어주기 위한 과제처럼 느껴졌다.

따로 다른 지원자와 소통하며 성장할 수 있도록 Discord 서버를 제공하고, 서로 리뷰하기, 토론하기, 함께 나누기, 다시 돌아보기 등과 같은 활동을 권장하고 있다. 모두 고통 속에서도 같이 공유하며 즐겁게 성장할 수 있도록 과제, 커뮤니티 모두에서 우테코 팀의 방향성이 보이는 것 같았다.

개발의 즐거움을 맛볼 수 있도록 설계해준 느낌이 강하게 난다..!

기능 목록 작성

이번엔 아래와 같은 추가 요구사항이 있었다. 따라서 기능 목록=함수 목록 이 될 수 있도록 최대한 쪼개서 작성하려 노력했다.

막상 개발을 시작해보니 더 쪼개지고 있는 기능과 코드들을 발견했지만..

다음에 비슷한 과제가 나온다면 코드를 작성하고 있다 생각하고 기능을 쪼개볼 생각이다.

MVC 패턴..

이번엔 MVC 패턴을 제대로 적용해보고자 아래 우테코 3기 제리님의 테코톡 영상을 시청했다.

1주차 때는 너무 많은 생각을 하느라, 메서드 하나 작성할 때에도 굉장히 오래 걸렸다. “이렇게 짜면.. 이런 점이 걸리는데..? 그렇다고 이러기엔…” 하며 갈팡질팡 코딩을 했던 것 같다.

오히려 이번엔 너무 많은 생각을 버리고, 딱 MVC 패턴을 위한 5가지 규칙(영상에서 언급)과 함수를 최대한 쪼개자 두 가지에만 집중해 코드를 짰고, 코드 작성 과정에서 맞닥뜨리는 갈림길에서 쉽게 판단할 수 있었다.

1️⃣ 재사용할 수 있는 InputView

코드를 짜면서 염두에 뒀던 점은 바로 재사용할 수 있는 기능은 웬만하면 재사용할 수 있도록 최대한 분리된 상태로 만드는 것이었다.

따라서 Output은 몰라도, Input은 다른 기능에서도 유사한 Input이 존재할거라 판단했고 InputView 역할을 담당하는 InputManager 는 다른 곳에서도 쓰일 수 있도록 도메인 의존적인 함수명이나 변수명을 쓰지 않으려 노력했다.

public class InputManager {
    public List<String> getStringListSplitByComma() {
        String input = this.readLine();
        return new ArrayList<>(Arrays.asList(input.split(",")));
    }

    public int getOnePositiveNumber() {
        String input = this.readLine();
        if (!InputVerifier.isPositiveInteger(input)) {
            throw new IllegalArgumentException();
        }
        return Integer.parseInt(input);
    }

		//... (생략)
}

2️⃣ 클라이언트에게 제공하는 GameManager

클라이언트에게 게임을 제공하는 클래스인 GameManager 를 따로 생성했다.

클라이언트는 자신이 원하는 게임의 controller를 생성해 GameManager 객체 생성 시 주입하면, 해당 게임을 실행할 수 있다.

물론 GameManager 클래스를 따로 두지 않고, GameController를 클라이언트에게 제공할 수 있었다. 하지만, GameController를 생성한 목적은 게임이라는 기능을 하는 클래스를 작성할 때 필요한 기능을 설계하기 위한 목적으로 Interface 타입이었고, 따라서 GameController를 따로 주입 받는 GameManager 클래스를 정의하는 결정을 내렸다.

public interface GameController {
    void setUp();

    void playOneTurn();

    void playGame();

    void showResult();

    void gameOver();
}

3️⃣ 게임 설정 데이터를 가지고 있는 Model

레이싱 게임에는 클라이언트가 설정하지 않는 내부 설정 값과 게임이 시작된 후에 클라이언트의 입력을 받아 설정되는 설정 값이 존재하고, 게임이 진행되는 동안 이 값들을 저장하고 있어야 했다.

모든 내부 설정 값은 변경이 되더라도 한 번에 바꿀 수 있도록 static final 타입을 이용해 각 도메인에 해당된다고 생각하는 클래스의 필드로 넣었다.

public class Car {
    public static final int RANDOM_NUMBER_RANGE_START = 0;
    public static final int RANDOM_NUMBER_RANGE_END = 9;
    private static final int MIN_MOVEMENT_THRESHOLD = 4;

    private static final String CAR_MARK = "-";
		
		//... (생략)
}

더 작게, 하나의 기능만

사실 토이 프로젝트 개발을 하다보면, 하나의 기능만 하도록 함수를 극단적으로 분리하기가 쉽지 않다.. 😦

1️⃣ 함수로 이루어져 있는 함수

사실 getCarNames , 메서드명만 보면 하나의 기능을 하고 있는 함수지만, 진짜 정말 최소의 기능만 할 수 있도록 모두 분리시키려 노력했다.

아래와 같은 함수들로 이루어져 있다.

  • 자동차 이름을 입력하도록 요청하는 메시지를 출력하는 함수
  • 사용자의 입력을 받아, 쉼표를 기준으로 분리하는 함수
  • 입력 받은 자동차 이름들이 유효한 지 검증하는 함수
public List<String> getCarNames() {
        outputManager.printRequestCarNameInputMessage();
        List<String> carNames = inputManager.getStringListSplitByComma();
        checkCarNameIsValid(carNames);
        return carNames;
}

2️⃣ stream을 활용하자

괜히 코드 길이만 길어지게 하고, 가독성을 떨어지게 하는 반복문보다 stream을 활용할 수 있도록 노력했다. stream을 사용하니, 코드는 짧아졌지만 오히려 어떤 기능을 하는 코드인지 한 눈에 들어오는 것 같았다.

public List<String> getAllCarsTrail() {
        return cars.stream().map(Car::getCarTrail).toList();
}

// ----------------

public List<Car> findWinners() {
        int maxPosition = cars.stream().mapToInt(Car::getPosition).max().orElse(0);
        return cars.stream().filter(car -> car.getPosition() == maxPosition).toList();
}

stream은 제공하는 기능이 많다보니, 제대로 공부하고 1000% 활용할 수 있도록 각 잡고 공부해야겠다.

테스트 코드

테스트 케이스 같은 경우는 게임 흐름을 생각하며 작성해보니 수월했다. 예를 들어,

자동차 입력을 받을 때의 기능들을 떠올려 테스트 케이스 생각하고, 테스트 코드 짜고, 다시 다음 흐름에서 필요한 기능 생각하고, 이 과정을 반복했다.

1️⃣ given, when, then

given, when, then은 전에 김영한님의 스프링 강의를 들었을 때부터 사용하던 방식이다. 단 3줄짜리의 주석이지만, 테스트 코드 작성시 큰 도움을 받을 수 있었다.

@Test
void 여러_문자열을_쉼표를_기준으로_나누어_저장() {
    // given
    String name1 = "pobi";
    String name2 = "woni";
    String name3 = "harry";
    List<String> names = List.of(name1, name2, name3);

    // when
    systemIn(String.join(",", name1, name2, name3));
    List<String> result = inputManager.getStringListSplitByComma();

    // then
    assertThat(result).isEqualTo(names);
}

2️⃣ 주객전도, 테스트 코드

서비스 코드를 테스트하기 위해 테스트 코드를 작성하는 건데, 이상하게 테스트 코드를 위해 서비스 코드를 수정하고 있었다.

원래는 Car 클래스 안에 자동차 이름의 list를 받아, Car list를 반환하는 정적 팩토리 메서드만 있었지만, 아무래도 테스트할 때는 Car 객체를 따로 하나씩 생성해야 할 부분이 생겨, Car 객체 하나만 생성할 수 있는 정적 팩토리 메서드를 추가했다.

public static List<Car> createCars(List<String> carNames) {
        return carNames.stream().map(Car::new).toList();
}

서비스 코드에서는 활용되지 않는 코드를 추가해, 찝찝하지만 테스트 코드를 위해 어쩔 수 없는 선택이었다고 생각이 들지만, 다음에는 더 나은 선택을 위해, 이번 주차 다른 지원자의 회고를 보며 어떻게 생각하는 지 봐야겠다. 👀

profile
한 걸음 한 걸음 쌓아가자😎

0개의 댓글