[우아한테크코스 6기] 프리코스 2주차 회고 && 최종 리팩토링 후기

조경찬 (Jo Gyungchan)·2023년 11월 22일
0

우아한테크코스

목록 보기
2/4
post-thumbnail

이 글은 프리코스 2주차 회고 글, 그리고 프리코스가 끝난 뒤 진행한 리팩토링 결과까지 포함하고 있다는 것을 미리 말씀드립니다!! 😀
리팩토링 후 달라진 점도 궁금하신 분들을 끝까지 다 봐주세요!~🧐🧐


2주 차 미션에 대한 글을 작성하기 이전에 먼저 1주 차 공통 피드백 내용들은 다음과 같습니다.

  • 요구사항을 정확히 준수한다
  • 커밋 메시지를 의미 있게 작성한다
  • git을 통해 관리할 자원에 대해서도 고려한다
  • Pull Request를 보내기 전 브랜치를 확인한다
  • PR을 한 번 작성했다면 닫지 말고 추가 커밋을 한다
  • 이름을 통해 의도를 드러낸다
  • 축약하지 않는다
  • 공백도 코딩 컨벤션이다
  • 공백 라인을 의미 있게 사용한다
  • space와 tab을 혼용하지 않는다
  • 의미 없는 주석을 달지 않는다
  • IDE의 코드 자동 정렬 기능을 활용한다
  • Java에서 제공하는 API를 적극 활용한다
  • 배열 대신 Java Collection을 사용한다

1주 차 공통 피드백에 대해 자세히 보고 싶은 분은 1주 차 공통 피드백에서 볼 수 있습니다.

2주 차 미션을 구현하기 이전 1주 차 공통 피드백 내용들을 준수할 수 있도록 노력했습니다.

2주차


자동차 경주

2주차 미션은 자동차 경주를 구현하는 것이었습니다.
미션을 진행하는 방식은 다음과 같이 나와있었습니다.

🔍 진행 방식

  • 미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.
  • 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
  • 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

요구 사항 분석


1주 차 공통 피드백에도 나와있듯이, 요구 사항을 정확히 준수하기 위해 노력했습니다.

미션을 진행하기 전에 요구 사항을 꼼꼼히 분석하고 이를 토대로 구현할 기능 목록을 작성하였습니다.

## 🚀 기능 요구 사항

## Converter
- [x] 입력된 문자열을 쉼표(,)를 기준으로 분리할 수 있다.

## RandomNumberGenerator
- [x] 0에서 9사이의 무작위 값을 하나 생성한다.

## Name
- [x] 자동차의 이름을 부여할 수 있다.
  - [x] **[예외 처리]** 자동차의 이름이 1자 미만이거나 5자를 초과하면 예외가 발생한다.

## Position
- [x] 자동차의 위치를 알 수 있다.
  - [x] **[예외 처리]** 자동차의 위치가 0보다 작다면 예외가 발생한다.

## Car
- [x] 자동차는 전진할 수 있다.
  - [x] 입력된 값이 4이상 9이하일 경우 전진할 수 있다.
  - [x] 입력된 값이 0이상 3이하일 경우 전진할 수 없다.
  - [x] **[예외 처리]** 입력된 값이 0이상 9이하가 아니라면 예외가 발생한다.

## Cars
- [x] 경주에 참여할 자동차들의 정보를 알 수 있다.
  - [x] **[예외 처리]** 경주에 참여할 자동차가 없다면 예외가 발생한다.
  - [x] **[예외 처리]** 경주에 참여하는 자동차의 이름 중 중복되는 이름이 있다면 예외가 발생한다.
- [x] 경주에 참여하는 자동차들이 전진 또는 멈출 수 있다.
- [x] 경주에 참여하는 자동차들 중 가장 많이 전진한 자동차를 알 수 있다.
  - [x] 가장 많이 전진한 자동차는 여러대일 수 있다.

## RaceCount
- [x] 경주를 진행할 횟수를 알 수 있다.
  - [x] **[예외 처리]** 경주를 진행할 횟수가 1보다 작다면 예외가 발생한다.

