우테코 2주차 후기
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주차가 끝나고 내가 전혀 객체지향적 설계를 하고 있지 않다는 것을 깨닫고, <객체지향의 사실과 오해> 를 완독했다. 책을 읽으니 어떻게 객체지향 설계를 해야 할지 좀 감이 잡혔다. 결국에는 객체의 역할, 책임, 협력이 가장 중요하다. 적절한 객체에게 적절한 책임을 부여하고, 객체끼리 메세지를 통해 소통하는 것이다. 객체가 해야 할 행동에 집중해야 한다.
자동차 경주 게임에서 어떠한 객체가 어떠한 행동을 해야 하는지, 객체끼리 어떠한 메세지를 주고받아야 하는지 고민해보았다. 아래는 내가 고민을 통해 그린 다이어그램이다.
각 객체가 해야 할 행동으로는 아래와 같이 정리했다.
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 패턴으로 설계하면서 그린 다이어그램이다.
- 🤔 검증 위치에 대한 고민
- 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주차가 된 지금 이 개념들을 구현해낸 제 자신이 놀랍기도 하다. 디스코드 서버를 보며, 저보다 잘하는 사람들이 너무나도 많다는 것을 보고 잠깐 좌절감을 느끼기도 했다. 그럼에도 포기하지 않고 저만의 속도로 묵묵히 공부해온 저 자신을 칭찬해주고 싶다.