ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,70 @@
+package bossmonster.domain;
+
+import bossmonster.domain.dto.GameHistoryDto;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GameHistory {
+
+    private class Turns {
+
+        private int turnCount;
+        private String playerName;
+        private String playerAttackType;
+        private int playerAttackDamage;
+        private int monsterAttackDamage;
+        private int playerMaxHP;
+        private int playerCurrentHP;
+        private int playerMaxMP;
+        private int playerCurrentMP;
+        private int monsterMaxHP;
+        private int monsterCurrentHP;
+        private boolean gameStatus;
+
+        public Turns(int turnCount, String playerAttackType, int playerAttackDamage,
+                     int monsterAttackDamage, boolean gameStatus, Player player, BossMonster bossMonster) {
+            this.turnCount = turnCount;
+            this.playerName = player.getName();
+            this.playerAttackType = playerAttackType;
+            this.playerAttackDamage = playerAttackDamage;
+            this.monsterAttackDamage = monsterAttackDamage;
+            this.playerMaxHP = player.getMaxHP();
+            this.playerCurrentHP = player.getCurrentHP();
+            this.playerMaxMP = player.getMaxMP();
+            this.playerCurrentMP = player.getCurrentMP();
+            this.monsterMaxHP = bossMonster.getMaxHP();
+            this.monsterCurrentHP = bossMonster.getCurrentHP();
+            this.gameStatus = gameStatus;
+        }
+    }
+
+    private static final String DEFAULT_ATTACK_TYPE = "";
+    private static final int DEFAULT_DAMAGE = 0;
+    private int recentRecord;
+    private List<Turns> raidHistory = new ArrayList<>();
+
+    public void addHistory(int turnCount, boolean gameStatus, Player player, BossMonster bossMonster) {
+        raidHistory.add(
+                new Turns(turnCount, DEFAULT_ATTACK_TYPE, DEFAULT_DAMAGE, DEFAULT_DAMAGE, gameStatus, player,
+                        bossMonster));
+    }
+
+    public void addHistory(int turnCount, String playerAttackType, int playerAttackDamage,
+                           int monsterAttackDamage, boolean gameStatus, Player player, BossMonster bossMonster) {
+        raidHistory.add(
+                new Turns(turnCount, playerAttackType, playerAttackDamage, monsterAttackDamage, gameStatus, player,
+                        bossMonster));
+    }
+
+    public GameHistoryDto requestRaidHistory() {
+        setRecentRecord();
+        Turns turns = raidHistory.get(recentRecord);
+        return new GameHistoryDto(turns.turnCount, turns.playerName, turns.playerAttackType, turns.playerAttackDamage,
+                turns.monsterAttackDamage, turns.playerMaxHP, turns.playerCurrentHP, turns.playerMaxMP,
+                turns.playerCurrentMP, turns.monsterMaxHP, turns.monsterCurrentHP, turns.gameStatus);
+    }
+
+    private void setRecentRecord() {
+        recentRecord = raidHistory.size() - 1;
+    }
+}

리뷰: 컨트롤러에서 게임을 제어하는 것 보다 RaidGame 클래스에서 전체적인 게임 로직이 돌아가는 형태로 생각하고 구현하다 보니 게임이 실행되면서 히스토리를 남기고 컨트롤러에서 출력을 위해서는 기록에 남겨진 부분을 가져다가 보여줘야 의존성이 떨어질 것 같다고 생각했습니다. 실제로 턴 제 게임을 하면 매 턴마다 턴에 대한 기록이 게임에서도 남게 되는데 이걸 생각해서 만들었던 것 같습니다!
@@ -0,0 +1,9 @@
+# 풀이 1 - Counter 이용
+from collections import Counter
+
+def solution(participant, completion):
+    # Counter를 이용하면 리스트 내의 성분의 숫자를 딕셔너리 형태로 반환해준다
+    # ex) list1 =['a', 'b', 'c', 'a'] => Counter(list1) : {'a':2, 'b':1, 'c':1}
+    not_completion = Counter(participant) - Counter(completion) # Counter의 경우 사칙연산도 가능
+     
+    return list(not_completion.keys())[0]

