[우아한테크코스] 자동차 경주

2025. 4. 29. 01:41·Java

스터디에서 우테코 자동차 경주 미션을 진행했다.

기능 요구사항은 생각보다 간단했다. 구체적인 요구사항은 다음의 링크에 정의되어 있다.

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

 

GitHub - woowacourse-precourse/java-racingcar-6

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

github.com

 

크게 시간이 걸릴 것 같지는 않았다. 주어진 요구사항에 맞춰 바로 코드를 짜 보았다.

 

Car 클래스

package racingcar.domain;

public class Car {
    private String name;
    private int distance;

    public Car(String name, int distance) {
        this.name = name;
        this.distance = distance;
    }

    public void move(int distance){
        this.distance+=distance;
    }

    public String getName(){
        return name;
    }

    public int getDistance(){
        return distance;
    }
}

 

RacingGame 클래스

package racingcar.game;

import racingcar.domain.Car;

import java.util.*;

public class GameStart {
    private Scanner sc = new Scanner(System.in);
    private ArrayList<Car> cars;

    public void setCars(ArrayList<Car> cars) { // 테스트를 위한 메서드
        this.cars = cars;
    }

    public void run() {
        try {
            System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
            createCar();

            System.out.println("시도할 횟수는 몇 회인가요?");
            int n = sc.nextInt();
            // 예외처리
            if (n <= 0) {
                throw new IllegalArgumentException("시도 횟수는 1회 이상이어야 합니다.");
            }

            System.out.println("실행결과\n");

            for (int i = 0; i < n; i++) {
                start();
                print();
            }

            System.out.print("최종 우승자 : ");

            // 우승자 받아오기
            List<String> winners = findWinner();
            // 우승자 출력
            for (int i = 0; i < winners.size(); i++) {
                System.out.print(winners.get(i));

                if (i != winners.size() - 1) { // , 처리
                    System.out.print(", ");
                }
            }
            System.out.println();

        } catch (IllegalArgumentException e) {
            System.out.println("예외 발생: " + e.getMessage()); // 예외 메시지 출력
        }
    }


    public void createCar(){
        String input = sc.nextLine();
        StringTokenizer st = new StringTokenizer(input,",");
        cars = new ArrayList<>();


        while(st.hasMoreTokens()){
            try{
                String name = st.nextToken();
                // 예외처리
                if (name.isEmpty()) {
                    throw new IllegalArgumentException("자동차 이름에 공백은 포함될 수 없습니다.");
                }
                if (name.contains(" ")) {
                    throw new IllegalArgumentException("자동차 이름에 공백을 포함할 수 없습니다.");
                }
                cars.add(new Car(name,0)); // 플레이어 생성
            }
            catch (IllegalArgumentException e) {
                System.out.println("예외 발생: " + e.getMessage()); // 예외 메시지 출력
                System.exit(1); // 예외 발생 시 종료
            }

        }
    }

    public void start(){
        for(int i=0; i<cars.size(); i++){
            if(is_move()){
                cars.get(i).move(1);
            }
        }
    }

    public boolean is_move(){
        int random = (int)(Math.random() * 10);
        if(random>=4){
            return true;
        }
        return false;
    }

    public void print(){
        for(int i=0; i<cars.size(); i++){
            System.out.println(cars.get(i).getName()+" : "+cars.get(i).getDistance());
        }
        System.out.println();
    }

    public List<String> findWinner(){
        ArrayList<String> winners = new ArrayList<>();

        Collections.sort(cars, new Comparator<Car>() {
            @Override
            public int compare(Car o1, Car o2) {
                return o2.getDistance() - o1.getDistance(); // 거리. 순으로 내림차순 정렬한다.
            }
        });

        int max = cars.get(0).getDistance(); // 첫 번쨰 인덱스가 max값

        // 동점자 찾는 로직
        for(Car car : cars){
            if(car.getDistance()==max){
                winners.add(car.getName());
            }
        }
        return winners;
    }
}

 

구현하면서 테스트 코드도 짜 보았다. 당연하게도 코드는 잘 돌아갔지만 생각하지 못한 부분이 있었다.

바로 객체지향적 설계이다.

 

또한 스터디에서 팀원들의 코드리뷰를 보면서 여러 가지 문제점들을 정리해보았다.

  • 패키지를 구분하는 기준과
  • 패키지명, 인스턴스명의 의미가 모호함
  • 하나의 객체가 많은 책임을 가짐

크게 3가지였고 이 외에도 여러 가지 리뷰가 있었다.

리팩토링

받은 코드리뷰들과 문제점들을 정리하면서 리팩토링을 해보았다. 특히 객체지향 체조원칙을 지키며 구현하는 데에 집중했다. 