## RacingGame
- [x] 자동차 경주를 진행할 수 있다.
  - [x] 경주를 한번 진행할때마다 진행할 횟수가 1씩 줄어든다.
- [x] 자동차 경주 게임에 대한 종료 여부를 알 수 있다.
  - [x] 경주를 진행할 횟수가 0이면 게임이 종료된다.
- [x] 자동차 경주 게임의 우승자를 알 수 있다.
  - [x] 우승자는 여러 명일 수 있다.
  - [x] **[예외 처리]** 자동차 경주 게임이 끝나지 않은 상태에서 우승자를 찾으려 하면 예외가 발생한다.

## 입력
- [x] **[공통 예외 처리]** 입력이 공백이면 예외가 발생한다.
- [x] 경주 할 자동차의 이름을 쉼표(,)를 기준으로 입력한다.
- [x] 경주를 진행할 횟수를 입력한다.
  - [x] **[예외 처리]** 경주를 진행할 횟수에 대한 입력이 숫자가 아니라면 예외가 발생한다.
- [x] 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다.

## 출력
- [x] 실행 결과 메시지를 출력한다.
- [x] 각 차수별 실행 결과를 출력한다.
  - [x] 자동차의 위치는 '-' 문자로 표시한다.
- [x] 자동차 경주 게임의 우승자를 출력한다.
  - [x] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.

이건 제가 2주 차 미션을 진행하기 이전 구현할 기능 목록을 작성한 내용들입니다.

특히 예외 상황에 대해 고민해보고 작성하려 노력했습니다.


핵심 기능

원시값 포장

저번 미션에서 원시값 포장을 하여 이점을 느낀만큼, 이번 미션에서도 원시값을 포장하려고 노력했습니다.

이번 미션에서 원시값을 포장한 한가지 예시를 작성해보겠습니다.

Car라는 객체는 각 이름위치에 대한 값을 가지고 있어야 했고, 이름은 5자 이하만 가능하다는 요구 사항이 있었습니다. 또한 위치 값은 0보다 작은 값이어서는 안된다는 것을 요구 사항을 통해 도출할 수 있었습니다.

이를 해결하기 위해, 다음과 같이 원시값을 포장하였습니다.

public class Name {
    private static final int MINIMUM_LENGTH_OF_NAME = 1;
    private static final int MAXIMUM_LENGTH_OF_NAME = 5;

    private final String name;

    public Name(String name) {
        validateName(name);
        this.name = name;
    }

    private void validateName(String name) {
        int length = name.length();

        if (length < MINIMUM_LENGTH_OF_NAME || length > MAXIMUM_LENGTH_OF_NAME) {
            throw new IllegalArgumentException(
                    String.format("자동차의 이름은 %d자 이상, %d자 이하여야 합니다.", MINIMUM_LENGTH_OF_NAME, MAXIMUM_LENGTH_OF_NAME)
            );
        }
    }
}
public class Position implements Comparable<Position> {
    private static final int MINIMUM_POSITION = 0;

    private int position;

    public Position(int position) {
        validatePosition(position);
        this.position = position;
    }

    private void validatePosition(int position) {
        if (position < MINIMUM_POSITION) {
            throw new IllegalArgumentException(
                    String.format("자동차의 위치는 %d보다 작을 수 없습니다.", MINIMUM_POSITION)
            );
        }
    }
}

이를 통해, Car 객체는 이름과 위치 값에 대한 검증 책임을 갖지 않도록 구현할 수 있었습니다.


Comparable 인터페이스 활용

이번 미션에서, 핵심적인 기능 중 하나라고 할 수 있는 우승자를 찾는 기능Comparable 인터페이스를 활용하여 구현하였습니다.

Comparable 인터페이스는 객체간의 비교를 가능하게 해주는 인터페이스이다. compareTo 메서드를 정의하여 사용 가능하다.

먼저, Car와 Position 객체에 Comparable 인터페이스의 compareTo 메서드를 정의하였습니다.