리뷰: `Counter`를 이용하는 방법이 굉장히 유용하군요 ! 💯
@@ -0,0 +1,18 @@
+import { useQuery } from '@tanstack/react-query';
+import { getMyGatheringJoined } from '@/axios/mypage/api';
+import { UserGatheringJoined } from '@/types/mypage.types';
+
+export const UseReviews = ({
+  reviewed,
+  offset = 0,
+  limit = 10,
+}: {
+  reviewed: boolean;
+  offset?: number;
+  limit?: number;
+}) => {
+  return useQuery<UserGatheringJoined[]>({
+    queryKey: ['myReviews', reviewed, offset, limit],
+    queryFn: () => getMyGatheringJoined({ reviewed, offset, limit }),
+  });
+};

리뷰: U 대문자 오타가 있네요.
@@ -0,0 +1,18 @@
+import { useQuery } from '@tanstack/react-query';
+import { getMyGatheringJoined } from '@/axios/mypage/api';
+import { UserGatheringJoined } from '@/types/mypage.types';
+
+export const UseReviews = ({
+  reviewed,
+  offset = 0,
+  limit = 10,
+}: {
+  reviewed: boolean;
+  offset?: number;
+  limit?: number;
+}) => {
+  return useQuery<UserGatheringJoined[]>({
+    queryKey: ['myReviews', reviewed, offset, limit],
+    queryFn: () => getMyGatheringJoined({ reviewed, offset, limit }),
+  });
+};