https://jamie95.tistory.com/99

 

[Java] 객체지향 생활 체조 원칙 9가지 (from 소트웍스 앤솔러지)

1. 한 메서드에 오직 한 단계의 들여쓰기만 한다. 한 메서드에 들여쓰기가 여러 개 존재한다면, 해당 메서드는 여러가지 일을 하고 있다고 봐도 무관하다. 메서드는 맡은 일이 적을수록(잘게 쪼

jamie95.tistory.com

 

리팩토링에서 mvc패턴을 나름대로 적용해보았다. 많이 쓰는 형식을 무작정 따라하는게 아닌 왜 사용하는지 스스로 고민해보며 짜보았다.

먼저 mvc에 대해 알아보자.

1) 비즈니스 로직과 (2) 시각적인 UI, 그리고 (3) 둘 사이를 연결해 주는 부분을 코드 안에서 분리하고 역할 부여를 해줘야 한다.

위의 내용이 mvc패턴의 기본이며 mvc의 본질은 관심사의 분리라고 생각했다. 관심사의 분리는 다음과 같은 장점이 있다고 생각했다.

  • 좋은 코드를 만들기 위해 코드 혹은 모듈의 역할을 나누고, 각자 역할에 따라서 움직이며, 서로가 서로에게 영향을 받지 않고 코드의 중복을 줄일 수 있다. 이렇게 되면 각 객체마다 부여된 역할만 하기 때문에 코드의 가독성과 유지보수가 용이하다.
  • 구성 요소간의 낮은 결합도로 인해, 코드의 재사용성이 높아지며, 개별 구성 요소를 독립적으로 개발, 수정 및 테스트할 수 있다.
  • 동일한 모델을 여러 뷰에서 사용할 수 있으므로, 애플리케이션의 유연성이 향상된다.

MVC 패턴의 세 가지 주요 구성 요소는 다음과 같다.

모델(Model)

모델은 애플리케이션의 핵심 데이터와 비즈니스 로직을 나타낸다. 데이터 저장소와의 상호 작용, 데이터 처리 및 유

효성 검사와 같은 작업을 수행한다. 모델은 독립적으로 작동하며, 뷰와 컨트롤러와 직접적으로 통신하지 않는다.

뷰(View)

뷰는 사용자에게 보여지는 애플리케이션의 UI 부분이다. 뷰는 모델에서 데이터를 받아 사용자에게 표시하고, 사용자의 입력을 컨트롤러에 전달한다. 뷰는 애플리케이션의 데이터 표시와 관련된 모든 작업을 처리한다.

컨트롤러(Controller)

컨트롤러는 사용자 입력을 처리하고, 애플리케이션의 흐름을 관리한다. 뷰에서 전달된 사용자 입력을 분석하고, 적절한 모델 기능을 호출하여 데이터를 조작하거나 업데이트한다. 그런 다음 결과를 다시 뷰에 전달하여 화면에 표시할 수 있다.


다음은 전체적인 흐름이다.

사용자 요청이 들어오면 → Controller가 중재자 역할을 하고 → View로 사용자 요청을 받아서 → Model을 생성하여 로직을 처리한다. → 그리고 다시 Controller라는 중재자가 View로 사용자에게 응답을 주도록 설계하였다.

 

새롭게 시작하는 마음으로 구현 목록을 작성하고 구현하여보자.

1. 입력 관련
    - 자동차 이름을 입력받아 자동차 생성
        - 쉼표로 구분
        - 이름이 5자 이하인지 체크
        - 중복 제거
        - 빈 값이 있는지 체크
    - 레이싱을 시도할 회수 입력
        - 숫자인지 체크
        - 양수인지 체크
2. 게임 진행
    - 0에서 9 사이의 랜덤한 값을 생성
    - 랜덤값이 4 이상이면 자동차를 전진
    - 매 시도마다 진행 상태 출력게임 결과
3. 우승자 계산
4. 우승자 출력
    - 결과를 String으로 만들어서 출력
    - 공동 우승 처리

 

몇몇 코드는 생략하겠다. 맨 아래의 링크에서 코드를 확인할 수 있다.

 

엔티티 패키지

도메인 패키지의 의미가 모호해서 엔티티라고 정의하였다.

Car 클래스와 Cars 클래스가 있다.

이 객체들을 구현하면서 일급 컬렉션을 활용해보았다.

일급 컬렉션이란

Collection을 Wrapping하면서, 그 외 다른 변수가 없는 클래스의 상태를 일급 컬렉션이라 한다.
  • 컬렉션을 감싼 단 하나의 필드만 가짐
  • 외부에서 직접 컬렉션을 수정하지 못하도록 새로운 리스트를 만들어 반환