// Position 객체 내부 compareTo 메서드
@Override
public int compareTo(Position other) {
	return this.position - other.position;
}

// Car 객체 내부 compareTo 메서드
@Override
public int compareTo(Car other) {
	return position.compareTo(other.position);
}

다음과 같이 구현하여 Position의 값을 통해 객체간의 비교를 할 수 있게 만들 수 있습니다.

그래서, List<Car> 중에서 가장 큰 위치 값을 가지는 Car 객체를 아래 코드와 같이 쉽게 구현할 수 있었습니다.

private Car findMaxPositionCar() {
	return cars.stream()
		.max(Car::compareTo)
		.get();
}

Stream 활용

이번 미션에서 Stream을 최대한 사용해보려고 노력해보았습니다.

  • 중복된 이름이 있는지 판단하기 위한 기능
private boolean hasDuplicatedName(List<Car> cars) {
	return cars.stream()
		.map(Car::getName)
		.distinct()
		.count() != cars.size();
}
  • 가장 많이 전진해있는 자동차들을 찾는 기능
private List<Car> findMostMovedCars(Car maxPositionCar) {
	return cars.stream()
		.filter(car -> car.isSamePosition(maxPositionCar))
		.toList();
}
  • 가장 많이 전진해있는 자동차 하나를 찾는 기능
private Car findMaxPositionCar() {
	return cars.stream()
		.max(Car::compareTo)
		.get();
}
  • 우승자를 찾는 기능
private List<String> findWinners(List<Car> mostMovedCars) {
	return mostMovedCars.stream()
		.map(Car::getName)
		.toList();
}
  • 각 차수별 자동차들의 이동 결과 메시지를 생성하는 기능
private String generateRaceResultMessage(List<CarDto> cars) {
	return cars.stream()
		.map(this::generateMessageOf)
		.collect(Collectors.joining(NEWLINE));
}

이번 프리코스 미션을 통해 조금씩 Stream의 여러 기능을 실행해 볼 수 있어서 좋은 기회였다고 더 느낄 수 있었습니다.


테스트를 쉽게 작성하기 위한 인터페이스 활용

만약 메서드 내부에 랜덤 값을 생성하는 기능이 있고, 생성된 랜덤 값을 통해 다른 역할을 수행하는 메서드가 있다면 테스트를 어떻게 작성해야 할까요??

이번 미션에서도 경주에 참여하는 자동차들을 이동시키기 위해 랜덤 값을 생성하고 랜덤값이 4 이상일 경우 자동차들을 이동시켜야 하는 요구 사항이 있었습니다.

이를 테스트를 작성하기 쉽게 인터페이스를 활용하였습니다.

@FunctionalInterface
public interface NumberGenerator {

    int generate();
}
@Test
@DisplayName("경주에 참여하는 자동차들을 전진시킬 수 있다.")
void move() {
	cars.makeMoveOrStop(() -> 4); // 인터페이스를 활용한 부분

	assertThat(cars).extracting("cars", InstanceOfAssertFactories.list(Car.class))
			.containsExactly(
					new Car("pobi", 1),
					new Car("woni", 1),
					new Car("jun", 1));
}

@Test
@DisplayName("경주에 참여하는 자동차들을 정지시킬 수 있다.")
void stop() {
	cars.makeMoveOrStop(() -> 3); // 인터페이스를 활용한 부분

	assertThat(cars).extracting("cars", InstanceOfAssertFactories.list(Car.class))
			.containsExactly(
					new Car("pobi", 0),
					new Car("woni", 0),
					new Car("jun", 0));
}

다음과 같이 랜덤 값을 통해 테스트를 진행하는 것이 아니라, 특정 값을 통해 테스트를 진행함으로써 원하는 결과를 얻을 수 있었습니다.


테스트 코드

모든 도메인 로직에 대한 테스트 코드를 작성하기 위해 노력했습니다.

또한, 테스트 코드를 통해 여러 발생 가능한 예외 상황에 대해 미리 확인할 수 있었다는 이점을 느낄 수 있었습니다.


