Skip to main content

우테코 2주차 후기

·6 mins· loading · loading ·
Soeun Uhm
Author
Soeun Uhm
problem-solving engineer, talented in grit.
Table of Contents

🚀 자동차 경주 게임 기능 요구 사항
#

  • 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
  • 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
  • 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
  • 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
  • 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
  • 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
  • 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
  • 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.

🎯 내가 구현한 기능 목록
#

  • n대의 자동차 이름들을 입력 받는다.
    • 각 자동차 이름은 쉼표(,) 로 나눈다.
    • 자동차 이름은 1글자 이상, 5글자 이하이어야 한다.
    • 자동차 이름에 공백이 포함되면 안된다.
    • 자동차 이름이 중복되면 안된다.
    • 잘못된 입력이면 IllegalArgumentException 을 발생시키고 어플리케이션을 종료한다.
  • 시도 횟수를 입력 받는다.
    • 시도 횟수는 0 초과의 정수이어야 한다.
    • 잘못된 입력이면 IllegalArgumentException 을 발생시키고 어플리케이션을 종료한다.
  • 자동차 객체는 조건에 따라 움직인다.
    • 자동차 객체의 초기 위치는 0이다.
    • 자동차 객체는 주어진 거리에 따라 움직인다.
    • Randoms.pickNumberInRange 를 사용해서 무작위 숫자를 구한다.
    • Randoms.pickNumberInRange 에서 Range 는 0 이상 9 이하이다.
    • 무작위 숫자가 4 이상이면 자동차의 position을 1 전진시킨다.
  • 시도 횟수마다 각 자동차의 position 을 출력해야 한다.
  • 모든 시도 횟수가 끝난 후 우승자를 판별해야 한다.
    • 우승자는 position 이 가장 큰 자동차이다.
    • 우승자는 한명 이상일 수 있다.
    • 우승자가 여러 명일 경우, 쉼표(,)를 이용하여 구분한다.

🧠 프로그램 설계
#

1주차가 끝나고 내가 전혀 객체지향적 설계를 하고 있지 않다는 것을 깨닫고, <객체지향의 사실과 오해> 를 완독했다. 책을 읽으니 어떻게 객체지향 설계를 해야 할지 좀 감이 잡혔다. 결국에는 객체의 역할, 책임, 협력이 가장 중요하다. 적절한 객체에게 적절한 책임을 부여하고, 객체끼리 메세지를 통해 소통하는 것이다. 객체가 해야 할 행동에 집중해야 한다.

자동차 경주 게임에서 어떠한 객체가 어떠한 행동을 해야 하는지, 객체끼리 어떠한 메세지를 주고받아야 하는지 고민해보았다. 아래는 내가 고민을 통해 그린 다이어그램이다.

image

객체가 해야 할 행동으로는 아래와 같이 정리했다.

  • Car
    • Car 객체는 name, position 을 속성으로 가진다.
    • Car 객체는 조건에 따라 position 을 움직인다.
  • CarName
    • CarName 객체는 자동차의 이름을 나타내는 객체이다.
    • 1글자 미만, 5글자 초과이지 않은지 검증한다.
    • 이름안에 공백이 없는지 검증한다.
  • CarPosition
    • CarPosition 의 초기값은 0이다.
    • CarPosition 은 주어진 DISTANCE 만큼 position 을 증가시킨다.
  • Cars
    • Cars 객체는 carNames 가 주어지면 자동차 이름을 파싱해 Car 객체의 리스트로 저장한다.
    • 주어진 carNames 에서 중복된 자동차 이름이 없는지 검증한다.
    • Cars 객체는 리스트 안에 있는 Car 들을 움직일 수 있다.
    • Cars 객체는 리스트 안에 있는 Car 중에 가장 큰 position 값을 구할 수 있다.
  • Winners
    • Cars에게 가장 큰 position 값을 달라고 메세지를 보낸다.
    • 가장 큰 position 값을 가진 Car 들을 리스트에 저장한다.
  • TrialNumber
    • TrialNumber 가 정수인지 검증한다.
    • TrialNumber 가 0 초과의 정수인지 검증한다.
    • TrialNumber 를 하나씩 감소시킨다.
    • TrialNumber 0 인지 확인한다.
  • MoveRule
    • 조건에 따라 움직일지 말지 여부를 알려준다.
  • Game
    • Cars 객체에게 모든 자동차를 움직이라고 메세지를 보낸다.
    • 다음 라운드로 가면 TrialNumber 에게 하나 감소하라고 메세지를 보낸다.
    • 게임이 끝났는지 여부를 판별하기 위해 TrialNumber에게 값이 0인지 확인하는 메세지를 보낸다.