리뷰: 감사합니다..
@@ -0,0 +1,30 @@
+package nextstep.security.context;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class SecurityContextHolderFilter extends GenericFilterBean {
+    private final SecurityContextRepository securityContextRepository = HttpSessionSecurityContextRepository.getInstance();
+
+    @Override
+    public void doFilter(
+            ServletRequest request,
+            ServletResponse response,
+            FilterChain chain
+    ) throws IOException, ServletException {
+        try {
+            SecurityContextHolder.setContext(
+                    securityContextRepository.loadContext((HttpServletRequest) request)
+            );
+            chain.doFilter(request, response);
+        } finally {
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: chain.doFilter(request, response) 중 예외가 발생하면 
clearContext()가 호출되지 않을 수 있겠네요
 finally 블록에서 호출하도록 변경해보면 어떨까요? 😄
@@ -0,0 +1,9 @@
+package nextstep.security.exception;
+
+public class AuthenticationException extends RuntimeException {
+    public AuthenticationException() {}
+
+    public AuthenticationException(String message) {
+        super(message);
+    }
+}

리뷰: 현재 `AuthenticationException`을 싱글턴 인스턴스로 미리 생성해두고 있는데요.  
이 방식은 불필요한 객체 생성을 방지하고 일관된 예외 메시지 제공이라는 장점이 있지만, 스택 트레이스가 남지 않는다는 문제가 있습니다. 혹시 이 부분을 인지하고 계실까요? 😄
@@ -0,0 +1,24 @@
+package nextstep.security.authentication.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.provider.AuthenticationProvider;
+import nextstep.security.exception.AuthenticationProviderException;
+
+import java.util.List;
+
+public class ProviderManager implements AuthenticationManager {
+    private final List<AuthenticationProvider> providers;
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authenticationToken) {
+        return providers.stream()
+                .filter(provider -> provider.supports(authenticationToken.getClass()))
+                .findFirst()
+                .orElseThrow(AuthenticationProviderException::new)
+                .authenticate(authenticationToken);
+    }
+}

리뷰: 소소하지만 다음과 같이 개행을 하면 좀더 가독성이 좋을 것 같아요 😄 
```suggestion
    @Override
    public Authentication authenticate(Authentication authenticationToken) {
        return providers.stream()
        .filter(provider -> provider.supports(authenticationToken.getClass()))
        .findFirst()
        .orElseThrow(AuthenticationException::notSupported)
        .authenticate(authenticationToken);
    }
```
@@ -0,0 +1,9 @@
+package nextstep.security.exception;
+
+public class AuthenticationException extends RuntimeException {
+    public AuthenticationException() {}
+
+    public AuthenticationException(String message) {
+        super(message);
+    }
+}

리뷰: 앗 스택 트레이스 부분을 생각을 못했네요.
[394ed26](https://github.com/next-step/spring-security-authentication/pull/28/commits/394ed26a7c3b3dea8c17c1192cbed19393315d0f) 커밋에서 매번 새로운 객체를 생성하도록 수정했습니다.
하는 김에 커스텀 예외를 더 세분화할 수 있도록 하였습니다.
@@ -0,0 +1,24 @@
+package nextstep.security.authentication.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.provider.AuthenticationProvider;
+import nextstep.security.exception.AuthenticationProviderException;
+
+import java.util.List;
+
+public class ProviderManager implements AuthenticationManager {
+    private final List<AuthenticationProvider> providers;
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authenticationToken) {
+        return providers.stream()
+                .filter(provider -> provider.supports(authenticationToken.getClass()))
+                .findFirst()
+                .orElseThrow(AuthenticationProviderException::new)
+                .authenticate(authenticationToken);
+    }
+}

리뷰: [58614f1](https://github.com/next-step/spring-security-authentication/pull/28/commits/58614f1d4af41421b2f16d76eb1323fbf34c3bf9) 커밋에서 indentation 스타일 변경 반영했습니다.
@@ -0,0 +1,30 @@
+package nextstep.security.context;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class SecurityContextHolderFilter extends GenericFilterBean {
+    private final SecurityContextRepository securityContextRepository = HttpSessionSecurityContextRepository.getInstance();
+
+    @Override
+    public void doFilter(
+            ServletRequest request,
+            ServletResponse response,
+            FilterChain chain
+    ) throws IOException, ServletException {
+        try {
+            SecurityContextHolder.setContext(
+                    securityContextRepository.loadContext((HttpServletRequest) request)
+            );
+            chain.doFilter(request, response);
+        } finally {
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: 앗 정말로 그렇겠네요.
Filter 에서 예외처리 때문에 고생한적이 있는데, 지적해주셔서 감사합니다! ❤️ 
항상 동작해야하는 로직은 finally 문법 활용하는 습관을 가져야겠네요.
실무에서 당장 활용해야겠다는 생각이 드는 유용한 피드백 감사합니다.

[9e5ef93](https://github.com/next-step/spring-security-authentication/pull/28/commits/9e5ef934299204a6fd37b266e7dbb52f0e2b294a) 커밋에서 finally 블록에서 clearContext 가 호출되도록 변경했습니다!
@@ -0,0 +1,41 @@
+package nextstep.security.authentication.token;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.exception.AuthenticationTokenException;
+
+import java.util.Base64;
+
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+
+public class BasicAuthenticationTokenConverter implements AuthenticationTokenConverter {
+    private BasicAuthenticationTokenConverter() {}
+
+    public static AuthenticationTokenConverter getInstance() {
+        return SingletonHolder.INSTANCE;
+    }
+
+    @Override
+    public Authentication convert(HttpServletRequest request) {
+        try {
+            final byte[] decoded = Base64.getDecoder().decode(
+                    request.getHeader(AUTHORIZATION).trim().split(" ")[1]
+            );
+            final String[] usernameAndPassword = new String(decoded).split(":");
+            return new UsernamePasswordAuthenticationToken(usernameAndPassword[0], usernameAndPassword[1]);
+        } catch (Exception e) {
+            throw new AuthenticationTokenException();
+        }
+    }
+
+    @Override
+    public boolean supports(HttpServletRequest request) {
+        final String authorizationHeader = request.getHeader(AUTHORIZATION);
+        return authorizationHeader != null
+                && authorizationHeader.trim().startsWith("Basic ");
+    }
+
+    private static final class SingletonHolder {
+        private static final BasicAuthenticationTokenConverter INSTANCE = new BasicAuthenticationTokenConverter();
+    }
+}

리뷰: `HttpHeaders.AUTHORIZATION`를 사용할 수 있을 것 같네요 😄
@@ -0,0 +1,41 @@
+package nextstep.security.authentication.token;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.exception.AuthenticationTokenException;
+
+import java.util.Base64;
+
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+
+public class BasicAuthenticationTokenConverter implements AuthenticationTokenConverter {
+    private BasicAuthenticationTokenConverter() {}
+
+    public static AuthenticationTokenConverter getInstance() {
+        return SingletonHolder.INSTANCE;
+    }
+
+    @Override
+    public Authentication convert(HttpServletRequest request) {
+        try {
+            final byte[] decoded = Base64.getDecoder().decode(
+                    request.getHeader(AUTHORIZATION).trim().split(" ")[1]
+            );
+            final String[] usernameAndPassword = new String(decoded).split(":");
+            return new UsernamePasswordAuthenticationToken(usernameAndPassword[0], usernameAndPassword[1]);
+        } catch (Exception e) {
+            throw new AuthenticationTokenException();
+        }
+    }
+
+    @Override
+    public boolean supports(HttpServletRequest request) {
+        final String authorizationHeader = request.getHeader(AUTHORIZATION);
+        return authorizationHeader != null
+                && authorizationHeader.trim().startsWith("Basic ");
+    }
+
+    private static final class SingletonHolder {
+        private static final BasicAuthenticationTokenConverter INSTANCE = new BasicAuthenticationTokenConverter();
+    }
+}

리뷰: [65922ab](https://github.com/next-step/spring-security-authentication/pull/28/commits/65922ab2a9189e06adb07bd8950a96416d723db0) 커밋에서 반영 완료했습니다!
@@ -0,0 +1,6 @@
+package racingcar;
+
+public interface RacingNumberGenerator {
+
+    int generate();
+}

리뷰: 다리 미션에서 이 인터페이스를 함수형인터페이스로 구현을 하더라구요.
`@FunctionalInterface` 어노테이션을 붙여서
한번쯤, 고려해볼만 한 것 같아, 관련 포스팅 링크 남겨놓습니다!
https://codechacha.com/ko/java8-functional-interface/
@@ -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);
+    }
+}

리뷰: 와 이거 충격적인데요...
Comparable<Car>를 통해 Car 클래스를 정의하신 거 진짜 아이디어 좋네요.
배워 갑니다!
@@ -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;
+    }
+}

리뷰: Position을 일급객체화할 필요가 있나?에 대한 의문이 듭니다. Car 객체의 int 필드로 선언해도 충분해 보여서요.
생성시 validation이 필요한 것도 아니고, 초기값 설정도 Car 객체에서 충분히 가능하고,
Car에서 compareTo를 오버라이딩할 때, Car에 대한 getter만 설정한다면 메소드 구현이 가능하다고 생각합니다.

혹 다른 의도가 있거나, 제 생각이 틀렸다면 피드백 부탁드립니다:)
@@ -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);
+    }
+}

