우테코 프리코스 [자동차 경주 게임] 구현 후기 (1)

Jii·2023년 8월 29일
0

우테코

목록 보기
1/5

* 자동차 경주 게임 코드 깃허브 링크

https://github.com/Jiihyun/java-racingcar-practice
-아무나 코드 리뷰 해주시면 ㅎ,,환영합니다

첫 번째 시도

느낀 점

앨리스 프로젝트가 끝난 후 무언가가 돌아가는 하나의 프로그램을 만드는 프로젝트를 너무 오랜만에 해봐서 그런지, 처음 자동차 경주 게임 구현하라는 readme.md 읽었을 땐 꽤나 복잡하게 느껴졌다.

또 객체지향스러운 코드가 좋은 코드라는 점은 여러 책, 블로그, 강의들로 접했어서 어느 정도 이론은 알고 있었지만, 그것들을 적용하여 백지 상태의 ide를 구조부터 구현까지 채워넣으려니 엄청 쉽지 않았다.
아니 거의 적용을 하지 못했다고 봐도 무방하다,,,ㅋㅋㅋㅋ

각설하고 처음 시도하여 만든 저의 자동차 경주 게임을 공개합니다,,,,
(벨로그는 코드블럭 스크롤 기능이 없나보네여,,,?)

package racingcar;

import camp.nextstep.edu.missionutils.Console;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class Application {
    public static void main(String[] args) {
        // TODO 구현 진행
        System.out.println("경주 할 자동차 이름(이름은 쉼표(,) 기준으로 구분)\n");
        String name = Console.readLine();
        String[] nameArr = name.split(",");
        Car[] cars = new Car[nameArr.length];
        Result result = new Result(nameArr, cars);
        for (int i = 0; i < result.nameArr.length; i++) {
            try {
                if (result.nameArr[i].length() > 5) {
                    throw new IllegalArgumentException();
                }
                result.cars[i] = new Car(result.nameArr[i]);
            } catch (Exception e) {
                System.out.println("[ERROR]: 이름은 5자 이하만 가능합니다.");
            }
        }

        System.out.println("시도할 회수는 몇회인가요?\n");
        String tryTimes = Console.readLine();

        int tryTime = Integer.parseInt(tryTimes);
        System.out.println("실행 결과\n");
        while (tryTime > 0) {
            for (Car car : result.cars) {
                car.move();
            }
            System.out.println();
            tryTime--;
        }

        List<Car> winnerList = Arrays.stream(result.cars)
                .sorted(Comparator.comparingInt(Car::getPosition).reversed())
                .collect(Collectors.toList());

        List<Car> winner = new ArrayList<>();
        for (
                int i = 0; i < winnerList.size(); i++) {
            int winnerPosition = winnerList.get(0).getPosition();
            if (winnerList.get(i).getPosition() == winnerPosition) {
                winner.add(winnerList.get(i));
            }
        }
        System.out.print("최종 우승자 : ");
        for (
                int i = 0; i < winner.size(); i++) {
            Car car = winner.get(i);
            if (i > 0) {
                System.out.print(", ");
            }
            System.out.print(car.getName());
        }
        System.out.println();

    }

    private static class Result {
        public final String[] nameArr;
        public final Car[] cars;

        public Result(String[] nameArr, Car[] cars) {
            this.nameArr = nameArr;
            this.cars = cars;
        }
    }

}

어쨌든 보시면 알 수 있듯이,, 이 Application 클래스에 모든 코드를 다 때려박았습니다.
여기서도 나름 분리해 보겠다고 발악한 흔적은 바로,,
static으로 Result클래스 하나만들고, 이 외에는 기존의 Car 클래스에 메서드 딱 2개..? 나눈 것 이죠 ㅎㅋㅎㅎ
그래도 돌아가는 예쁜 스레기를 구현했다는 기쁨에 1차 시도는 이렇게 마무리했습니다.

두 번째 시도

이번 시도의 중점은 비슷한 기능을 하는 메서드끼리 클래스에 모아두는 것이였습니다.
예를 들어 Printer 라는 클래스가 있으면 반드시 출력하는 메서드들만 모아 놓고, GameView라는 클래스가 있으면 view와 관련된 기능들만 하는 클래스 인 것이죠.
Application 클래스에서는 컨트롤러와 연결하여 게임을 시작하게 하는 것 외에 다른 기능은 하지 않게하는 것이라 볼 수 있죠.

구조는 다음과 같습니다.

따로 package 분리는 이번에 해주지 않았습니다.

그럼 한 번 코드를 살펴보겠습니다.⬇️

Container class

새로운 시도로 container 클래스를 만들어 그 안에 아래와 같이 객체를 한 번만 생성할 수 있게끔 처리해주었습니다. 마치 스프링 실행하면 싱글톤이 유지되는 것 같이 말이죠.