장점은 어떤 게 있을까

비즈니스에 종속적인 자료구조이다. 해당 컬렉션에서 필요한 모든 로직은 일급 컬렉션에서 구현한다.

 

Collection의 불변성 보장

private final을 선언하여 Collection을 생성해주고, 생성자를 통해 생성해주면 재할당이 불가능하므로 불변 컬렉션이 된다.

일급 컬렉션 내에서 컬렉션을 직접 조작하도록 강제한다.

  • 외부에서 List<Car>를 직접 조작하지 못하도록 한다.
  • 일급 컬렉션이 제공하는 메서드만 사용하도록 유도한다.
비즈니스에 종속적인 자료구조이다. 해당 컬렉션에서 필요한 모든 로직은 일급 컬렉션에서 구현한다.

Collection의 불변성 보장
- private final을 선언하여 Collection을 생성해주고, 생성자를 통해 생성해주면 재할당이 불가능하므로 불변 컬렉션이 된다.
- 일급 컬렉션 내에서 컬렉션을 직접 조작하도록 강제한다.
- 외부에서 List<Car>를 직접 조작하지 못하도록 한다.
- 일급 컬렉션이 제공하는 메서드만 사용하도록 유도한다.

 

도메인 구조는 아래와 같다.

  • Cars : List<Car> 를 가지고 있는 일급 컬렉션으로 전체 자동차를 관리
  • Car : 인스턴스 변수로 CarName , Position 을 가지고 있으며 자동차의 역할을 한다.
  • CarName : String 으로 자동차 이름을 가지고 있다. 자동차 이름에 대한 검증을 관리한다.
  • Position : int 로 자동차 위치 정보를 가지고 있다. 위치에 대한 검증을 관리한다.

객체 생성 패키지

생성관련된걸 구분하고자 사용자가 입력한 내용을 바탕으로 생성하는 팩토리 패키지를 구현하여 객체 생성을 담당하게 했다.

 

CarFactory 클래스

public class CarFactory {
    public static Car createCar(String name){
        return new Car(name.trim());
    }
}

 

CarsFactory 클래스

public class CarsFactory {
    public static Cars createCars(List<String> carNameList){
        // Car 객체 리스트를 생성
        List<Car> cars = new ArrayList<>();
        for (String carName : carNameList) {
            Car car = CarFactory.createCar(carName);
            cars.add(car);
        }
        // Cars 객체 생성 시, List<Car>를 주입하여 생성
        return new Cars(cars);
    }
}

 

도구 패키지

util 패키지는 프로젝트에서 자주 사용하는 공통적인 기능(유틸리티)을 모아두는 패키지이다.

  • Parser 클래스는 사용자로부터 입력받은 문자열 데이터를 파싱(변환)하는 역할을 담당하도록 구현했다.
  • validation클래스는 자동차 이름 입력값이 유효한지 검증하는 역할을 하도록 구현했다.

Parser 클래스

public class Parser {
    public static List<String> parseCarName(String input) {
        return Arrays.stream(input.replaceAll(" ","").split(",")).toList();
    }
    public static int parseTrail(String input){
        return Integer.parseInt(input.trim());
    }
}

 

 

validation클래스의 코드는 생략한다.

 

뷰 패키지

view 패키지는 사용자와 상호작용하는 역할을 담당하며 출력(화면 표시)과 입력(사용자 입력 처리)을 담당하도록 구현했다.

MVC패턴에서 "View" 역할을 수행하며, 비즈니스 로직과는 분리되어 UI를 처리한다.

코드는 마찬가지로 생략하겠다.

서비스 패키지

service 패키지는게임의 핵심 로직(비즈니스 로직)을 처리하는 패키지이다. 즉, 자동차 경주 게임에서 경기 진행, 자동차 이동, 승자 판별과 같은 주요 기능을 담당한다. 백엔드의 입장에서 가장 중요한 부분이라고도 볼 수 있다.

RacingService 클래스

public class RacingService {
    // 한 턴을 실행하는 함수
    public void doGame(Cars cars){
        for(Car car : cars.getCars()){ // 모든 자동차 리스트 순회
            if(isMove()){
                car.move(1); // 4 이상일 시 car객체 한칸 이동함.
            }
        }
    }

    // 랜덤값이 4 이상이면 true를 반환하는 함수
    private boolean isMove(){
        return Randoms.pickNumberInRange(0,9) >= 4;
    }

    // 가장 멀리 간 차를 구하는 함수
    public List<String> getWinner(Cars cars){
        int maxPosition = getMaxPosition(cars); // 가장 멀리 간 위치를 구함.
        List<String> winners = new ArrayList<>();

        for(Car car : cars.getCars()){
            if(car.getPosition()==maxPosition){ // 멀리 간 위치가 같으면 리스트에 추가
                winners.add(car.getName());
            }
        }

        return winners;
    }