리팩토링

요구 사항을 토대로 모든 기능을 구현하고 난 뒤에는 리팩토링에 많은 시간을 할여하였습니다.

분리 가능한 클래스와 메서드가 있는지 판단하며 분리하기도 하였고, 각각의 네이밍이 의도가 명확히 드러나는지를 판단하여 아니라면 이름을 수정하기도 하였습니다.

또한, 1주 차 공통 피드백에도 명시된 듯이 IDE의 코드 자동 정렬 기능을 활용하였습니다.

아직까지도, 부족한 부분들이 많을 것이라 생각하고 계속해서 리팩토링을 진행할 예정입니다.

리팩토링을 진행하고 변경된 부분이 있으면 다시 블로그 글을 작성하여 나타내보겠습니다.


마치며

우아한테크코스 프리코스가 좋은 과정인가? 라는 생각을 가지는 분들이 혹시 계시다면 저는 무조건 좋다!라고 대답을 드리고 싶고, 추천드리고 싶습니다.

미션을 진행하는 과정이 너무나 재밌고, 유익하다 말씀드리고 싶습니다.

저의 자세한 코드가 궁금하신 분은 아래 링크에서 확인할 수 있습니다.
https://github.com/jcoding-play/java-racingcar-6/tree/gyungchan


최종 리팩토링 후 달라진 부분

이 부분은 프리코스가 끝나고 난 뒤 리팩토링을 진행하고 이전과 크게 달라진 점을 알려드리기 위해 작성해보았습니다!!

자동차 경주 구현 메서드

리팩토링 전엔 자동차 경주에 대한 결과를 출력하기 위해 자동차 경주 한번 시작 -> 결과 출력 -> 자동차 경주 한번 시작 -> 결과 출력 ... 과 같은 순서로 미션을 진행하였습니다.

public class MainController {
    ...
    
    private void race(RacingGame racingGame, NumberGenerator numberGenerator) {
        while (canRace(racingGame)) {
            racingGame.race(numberGenerator);

            List<CarDto> cars = toCarDto(racingGame.getCars());
            outputView.printRaceResult(cars);
        }
    }

    private boolean canRace(RacingGame racingGame) {
        return !racingGame.isGameEnd();
    }

    private List<CarDto> toCarDto(List<Car> cars) {
        Converter<List<Car>, List<CarDto>> converter = new CarToDtoConverter();
        return converter.convert(cars);
    }
    
    ...
}
public class RacingGame {
    ...

    public void race(NumberGenerator numberGenerator) {
        cars.makeMoveOrStop(numberGenerator);

        raceCount.decrease();
    }
    
    ...
}

이를 모든 자동차 경주 시작 -> 결과 출력과 같은 순서로 리팩토링 해보았습니다.

public class MainController {
    ...

    private void race(RacingGame racingGame) {
        RaceResult raceResult = racingGame.race(new RandomNumberGenerator());
        List<MovingResult> movingResults = raceResult.getMovingResults();
        
        outputView.printResult(movingResults);
    }
    
    ...
}
public class RacingGame {
    ...
    
    public RaceResult race(NumberGenerator numberGenerator) {
        List<MovingResult> movingResults = new LinkedList<>();

        while (canPlay()) {
            MovingResult movingResult = cars.makeMoveOrStop(numberGenerator);
            movingResults.add(movingResult);

            movingCount.decrease();
        }

        return new RaceResult(movingResults);
    }

	...
}

이를 위해, 자동차들이 한번 움직일 때마다 이를 MovingResult 객체에 기록하고, 전체 경주 결과를 RaceResult 객체 안에 기록하여 경주 결과를 한번에 출력할 수 있도록 리팩토링하였습니다.

큰 차이가 없다고 느끼실 수도 있겠지만, 여러 방법으로 구현해보고 이를 경험해보는 것도 조금씩 성장할 수 있는 발판을 마련해준다 생각합니다!! 😀

profile
한걸음씩 성장하는 개발자

0개의 댓글