ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,48 @@
+package racingcar.model;
+
+import java.util.Objects;
+
+public class Position implements Comparable<Position> {
+
+    private static final int STARTING_POSITION = 0;
+
+    private int position;
+
+    public Position(int position) {
+        this.position = position;
+    }
+
+    public static Position from(int position) {
+        return new Position(position);
+    }
+
+    public static Position init() {
+        return new Position(STARTING_POSITION);
+    }
+
+    public void increase() {
+        position++;
+    }
+
+    public int getPosition() {
+        return position;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Position position1 = (Position) o;
+        return position == position1.position;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(position);
+    }
+
+    @Override
+    public int compareTo(Position other) {
+        return this.position - other.position;
+    }
+}

리뷰: 말씀해주신 대로 현재 미션에서는 굳이 일급 객체화할 필요가 없다는 것에 동의합니다.
나열해주신 근거들도 모두 타당하다고 생각해요..!

다만 미션을 진행하면서 최대한 객체지향 생활 체조 원칙들을 지키면서 구현해보는 것을 목표로 하고 있어요. (객체지향 생활 체조 원칙에 **모든 원시 값과 문자열을 포장한다.** 라는 항목이 있더라구요!) 일종의 연습이라고 봐주시면 될 것 같습니다! 원시 값을 포장하는 것이 추후에 요구사항이 변했을 때 유지 보수에도 큰 이점이 있다는 것을 고려해서 억지로라도 Wrapping 하려고 노력하고 있습니다...