    private int getMaxPosition(Cars cars){
        int max = Integer.MIN_VALUE;

        for(Car car : cars.getCars()){
            if(car.getPosition()>max){
                max = car.getPosition();
            }
        }
        return max;
    }

}

컨트롤러 패키지

controller 패키지는 사용자의 입력을 받아 게임을 진행하고, 서비스 및 뷰와 연결하는 역할을 한다.

즉, 게임의 전체적인 흐름을 관리하는 중추적인 역할을 한다.

RacingController 클래스

public class RacingController {
    private final RacingService racingService;

    public RacingController(RacingService racingService) {
        this.racingService = racingService;
    }

    public void run(){
        String carNamesInput = getCarNamesByInput();
        Validation.checkCarNamesInput(carNamesInput);
        List<String> carNames = Parser.parseCarName(carNamesInput);
        Cars cars = CarsFactory.createCars(carNames);

        int trailInput = Parser.parseTrail(getTrailByInput());

        OutputView.printHead();

        // 입력받은 횟수만큼 게임 실행
        for(int i=0; i<trailInput; i++){
            racingService.doGame(cars);
            OutputView.printScore(cars);
        }

        List<String> winners = racingService.getWinner(cars);
        OutputView.printWinners(winners);
    }

    private String getCarNamesByInput(){
        InputView.requestCarNames();
        return Console.readLine();
    }

    private String getTrailByInput(){
        InputView.requestNumberOfTrail();
        return Console.readLine();
    }
}

 

리팩토링 하며 느낀 것은 처음 코드를 짰을 때는 모든 로직이 한 클래스에 몰려 있어 가독성이 떨어지고 유지보수가 어려웠는데

mvc패턴을 활용하여 패키지를 나누고 클래스마다 역할을 분담해서 앞의 문제들을 해결할 수 있었다.

또한 패키지를 나누어서 코드 재사용성이 높아져 쉽게 활용이 될 것 같고 기능을 교체할 때도 큰 수정 없이 변경 가능할 것 같다.

 

이번 리팩토링을 통해 mvc 패턴을 적용해 볼 수 있었고, 책임에 대해 더 많은 고민을 해보고,

더 많은 고민을 통해 튼튼해진 구조 설계가 개발에 있어 중요하다는 것을 느꼈다.

 

더 생각해볼 점

  • entity 패키지의 이름이 여전히 모호하다. 다른 사람이 코드를 보는 비용도 있기 때문에 패키지나 클래스의 이름을 보고 어떤 역할인지 알 수 있게 이름을 지어야 한다.
  • 서비스의 몇몇 로직이 Cars와 Car에서 충분히 해결이 가능하다. 기능이 확장되면 자칫 서비스가 맡은 책임이 많아질 수 있다.
  • util패키지는 말 그대로 도구이다. 다른 프로젝트에서도 사용이 가능해야한다.

https://github.com/hwanh2/java-racingcar

 

GitHub - hwanh2/java-racingcar

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

github.com

 

'Java' 카테고리의 다른 글

@Entity를 붙이면 일어나는 일 - JPA 내부 동작 원리  (0) 2026.02.01
JPA 영속성 컨텍스트  (0) 2026.01.31
[우아한테크코스] 출석부  (1) 2025.05.29
[우아한테크코스] 로또  (2) 2025.05.02
'Java' 카테고리의 다른 글
  • @Entity를 붙이면 일어나는 일 - JPA 내부 동작 원리
  • JPA 영속성 컨텍스트
  • [우아한테크코스] 출석부
  • [우아한테크코스] 로또
hwanheee
hwanheee
hwanheee 님의 블로그 입니다.
  • hwanheee
    hwanheee 님의 블로그
    hwanheee
  • 전체
    오늘
    어제
    • 분류 전체보기 (20)
      • 백엔드 (8)
      • Java (5)
      • cs (1)
      • 알고리즘 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    크루스칼
    celery
    소수 구하기
    우테코 출석부
    JPA
    우아한테크코스
    우테코 로또
    유니온파인드
    rabbitmq
    생성형AI
    주식 모의투자
    우테코 레이싱카
    스프링
    영속성컨텍스트
    동시성
    동시성 제어
    알고리즘
    체결엔진
    비동기처리
    비관락
    모의투자
    배낭문제
    분산락
    레디스
    최단경로 알고리즘
    최소 스패닝 트리
    다이나믹 프로그래밍
    최소 신장 트리
    프림
    우테코
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hwanheee
[우아한테크코스] 자동차 경주
상단으로

티스토리툴바