리뷰: from이라는 메소드가 모든 클래스에서 반복되는데,
Cars.from(names)이런 식으로 연결된 경우 의미가 드러나지만
메소드 이름이란 결국 객체에게 보내는 메세지라는 관점에서는 'from'이라는 메소드 이름만 볼 경우 어떤 용도인지 알기 힘든게 사실인것 같습니다.
`setNewCars()`등등 적절하게 메소드명을 바꿔보시면 어떨까 생각해보았습니다.ㅎㅎ
@@ -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--;
+    }
+}

리뷰: AttemptCount 클래스는 시도 횟수에 대한 일급 객체로서의 성격보다,
RacingGame의 전체 코드 흐름을 제어하는 역할을 하는 것 같습니다.
isPlayable 메소드를 통해 게임 진행여부를 판단해 looping을 어디까지 돌 지 판단해주는 것이 핵심이니까요.
`RacingGame`등으로 클래스명을 바꾸고, attemptCount를 인스턴스 필드화 해보면 어떨까요?ㅎㅎ
@@ -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--;
+    }
+}

리뷰: 저도 사용하면서 찝찝했는데 `RacingGame` 객체로 게임을 진행하는 역할을 분리해봐야겠네요...!
좋은 지적 감사합니다!
@@ -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);
+    }
+}

리뷰: 순수 제 아이디어는 아니구... **Tecoble** 에서 `getter()` 에 관련된 내용을 읽다가 알게 된 내용을 그대로 적용해본 거에요... 하하... 저도 처음 이 아이디어를 접하고 충격을 많이 받았습니다 ㅎ

- [getter() 를 사용하는 대신 객체에 메시지를 보내지](https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/)
- [Comparator 와 Comparable 의 이해](https://st-lab.tistory.com/243)

너무 좋은 자료들이라 한번 읽어보시는 거 강력히 추천드립니다!!
@@ -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);
+    }
+}

리뷰: 정적 팩토리 메서드 네이밍에 컨벤션이 존재한다고 알고있습니다!
저도 개인적으로는 적절한 이름을 지어주는 것이 좋다고 생각합니다.
하지만 컨벤션이니까...! 컨벤션을 지키는 방향으로 진행하고 있어요...

- [정적 팩토리 메서드의 네이밍 방식](https://velog.io/@saint6839/%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%EB%84%A4%EC%9D%B4%EB%B0%8D-%EB%B0%A9%EC%8B%9D)