- [원시 타입을 포장해야 하는 이유](https://tecoble.techcourse.co.kr/post/2020-05-29-wrap-primitive-type/)
@@ -0,0 +1,6 @@
+package racingcar;
+
+public interface RacingNumberGenerator {
+
+    int generate();
+}

리뷰: 와 좋은 자료 감사합니다! 최근에 람다를 학습하면서 함수형 인터페이스도 함께 공부했었는데
직접 활용해볼 생각은 못해봤네요. 지금 당장 리팩토링 해봐야겠습니다! 감사합니다!!
@@ -0,0 +1,57 @@
+package racingcar.view;
+
+import racingcar.model.Car;
+import racingcar.model.CarName;
+import racingcar.model.Cars;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class OutputView {
+
+    private static final String NEW_LINE = "\n";
+    private static final String SCORE_UNIT = "-";
+    private static final String SCORE_BOARD = "%s : %s";
+    private static final String WINNER_SEPARATOR = ", ";
+    private static final String WINNERS_ARE = "최종 우승자 : ";
+    private static final String ERROR_PREFIX = "[ERROR] ";
+    private static final String RESULT_INTRO = "실행 결과";
+
+    public void printResult(Cars cars) {
+        System.out.println(getScore(cars));
+    }
+
+    private String getScore(Cars cars) {
+        return cars.get().stream()
+                .map(this::toScoreBoard)
+                .collect(Collectors.joining(NEW_LINE));
+    }
+
+    private String toScoreBoard(Car car) {
+        return String.format(SCORE_BOARD, car.getName(), convertToScore(car.getPosition()));
+    }
+
+    private String convertToScore(int position) {
+        return SCORE_UNIT.repeat(position);
+    }
+
+    public void printWinners(List<CarName> winners) {
+        System.out.print(WINNERS_ARE);
+        System.out.println(getWinnerNames(winners));
+    }
+
+    private String getWinnerNames(List<CarName> winners) {
+        return winners.stream()
+                .map(CarName::get)
+                .collect(Collectors.joining(WINNER_SEPARATOR));
+    }
+
+    public void printError(IllegalArgumentException error) {
+        System.out.print(ERROR_PREFIX);
+        System.out.println(error.getMessage());
+    }
+
+    public void printRaceIntro() {
+        System.out.println(RESULT_INTRO);
+    }
+}

리뷰: 이 부분은 메세지 부분이라 view에서 처리하신 것 같은데, 이 경우 `cars`가 직접 처리하는 것이 아닌 직접 꺼내와서 처리하는 것인 것 같아서 차라리 메시지를 `cars`에서 처리하는 게 더 객체를 독립적으로 만드는 게 아닐까 하는 생각이 드네요!
@@ -0,0 +1,37 @@
+package racingcar.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import racingcar.model.AttemptCount;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InputView {
+
+    private static final String SPLIT_REGEX = ",";
+    private static final String NON_NUMERIC_INPUT_MESSAGE = "숫자만 입력할 수 있습니다.";
+    private static final String REQUEST_ATTEMPT_COUNT_MESSAGE = "시도할 회수는 몇회인가요?";
+
+    public List<String> readNames() {
+        String names = Console.readLine();
+
+        return Arrays.stream(names.split(SPLIT_REGEX))
+                .collect(Collectors.toList());
+    }
+
+    public AttemptCount readAttemptCount() {
+        System.out.println(REQUEST_ATTEMPT_COUNT_MESSAGE);
+        String attemptCount = Console.readLine();
+
+        return AttemptCount.from(parseInt(attemptCount));
+    }
+
+    private int parseInt(String input) {
+        try {
+            return Integer.parseInt(input);
+        } catch (NumberFormatException error) {
+            throw new IllegalArgumentException(NON_NUMERIC_INPUT_MESSAGE);
+        }
+    }
+}

리뷰: 이러한 메시지들을 일일이 상수처리하면 나중에 규모가 커졌을 때, 관리하기 힘들어진다고 합니다. 재사용되지 않는 메시지들은 직접 값에 담아보는 건 어떨까요?
@@ -0,0 +1,24 @@
+package racingcar.model;
+
+public class AttemptCount {
+
+    private static final int IS_OVER = 0;
+
+    private int count;
+
+    public AttemptCount(int count) {
+        this.count = count;
+    }
+
+    public static AttemptCount from(int count) {
+        return new AttemptCount(count);
+    }
+
+    public boolean isPlayable() {
+        return count > IS_OVER;
+    }
+
+    public void decrease() {
+        count--;
+    }
+}

리뷰: 저도 많이 배워가네요 감사합니다 :D
@@ -0,0 +1,47 @@
+package racingcar.model;
+
+public class Car implements Comparable<Car> {
+    private static final int MOVABLE_LOWER_BOUND = 4;
+
+    private final CarName name;
+    private final Position position = Position.init();
+
+    private Car(String name) {
+        this.name = CarName.from(name);
+    }
+
+    public static Car from(String name) {
+        return new Car(name);
+    }
+
+    public void move(int command) {
+        if (isMovable(command)) {
+            position.increase();
+        }
+    }
+
+    private static boolean isMovable(int command) {
+        return command >= MOVABLE_LOWER_BOUND;
+    }
+
+    public boolean isAt(Position expectedPosition) {
+        return position.equals(expectedPosition);
+    }
+
+    public int getPosition() {
+        return position.getPosition();
+    }
+
+    public CarName getName() {
+        return name;
+    }
+
+    public boolean isSamePosition(Car other) {
+        return this.position.equals(other.position);
+    }
+
+    @Override
+    public int compareTo(Car other) {
+        return this.position.compareTo(other.position);
+    }
+}

리뷰: 저도 여기서 놀랐는데 덕분에 좋은 컨텐츠들을 배워가네요 감사합니다!
@@ -0,0 +1,44 @@
+package racingcar.model;
+
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import racingcar.RacingNumberGenerator;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.util.Lists.newArrayList;
+
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+public class CarsTest {
+
+    @Test
+    void 자동차들의_이름은_서로_중복될_수_없다() {
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> Cars.from(List.of("pobi,jun,pobi")));
+    }
+
+    @Test
+    void 가장_멀리_이동한_사림이_우승한다() {
+        Cars cars = Cars.from(List.of("pobi", "jun", "khj"));
+        TestRacingNumberGenerator numberGenerator = new TestRacingNumberGenerator();
+
+        cars.race(numberGenerator);
+        cars.race(numberGenerator);
+
+        assertThat(cars.findWinners())
+                .contains(CarName.from("pobi"), CarName.from("khj"));
+    }
+
+    static class TestRacingNumberGenerator implements RacingNumberGenerator {
+
+        private final List<Integer> numbers = newArrayList(4, 1, 4, 4, 1, 4);
+
+        @Override
+        public int generate() {
+            return numbers.remove(0);
+        }
+    }
+}