원시값 포장
#

원시값을 포장하라는 ‘객체지향 생활체조 원칙’에 등장하는 내용이다. Primitive 타입을 그대로 사용하지 않고, 객체로 사용하기 위해 하나의 클래스로 선언해주는 것이다.

자동차 이름은 String 원시값이지만, 검증 로직이 필요했다.

자동차 이름은,

  • 자동차 이름은 1글자 이상, 5글자 이하이어야 한다.
  • 자동차 이름에 공백이 포함되면 안된다.
    위 2가지 검증로직이 있었다.

모든 자동차 이름이 위의 3가지 로직을 만족한다는 것을 보장하기 위해, 해당 조건으로만 생성할 수 있는 자료구조를 새롭게 만들었다. 이것이 CarName 객체이다. 모든 CarName 을 사용하는 곳에서 CarName 은 위 3가지 검증 로직을 만족한다는 것을 보장해서, 또 검증로직을 사용할 필요가 없다.

public class CarName {  
    public CarName(String name) {  
        validateCarName(name);  
        this.name = name;  
    }  
    private void validateCarName(String name) {  
        validateCarNameLength(name);  
        validateCarNameHasNoSpace(name);  
    }

자동차 위치도 원시값을 포장했다. 자동차 위치에게 1만큼 전진하라는 책임을 부여했다. 그리고 스스로 위치값을 관리하도록 했다.

public class CarPosition {  
    private final int INITIAL_POS = 0;  
    private int pos;  
    private final int DISTANCE = 1;  
  
    public CarPosition() {  
        this.pos = INITIAL_POS;  
    }  
  
    public void advancePosition() {  
        pos += DISTANCE;  
    }  
}

시도 횟수도 원시값을 포장했다. 시도횟수가 0 이상의 정수라는 검증 로직이 필요했고, 시도횟수를 1씩 줄이는 것도 시도 횟수 자체의 책임이라고 생각했다.

public class TrialNumber {  
    private int trial;  
  
    public TrialNumber(String input) {  
        validateTrialNumber(input);  
        this.trial = Integer.parseInt(input);  
    }  
    public void decreaseTrialNumber() {  
        trial--;  
    }  
    public boolean isTrialNumberZero() {  
        return trial == 0;  
    }

MVC 패턴
#

‘헤드퍼스트 디자인패턴’의 MVC 전략과, 테코톡에서 MVC 를 다루는 영상을 찾아보았다. 왜 MVC 전략으로 프로그램을 설계하는지와, Model, View, Controller 가 각각 어떠한 역할을 해야 하는지 감을 잡을 수 있었다.