package racingcar2;

public class Container {
    private Reader reader;
    private Printer printer;
    private GameController gameController;
    private GameView gameView;

    public Reader reader() {
        if (reader == null) {
            reader = new Reader();
        }
        return reader;
    }

    public Printer printer() {
        if (printer == null) {
            printer = new Printer();
        }
        return printer;
    }

    public GameView gameView() {
        if (gameView == null) {
            gameView = new GameView();
        }
        return gameView;
    }

    public GameController gameController() {
        if (gameController == null) {
            gameController = new GameController(
                    reader(),
                    printer(),
                    gameView()
            );
        }
        return gameController;
    }
}

⏺️ 이로 인해 Application 클래스에서는 Container에서 미리 만들어둔 controller 객체를 불러 게임 스타뜨 메서드만 실행해주었습니다.

package racingcar2;

public class Application {
   public static void main(String[] args) {
       // TODO 구현 진행
       Container container = new Container();
       GameController gameController = container.gameController();
       gameController.start();
   }
}

일급 콜렉션

여러 car가 생김으로 인해 Cars 클래스를 만들어 일급 컬렉션으로 만들어 주었습니다.

public final class Cars { 

    private final List<racingcar2.Car> cars;
    
    }

일급콜렉션은 위와 같이 이 cars 콜렉션 외에는 다른 멤버 변수가 없는 상태를 말합니다.

이로 인해 다른 사람들이 사용자가 입력한 이름을 가진 car들만 들어있던 리스트를 변경하는 등의 불참사가 일어날 우려를 하지 않아도 되는 장점이 있습니다.

사진과 같이 이 조건으로만 생성할 수 있는 자료구조를 만들었기 때문입니다.


이렇게 cars 콜렉션을 클래스로 한 번 감싸주었기 때문에 list임에도 불구하고 add, remove와 같은 메서드들이 나타나지 않으며, 오로지 Cars 클래스에서 개발자가 만든 메서드들만 추천으로 띄워줍니다.

따라서 Cars 자료구조가 필요한 모든 곳에서 일일이 검증해주지 않아도 되며, 설상 검증로직이 필요한지 모르더라도 문제가 일어나지 않게 됩니다.

이 외에도 많은 장점들이 존재하지만 더 자세한 내용은
https://jojoldu.tistory.com/412 참고 하시면 좋을 것 같습니다.

문제점

package racingcar2;

public class GameView {
    //view에서는 데이터 가공을 하지 않음(렌더링 및 응답만) -> 데이터 변경 및 처리해주는 service계층 만들기
    //데이터 다 섞여있으므로 -> 계층을 나눌 것
    public void render(final Cars cars, final int tryTimes) {
        for (int trial = 0; trial < tryTimes; trial++) {
            for (int i = 0; i < cars.getCarList().size(); i++) {
                Car car = cars.getCarList().get(i);
                car.moveChance(); //이 부분이 데이터 처리해주는 거라 잘못됨
                String move = car.carMove();
                System.out.println(car.getName() + " : " + move);
            }
            System.out.println();
        }
        System.out.println("최종 우승자 : " + cars.getWinner());
    }
}

기능 별로 최대한 나누고자 했는데,
아쉽게도 조건 충족 시 car position을 증가시키는 비지니스 로직을 오직 view 출력만 해야하는 GameView 클래스에 넣어버렸다.
이에 메소드가 한 기능만 해야하는 단일 책임의 원칙을 만족하지 못한 것이죠.
이를 최대한 지킨 코드는 racingcar3(세 번째 시도) package 에서 볼 수 있습니다!

느낀 점

이외에도 변수명이라던가 예외처리 부족 등 아쉬움이 남았던 시도였다.

글이 길어지는 관계로 세 번째 시도는 다음 글에서 작성하도록 하겠다.

이번 구현을 통해 현업에서 개발자들이 어떻게 협업하는지 아주 사아알짝 알게되었다.
무엇보다도 그 거대한 프로그램을 돌아가게 하기위해 여럿이서 파트를 나눠 코드를 작성하는 것이므로, 무엇보다도 읽기 쉽고 알아보기 쉬운 코드를 짜는게 매우 중요하다는 것을 다시금 깨달았다.
또 많은 코드들의 검증 사항을 개발자들이 알아서 유의해서 사용하는 것도 물론 맞지만, 애초에 만들면서 접근하지 말아햐 하는 부분은 막고 오로지 접근 가능한 것들만 열어두어야 한다는 점도 새로 배웠다.

앞으로도 더 좋은 변수명, 더 읽기 쉬운 코드, 클린한 코드를 짤 수 있도록 더 많이 구현해봐야겠음을 다시 다짐하게 되었다.

하다보면 늘더라!!!!

profile
Empower Yourself

0개의 댓글