리뷰: 이 부분은 `RacingNumberGenerator`라고 함수형 인터페이스로 이미 정의해 두셨으니 직접 구현할 필요 없이 간단하게 람다로 처리가 가능할 것 같습니다!
@@ -0,0 +1,44 @@
+package racingcar.model;
+
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import racingcar.RacingNumberGenerator;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.util.Lists.newArrayList;
+
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+public class CarsTest {
+
+    @Test
+    void 자동차들의_이름은_서로_중복될_수_없다() {
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> Cars.from(List.of("pobi,jun,pobi")));
+    }
+
+    @Test
+    void 가장_멀리_이동한_사림이_우승한다() {
+        Cars cars = Cars.from(List.of("pobi", "jun", "khj"));
+        TestRacingNumberGenerator numberGenerator = new TestRacingNumberGenerator();
+
+        cars.race(numberGenerator);
+        cars.race(numberGenerator);
+
+        assertThat(cars.findWinners())
+                .contains(CarName.from("pobi"), CarName.from("khj"));
+    }
+
+    static class TestRacingNumberGenerator implements RacingNumberGenerator {
+
+        private final List<Integer> numbers = newArrayList(4, 1, 4, 4, 1, 4);
+
+        @Override
+        public int generate() {
+            return numbers.remove(0);
+        }
+    }
+}

리뷰: ```java
cars.race(() -> new Integer[]{4, 1, 4, 4, 1, 4})
```
이런 느낌으로요!
@@ -0,0 +1,65 @@
+package racingcar.model;
+
+import racingcar.RacingNumberGenerator;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class Cars {
+
+    private static final String DUPLICATED_CAR_NAMES = "자동차들의 이름은 서로 중복될 수 없습니다.";
+
+    private final List<Car> cars;
+
+    private Cars(List<String> names) {
+        validate(names);
+        cars = toCars(names);
+    }
+
+    private List<Car> toCars(List<String> names) {
+        return names.stream()
+                .map(Car::from)
+                .collect(Collectors.toList());
+    }
+
+    private void validate(List<String> names) {
+        if (isDuplicated(names)) {
+            throw new IllegalArgumentException(DUPLICATED_CAR_NAMES);
+        }
+    }
+
+    private boolean isDuplicated(List<String> names) {
+        Set<String> uniqueNames = new HashSet<>(names);
+
+        return uniqueNames.size() != names.size();
+    }
+
+    public static Cars from(List<String> names) {
+        return new Cars(names);
+    }
+
+    public void race(RacingNumberGenerator numberGenerator) {
+        cars.forEach(car -> car.move(numberGenerator.generate()));
+    }
+
+    public List<CarName> findWinners() {
+        Car maxPositionCar = getCarWithMaxPosition();
+        return cars.stream()
+                .filter(maxPositionCar::isSamePosition)
+                .map(Car::getName)
+                .collect(Collectors.toList());
+    }
+
+    private Car getCarWithMaxPosition() {
+        return cars.stream()
+                .max(Car::compareTo)
+                .orElseThrow(() -> new IllegalArgumentException("차량 리스트가 비어있습니다."));
+    }
+
+    public List<Car> get() {
+        return Collections.unmodifiableList(cars);
+    }
+}

