카테고리 없음

(1)스프링 스터디 : 레이싱카 - 콘솔

문상휘파람 2024. 7. 24. 17:19

이제 드디어 스프링을 해 볼 차례다 !! 

일단 계산기, 로또로 다져진 실력을 레이싱카 미션에 적용시켜 보았다. 결론부터 말하자면 실력이 많이 늘었다는게 느껴질 정도였다. 쉬웠음!!

 

 

일단 문제 조건은 요렇다. 

 

그리구 내 패키지 구조

 

이제 내가 작성한 코드를 소개해보겠다.


*exception 패키지 - CustomException은 여기서 처음 구현해보았다. 정석적인 CustomException은 아님.

 

- CustomErrorCode

package com.racing.common.domain.exception;

public enum CustomErrorCode {

    EXCEPTION_RANGE("차 이름 너무 긺"),
    EXCEPTION_CAR("차 없음");

    private final String message;

    CustomErrorCode(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

enum으로 관리해서 메세지만 넘겨주도록 만들었음! 

 

- CustomException

package com.racing.common.domain.exception;

public class CustomException extends RuntimeException{

    public CustomException(CustomErrorCode customErrorCode) {
        super(customErrorCode.getMessage());
    }
}

RuntimeException 상속받아서 CustomErrorCode에서 메세지만 전달받도록 작성했다.


* vo 패키지

 

- Name

package com.racing.common.domain.vo;


import com.racing.common.domain.exception.CustomErrorCode;
import com.racing.common.domain.exception.CustomException;

public class Name {

    private static final int CAR_NAME_BOUNDARY = 5;

    private final String name;

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

    public static Name from(final String name) {
        validateNameRange(name);
        return new Name(name);
    }

    public String getName() {
        return name;
    }

    private static void validateNameRange(String name) {
        if (name.length() > CAR_NAME_BOUNDARY) {
            throw new CustomException(CustomErrorCode.EXCEPTION_RANGE);
        }
    }
}

차 이름 하나 하나를 원시값 포장해줌. 여기서 static 메서드를 통해 검증까지 한 후 값 포장해줄 수 있도록 코드 작성하였다.


- Car

package com.racing.common.domain;

import com.racing.common.domain.vo.Name;
import com.racing.web.random.CreateRandomNumber;


public class Car {

    private static final int CAR_MOVE_BOUNDARY = 4;

    public int moveCount;
    private final Name carName;

    public Car(int moveCount, String carName) {
        this.moveCount = moveCount;
        this.carName = Name.from(carName);
    }

    public void moveCar(CreateRandomNumber createRandomNumber) {
        if (createRandomNumber.generateRandomNumber() >= CAR_MOVE_BOUNDARY) {
            moveCount++;
        }
    }

    public int getMoveCount() {
        return moveCount;
    }

    public String getCarName() {
        return carName.getName();
    }
}

Car 클래스에서 아쉬운건 moveCount 부분.. moveCount 원시값 포장한거 넘겨 받거나 아니면 생성자로 값 받아서 관리 해줄지 고민했는데 필드로 관리하였다.. 뭔가 아쉽. 여기서 CreatRandomNumber 클래스는 Web 패키지 하위에 있다. Random 값 생성해주는 클래스(CreateRandomNumber)를 나중에 Spring Api로 작성할 때 빈에 등록해야 했고, @Component 어노테이션을 붙여줌에 따라 패키지를 옮길 수 밖에 없었다 ㅠ 나중에 Spring APi 설명해줄 때 소개 해 드릴게여.

 

- CarNameParser

package com.racing.common.domain;

import com.racing.common.domain.vo.Name;

import java.util.ArrayList;
import java.util.List;

public class CarNameParser {

    private static final String SPLIT_STRING_DELIMITER = ",";

    public final List<Name> carNames;

    public CarNameParser(String carNames) {
        this.carNames = parseCarName(carNames);
    }

    public List<Name> getCarNames() {
        return carNames;
    }

    private List<Name> parseCarName(String carNames) {
        String[] parsedCarNames = carNames.split(SPLIT_STRING_DELIMITER);
        List<Name> Names = new ArrayList<>();
        for (String carName : parsedCarNames) {
            Names.add(Name.from(carName));
        }
        return Names;
    }
}

 차 이름 입력받았을 때 하나 하나 Name으로 원시값 포장 받아서 List<Name> 일급컬렉션 형태로 만들어줌.

 

- Car 

package com.racing.common.domain;

import com.racing.common.domain.vo.Name;

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

public class Cars {

    private static final int INITIAL_NUMBER = 0;

    private final List<Car> cars;

    public Cars(List<Name> carNames) {
        this.cars = makeCars(carNames);
    }

    public Cars(List<Car> carBundle, boolean dummy) {
        this.cars = carBundle;
    }

    public List<Car> getCars() {
        return cars;
    }

    public List<String> getWinner() {
        return cars.stream()
                .filter(this::isMaxCount)
                .map(Car::getCarName)
                .collect(Collectors.toList());
    }

    private boolean isMaxCount(Car car) {
        return car.getMoveCount() == getMaxCount();
    }

    private int getMaxCount() {
        return cars.stream()
                .mapToInt(Car::getMoveCount)
                .max()
                .orElse(INITIAL_NUMBER);
    }

    private List<Car> makeCars(List<Name> carNames) {
        List<Car> cars = new ArrayList<>();
        for (Name carName : carNames) {
            Car car = new Car(INITIAL_NUMBER, carName.getName());
            cars.add(car);
        }
        return cars;
    }
}

Car들을 관리하는 클래스. 역시 List<Car> 일급 컬렉션 형태로 만들어 줬고, Winner 뽑는 메서드도 구현해줬다.


*view 패키지

 

-InputView

package com.racing.console.view;

import java.util.Scanner;

public class InputView {

    private final Scanner input;

    public InputView(Scanner input) {
        this.input = input;
    }

    public String inputCarNames() {
        return input.next();
    }

    public int inputChance() {
        return input.nextInt();
    }
}

a

 

-OutputView

package com.racing.console.view;

import java.util.List;

public class OutputView {

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

    public void getChanceGuide() {
        System.out.println("시도할 회수는 몇회인가요?");
    }

    public void runGuide() {
        separateLine();
        System.out.println("실행결과");
    }

    public void showCar(String carName, int moveCount) {
        System.out.println(carName + " : " + "-".repeat(moveCount));
    }

    public void separateLine() {
        System.out.println();
    }

    public void showWinner(List<String> carWinners) {
        System.out.println(String.join(",", carWinners) + "가 최종 우승했습니다.");
    }
}

 

 

view 패키지들은 어디에도 의존하지 않도록 MVC 패턴 철저하게 지켜서 작성 (물론 domain 도 마찬가지임)


*Controller 패키지

 

- CarController

package com.racing.console.controller;

import com.racing.common.domain.*;
import com.racing.common.domain.vo.Name;
import com.racing.console.view.InputView;
import com.racing.console.view.OutputView;
import com.racing.web.random.CreateRandomNumber;

import java.util.List;

public class CarController {

    private static final int INITIAL_NUMBER = 0;

    private final InputView inputVIew;
    private final OutputView outputView;
    private final CreateRandomNumber createRandomNumber;

    public CarController(InputView inputVIew, OutputView outputView, CreateRandomNumber createRandomNumber) {
        this.inputVIew = inputVIew;
        this.outputView = outputView;
        this.createRandomNumber = createRandomNumber;
    }

    public void run() {
        Cars cars = new Cars(getCarNames());
        List<Car> carBundle = cars.getCars();
        int moveCarChance = getChance();
        outputView.runGuide();
        moveCars(carBundle, moveCarChance);
        outputView.showWinner(cars.getWinner());
    }

    private List<Name> getCarNames() {
        outputView.inputCarNamesGuide();
        CarNameParser carNameParser = new CarNameParser(inputVIew.inputCarNames());
        return carNameParser.getCarNames();
    }

    private int getChance() {
        outputView.getChanceGuide();
        return inputVIew.inputChance();
    }

    private void moveCar(List<Car> carBundle) {
        for (Car car : carBundle) {
            car.moveCar(createRandomNumber);
            outputView.showCar(car.getCarName(), car.getMoveCount());
        }
        outputView.separateLine();
    }

    private void moveCars(List<Car> carBundle, int moveCarChance) {
        for (int i = INITIAL_NUMBER; i < moveCarChance; i++) {
            moveCar(carBundle);
        }
    }
}

요론식으로 잘 조합해줬다.


- ConsoleApplication

package com.racing.console;

import com.racing.console.controller.CarController;
import com.racing.console.view.InputView;
import com.racing.console.view.OutputView;
import com.racing.web.random.CarRandomNumber;
import com.racing.web.random.CreateRandomNumber;

import java.util.Scanner;

public class ConsoleApplication {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        OutputView outputView = new OutputView();
        InputView inputVIew = new InputView(scanner);
        CreateRandomNumber createRandomNumber = new CarRandomNumber();
        CarController carController = new CarController(inputVIew, outputView, createRandomNumber);
        carController.run();
    }
}

여기서 scanner 를 외부에서 주입받은 이유는 테스트 할 때, 이런식으로 외부에서 주입 받아주면 다른 입력 소스값을 넣어 줄 수 있어서 다양한 테스트 가능하고, 조금 더 객체지향적인 코드를 구현할 수 있어서 외부에서 주입받을 수 있도록 코드 구현해주었다.


느낀점. 

 

확실히 초록스터디 미션을 하면서 실력이 많이 는 것 같다. 어떤식으로 코드를 작성해야 할 지 알게 되었고, 원시값 포장 일급컬렉션 같은 용어들에 대해 실습을 통해 확실하게 알 수 있었다. 앞으로는 Map, Optional 등 더 다양한 메서드와 자료구조 형태를 이용해보면서 java에 대한 이해를 한 층 높이고 싶다. 레이싱카 굿굿. 나중에 보면 왜 이렇게 작성했나 싶겠지만, 나름 지금 열심히 작성한 것...