  • Model : 객체의 행동을 정의하고, 객체마다 있는 예외 로직도 처리.
  • View : 입력, 출력을 처리
  • Controller : View로부터 사용자의 입력을 받아서 Model 에게 보내서 정보를 가공하고, Model 로부터 결과를 받아서 View 에게 전달

MVC 패턴으로 설계하면서 그린 다이어그램이다.

image
  • 🤔 검증 위치에 대한 고민
    • View 에서 입력을 받을 때 원시값에 대한 검증 (ex. 이름을 입력하라고 하면 String 인지 확인) 을 할 지 고민했었는데, 이번 주차에서는 모델의 객체에서 검증 로직을 다루었다. 하지만, 검증 로직이 훨씬 많아지면 모델이 뚱뚱해질 것 도 같아서, 고민이 된다.

테스트 코드 작성
#

객체의 단위로 분리하고 나니, 테스트 코드 작성이 한결 수월해졌다. 그럼에도 랜덤값을 테스트하는 코드는 작성하지 못했는데.. 다른 분들이 코드 리뷰를 해주실 때 조언을 많이 해주셨다. 따라서 바로 리팩토링 하고 따로 포스팅을 올릴 예정이다.

💧 아쉬운 점
#

랜덤값에 대한 테스트 코드 작성 실패
#

MoveRule 에서 랜덤값을 생성해서 움직여야 할지 말지 True/False 값을 리턴하게 하는 shouldMove() 메서드를 만들었다. 이것을 Car 객체 내에서 MoveRule 의 인스턴스를 만든 후, moveByCondition() 에서 사용했다. 이렇게 코드를 작성하니, moveByCondition() 메서드가 랜덤값에 의존해서, 테스트 코드를 작성하기 쉽지 않았다.

우테코 미션을 제출한 후 여러 분들의 코드 리뷰를 받았는데, 거기서 테스트코드 작성이 쉽지 않다는 것은 설계가 잘못되었다는 피드백을 받았다 . 전략패턴을 공부해보니, strategy 의 인터페이스를 생성하고 그것을 Car 객체에게 주입하는 방식으로 작성했더라면 훨씬 나았을 것 같다는 생각이 들었다. 그래서 따로 하나의 글을 작성할 계획이다.

public class MoveRule {  
    private final int MOVE_CONDITION = 4;  
    private int randomNumber;  
  
    private final RandomNumberGenerator randomNumberGenerator = new RandomNumberGenerator();  
  
    public boolean shouldMove() {  
        randomNumber = randomNumberGenerator.getRandomNumber();  
        return randomNumber >= MOVE_CONDITION;  
    }  
}
// Car 객체 내 메서드
public class Car {  
    private final CarName carName;  
    private final CarPosition carPosition;  
    private final MoveRule moveRule = new MoveRule();  
  
    public Car(String name) {  
        this.carName = new CarName(name);  
        this.carPosition = new CarPosition();  
    }  
    public void moveByCondition() {  
        if (moveRule.shouldMove()) {  
            carPosition.advancePosition();  
        }  
    }

하나의 메서드는 하나의 일만 하도록 !
#

메서드를 최대한 작게, 하나의 일만 하도록 했어야 하는데, Controller 부분에서 race() 메서드에서 과도한 일을 시켰다.

private Winners race(Cars cars, TrialNumber trialNumber, Game game) {  
    while (!game.isGameEnd(trialNumber)) {  
        game.playGame(cars);  
        outputView.displayRoundStatus(cars);  
        game.nextRound(trialNumber);  
    }  
    Winners winners = new Winners(cars);  
    return winners;  
}

실제 game 을 진행하는 과정과, Winners 를 분리했으면 더 좋은 코드가 되었을 것이라는 피드백을 받았다. 따라서 아래의 방식으로 리팩토링 해보았다.

private Winners race(Cars cars, TrialNumber trialNumber, Game game) {  
    playRace(cars, trialNumber, game);  
    return getWinnersOfRace(cars);  
}  
  
private void playRace(Cars cars, TrialNumber trialNumber, Game game) {  
    while (!game.isGameEnd(trialNumber)) {  
        game.playGame(cars);  
        outputView.displayRoundStatus(cars);  
        game.nextRound(trialNumber);  
    }  
}  
  
private Winners getWinnersOfRace(Cars cars) {  
    return new Winners(cars);  
}

소감
#

매일 매일 java 와 객체지향에 대해 학습하면서, 어떻게 하면 더 나은 프로그램을 만들 수 있는지에 대한 방향성을 알 수 있었다. 프리코스 시작 전에는 객체지향, TDD, MVC 패턴에 대해 잘 몰랐는데, 2주차가 된 지금 이 개념들을 구현해낸 제 자신이 놀랍기도 하다. 디스코드 서버를 보며, 저보다 잘하는 사람들이 너무나도 많다는 것을 보고 잠깐 좌절감을 느끼기도 했다. 그럼에도 포기하지 않고 저만의 속도로 묵묵히 공부해온 저 자신을 칭찬해주고 싶다.

Reference
#