리뷰: 숫자 생성하는 부분을 생성자가 아닌 `race`에서 주입받을 수도 있네요! 하나 배워갑니다
@@ -0,0 +1,44 @@
+package racingcar.model;
+
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import racingcar.RacingNumberGenerator;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.util.Lists.newArrayList;
+
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+public class CarsTest {
+
+    @Test
+    void 자동차들의_이름은_서로_중복될_수_없다() {
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> Cars.from(List.of("pobi,jun,pobi")));
+    }
+
+    @Test
+    void 가장_멀리_이동한_사림이_우승한다() {
+        Cars cars = Cars.from(List.of("pobi", "jun", "khj"));
+        TestRacingNumberGenerator numberGenerator = new TestRacingNumberGenerator();
+
+        cars.race(numberGenerator);
+        cars.race(numberGenerator);
+
+        assertThat(cars.findWinners())
+                .contains(CarName.from("pobi"), CarName.from("khj"));
+    }
+
+    static class TestRacingNumberGenerator implements RacingNumberGenerator {
+
+        private final List<Integer> numbers = newArrayList(4, 1, 4, 4, 1, 4);
+
+        @Override
+        public int generate() {
+            return numbers.remove(0);
+        }
+    }
+}

리뷰: 저도 피드백 받고 난 뒤 람다로 표현해보려고 했는데  `TestRacingNumberGenerator` 처럼 내부에 상태가 필요한 경우엔 온전히 람다로 표현하는게 힘들더라구요!

만약 람다로 처리하려면 

```java
cars.race(() -> newArrayList(4, 1, 4, 4, 1, 4).remove(0));
```

과 같은 형태로 사용해야 하는데 이렇게 되면 일단 가독성이 너무 떨어지더라구요.

무엇보다도 `race()` 를 여러 번 반복하더라도 항상 *List*가 초기화 되기 때문에 항상 같은 값만 반복하게 되기도 하구요.

```java
cars.race(() -> newArrayList(4, 1, 4, 4, 1, 4).remove(0)); // 4 반환
cars.race(() -> newArrayList(4, 1, 4, 4, 1, 4).remove(0)); // 4 반환
```

[Functional Interface 란?](https://tecoble.techcourse.co.kr/post/2020-07-17-Functional-Interface/) 을 읽어보니 다음과 같은 대목이 있더라구요!

```
함수형 개발 방식은 행위에 해당하는 부분도 값으로 취급이 가능해 졌다는 것인데 
자바에서 의미하는 기본형의 데이터(Integer 나 String)만 값이 아니라 행위(로직)도 
값으로 취급할 수 있게 되었다는 이야기입니다. 

이것은 자바가 코드의 재활용 단위가 클래스였던 것이 함수 단위로 재사용이 가능해 지면서 
조금 더 개발을 유연하게 할 수 있게 된 점이라고 할 수 있습니다. 
```

함수형 인터페이스의 대상이 되는 것은 
클래스에 종속 되어있는 (제가 느끼기엔 상태와 묶여있는) **메서드**가 아니라
오로지 행위 만을 다루고 있는 **함수** 뿐인 것 같습니다.

(그러고 보니 이름도 **함수형** 인터페이스네요...)

분명 전에 함수형 인터페이스를 학습했었는데 함수와 메서드의 차이를 생각하지 않고 사용하려고 했네요 흑...
서경님 덕분에 저도 함수형 인터페이스에 대해서 좀 더 명료하게 이해하게 되는 계기가 되었습니다.
좋은 지적 감사드려요!!! :)
@@ -0,0 +1,57 @@
+package racingcar.view;
+
+import racingcar.model.Car;
+import racingcar.model.CarName;
+import racingcar.model.Cars;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class OutputView {
+
+    private static final String NEW_LINE = "\n";
+    private static final String SCORE_UNIT = "-";
+    private static final String SCORE_BOARD = "%s : %s";
+    private static final String WINNER_SEPARATOR = ", ";
+    private static final String WINNERS_ARE = "최종 우승자 : ";
+    private static final String ERROR_PREFIX = "[ERROR] ";
+    private static final String RESULT_INTRO = "실행 결과";
+
+    public void printResult(Cars cars) {
+        System.out.println(getScore(cars));
+    }
+
+    private String getScore(Cars cars) {
+        return cars.get().stream()
+                .map(this::toScoreBoard)
+                .collect(Collectors.joining(NEW_LINE));
+    }
+
+    private String toScoreBoard(Car car) {
+        return String.format(SCORE_BOARD, car.getName(), convertToScore(car.getPosition()));
+    }
+
+    private String convertToScore(int position) {
+        return SCORE_UNIT.repeat(position);
+    }
+
+    public void printWinners(List<CarName> winners) {
+        System.out.print(WINNERS_ARE);
+        System.out.println(getWinnerNames(winners));
+    }
+
+    private String getWinnerNames(List<CarName> winners) {
+        return winners.stream()
+                .map(CarName::get)
+                .collect(Collectors.joining(WINNER_SEPARATOR));
+    }
+
+    public void printError(IllegalArgumentException error) {
+        System.out.print(ERROR_PREFIX);
+        System.out.println(error.getMessage());
+    }
+
+    public void printRaceIntro() {
+        System.out.println(RESULT_INTRO);
+    }
+}

리뷰: 말씀해주신 대로 *UI* 를 다루는 로직을 도메인 내부에서 처리하는 것이 마음에 걸리더라구요...
*UI* 요구사항이 변하면 도메인 내부에도 변화가 강제된다 는 게 오히려 객체 독립성을 해친다고 생각했습니다.

그래서 객체를 직접 꺼내오는 대신 다음과 같이 불변 객체로 리스트를 반환하도록 처리해주었습니다.

```java
public List<Car> get() {
    return Collections.unmodifiableList(cars);
}
```

그래도 역시 일급 컬렉션 내의 상태를 직접 꺼내와서 사용하는 게 너무 찝찝하긴 하더라구요...

제 개인적인 생각을 더 자세하게 말씀드리겠습니다. 
저는 *UI* 로직을 도메인에 통합하는 것 보다는 `getter()` 를 사용하는 것이 오히려 더 낫다고 판단했습니다.
미션을 진행하면서 저는 `getter()`를 무조건적으로 배제하는게 너무 힘들더라구요...
두 객체를 비교하거나, 진위 여부를 판단하는 로직에서는 메시지만으로도 충분히 구현할 수 있었지만,
두 객체의 값으로 연산을 해야 한다거나, 객체 내부의 값을 출력해야 하는 경우엔 `getter()`를 사용하는 게
오히려 더 효율적이라고 생각했습니다. 

그래서 `getter()`를 최대한 지양하는 이유 즉, 캡슐화를 위반하진 않는지, 불변성을 해치진 않는지를 계속 의식하면서 안전하게 `getter()`를 사용해보려고 노력하고 있습니다.

혹시 서경님은 `getter()`를 사용하는 것 보다 UI 로직을 도메인 내부에서 구현하는 것 이 더 합리적이라고 생각하신
또 다른 이유가 있나요? 궁금합니다!
@@ -0,0 +1,37 @@
+package racingcar.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import racingcar.model.AttemptCount;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InputView {
+
+    private static final String SPLIT_REGEX = ",";
+    private static final String NON_NUMERIC_INPUT_MESSAGE = "숫자만 입력할 수 있습니다.";
+    private static final String REQUEST_ATTEMPT_COUNT_MESSAGE = "시도할 회수는 몇회인가요?";
+
+    public List<String> readNames() {
+        String names = Console.readLine();
+
+        return Arrays.stream(names.split(SPLIT_REGEX))
+                .collect(Collectors.toList());
+    }
+
+    public AttemptCount readAttemptCount() {
+        System.out.println(REQUEST_ATTEMPT_COUNT_MESSAGE);
+        String attemptCount = Console.readLine();
+
+        return AttemptCount.from(parseInt(attemptCount));
+    }
+
+    private int parseInt(String input) {
+        try {
+            return Integer.parseInt(input);
+        } catch (NumberFormatException error) {
+            throw new IllegalArgumentException(NON_NUMERIC_INPUT_MESSAGE);
+        }
+    }
+}

리뷰: 제가 생각했을 때 상수를 사용하는 이유에는 
- 클래스 간, 메서드 간 값을 재사용하는 측면도 있지만 
- **인스턴스 간에 값을 공유**하는 측면도 있다고 알고 있습니다!
- (이름을 지어줌으로써 가독성을 고려하는 측면도 있구요!)

`InputView` 에서 사용되는 에러 메시지들 같은 경우 인스턴스의 상태와 무관하게 항상 값이 고정되어 있습니다.
이런 경우 하드코딩된 값을 `static final` 로 선언해주면 프로그램 실행 단계에서 딱 한번만 초기화되기 때문에
서로 다른 인스턴스를 여러 번 호출하더라도 인스턴스와 함께 매번 초기화해 줄 필요가 없어지게 됩니다. 
그래서 메모리 적으로도 더 효율적이라고 생각해요!

(숫자 야구 피드백 영상에서 클래스 변수와 인스턴스 변수의 차이를 떠올리시면 될 것 같아요!!)

이러한 이유에서 저는 하드코딩된 값은 무조건 상수 처리 해버리자! 라는 주의로 프로그래밍 하고있습니다.
자동차 경주 미션에서는 `InputView` 가 여러번 초기화 될 가능성이 거의 없지만 그렇다 하더라도
하드코딩 된 값을 최대한 상수로 처리하는 습관을 들이려고 노력이라고 이해해주시면 될 것 같아요!

혹여나 규모가 커진다면 `InputView` 클래스를 세부 클래스들로 나누는 것도 괜찮은 방법일 것 같아요!
그렇게 되면 클래스가 관련된 상수를 함께 관리할 수 있기 때문에 유지보수 측면에서도 더 유리하다고 생각합니다!
@@ -0,0 +1,37 @@
+package racingcar.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import racingcar.model.AttemptCount;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InputView {
+
+    private static final String SPLIT_REGEX = ",";
+    private static final String NON_NUMERIC_INPUT_MESSAGE = "숫자만 입력할 수 있습니다.";
+    private static final String REQUEST_ATTEMPT_COUNT_MESSAGE = "시도할 회수는 몇회인가요?";
+
+    public List<String> readNames() {
+        String names = Console.readLine();
+
+        return Arrays.stream(names.split(SPLIT_REGEX))
+                .collect(Collectors.toList());
+    }
+
+    public AttemptCount readAttemptCount() {
+        System.out.println(REQUEST_ATTEMPT_COUNT_MESSAGE);
+        String attemptCount = Console.readLine();
+
+        return AttemptCount.from(parseInt(attemptCount));
+    }
+
+    private int parseInt(String input) {
+        try {
+            return Integer.parseInt(input);
+        } catch (NumberFormatException error) {
+            throw new IllegalArgumentException(NON_NUMERIC_INPUT_MESSAGE);
+        }
+    }
+}

리뷰: 오호 좋은 습관인 것 같아요 ㅎㅎ 하드코딩된 값을 사용하지 않다가 사용하는 건 일일이 의식하기 힘들지만, 상수를 사용하는 식으로 코딩하다가 값을 직접 쓰는 방식으로 전환하기 쉬울 테니까요

이러한 피드백을 드린 이유는 프리코스 외에 인턴했던 회사에서 `예외 메세지에 상수 값을 직접 다 받을 경우 상수를 무분별하게 많이 사용하게 돼서 코드를 읽을 때 정작 무엇이 중요한지 판별하기 힘들어지고 코드가 길어진다는 문제점이 발생한다` 라는 피드백을 받았기 때문인데요

말씀하신 것처럼 연습의 과정이라면 오히려 좋은 신호겠네요! 😃
@@ -0,0 +1,44 @@
+package racingcar.model;
+
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import racingcar.RacingNumberGenerator;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.util.Lists.newArrayList;
+
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+public class CarsTest {
+
+    @Test
+    void 자동차들의_이름은_서로_중복될_수_없다() {
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> Cars.from(List.of("pobi,jun,pobi")));
+    }
+
+    @Test
+    void 가장_멀리_이동한_사림이_우승한다() {
+        Cars cars = Cars.from(List.of("pobi", "jun", "khj"));
+        TestRacingNumberGenerator numberGenerator = new TestRacingNumberGenerator();
+
+        cars.race(numberGenerator);
+        cars.race(numberGenerator);
+
+        assertThat(cars.findWinners())
+                .contains(CarName.from("pobi"), CarName.from("khj"));
+    }
+
+    static class TestRacingNumberGenerator implements RacingNumberGenerator {
+
+        private final List<Integer> numbers = newArrayList(4, 1, 4, 4, 1, 4);
+
+        @Override
+        public int generate() {
+            return numbers.remove(0);
+        }
+    }
+}

리뷰: 그렇군요! 사실 이 부분은 취향이라 더 간단하게 표현이 가능할 것 같아서 말씀드린 부분이었습니다 ㅎㅎ

저같은 경우에는 js만 사용하다가 이번 프리코스에서 자바를 시작했는데 오히려 메서드적 사고 하는게 힘들더라구요 왜이렇게 지원 안 되는 부분들이 많은지...ㅠㅠ

첨부해주신 내용에 대해 덧붙이자면 저 설명에서 말하고 있는건 함수형 인터페이스를 사용하게 되면 인자로 함수를 넘길 수 있게 된다는 걸 말하는 것 같습니다. (js처럼요!) 이런 특징을 `일급함수`라고 부르는데요, 이 일급함수는 함수형 프로그래밍 수행하기 위한 최소한의 조건이 됩니다.

따라서, 함수형 인터페이스를 넘기면 사실은 익명클래스지만 함수를 넘기는 것처럼 만들어 함수형 프로그래밍을 흉내내는 방식의 코드를 짤 수 있다! 라는거죠
@@ -0,0 +1,57 @@
+package racingcar.view;
+
+import racingcar.model.Car;
+import racingcar.model.CarName;
+import racingcar.model.Cars;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class OutputView {
+
+    private static final String NEW_LINE = "\n";
+    private static final String SCORE_UNIT = "-";
+    private static final String SCORE_BOARD = "%s : %s";
+    private static final String WINNER_SEPARATOR = ", ";
+    private static final String WINNERS_ARE = "최종 우승자 : ";
+    private static final String ERROR_PREFIX = "[ERROR] ";
+    private static final String RESULT_INTRO = "실행 결과";
+
+    public void printResult(Cars cars) {
+        System.out.println(getScore(cars));
+    }
+
+    private String getScore(Cars cars) {
+        return cars.get().stream()
+                .map(this::toScoreBoard)
+                .collect(Collectors.joining(NEW_LINE));
+    }
+
+    private String toScoreBoard(Car car) {
+        return String.format(SCORE_BOARD, car.getName(), convertToScore(car.getPosition()));
+    }
+
+    private String convertToScore(int position) {
+        return SCORE_UNIT.repeat(position);
+    }
+
+    public void printWinners(List<CarName> winners) {
+        System.out.print(WINNERS_ARE);
+        System.out.println(getWinnerNames(winners));
+    }
+
+    private String getWinnerNames(List<CarName> winners) {
+        return winners.stream()
+                .map(CarName::get)
+                .collect(Collectors.joining(WINNER_SEPARATOR));
+    }
+
+    public void printError(IllegalArgumentException error) {
+        System.out.print(ERROR_PREFIX);
+        System.out.println(error.getMessage());
+    }
+
+    public void printRaceIntro() {
+        System.out.println(RESULT_INTRO);
+    }
+}

리뷰: 음 우선은 저는 메세지를 만드는 부분 자체를 UI 로직이라고 생각하기 보다는 도메인에서 해결 가능한 영역이라고 생각했습니다. 왜냐하면 이미 `Car`에서 해당 상태에 대해 모든 걸 알고 있는데 이를 굳이 밖으로 꺼낸다면 캡슐화가 깨진다고 생각했거든요. 그런데 말씀해주신 내용을 읽어보니 이를 UI라고 생각하면 애초에 다른 영역으로 던져주는 것이기 때문에 그대로 꺼낸다고 해서 캡슐화가 깨지는 건 아닌가? 하는 생각도 들고 어렵네요 ㅎㅎ

사실 저도 `getter`를 무조건적으로 배제하는 것은 옳지 않다고 생각하는 주의기도 하구요! 다 필요하니까 쓰는 것인데 하는 생각도 들고요 :D 남겨주신 의견 모두 즐거이 잘 들었습니다 ㅎㅎㅎ 다시 생각해볼 수 있는 계기도 되었구요