우테코 2주차 과제를 진행하면서 고민했던 내용을 정리한다. 참고 없이 진행한 내용으로 다소 주관적인 내용이 포함되어 있다.
과제
2주차 과제는 자동차 경주를 구현하는 것이다. 기능 요구사항은 다음과 같다.
- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.
기능 목록
1주차와 마찬가지로 과제를 시작하기 전에 기능 목록을 작성하였다. 동일하게 중심이 되는 기능들을 먼저 정의하고 나머지 부과요소들을 작성하는 순으로 진행하였다.
1. 자동차는 전진, 정지 할 수 있다.
2. 자동차 엔진은 자동차의 전진, 정지를 지원한다.
3. 트랙에는 여러 자동차들이 존재한다.
// 중략
1주차 비해 자동차는 객체로 분리하기 어렵지 않았다. 다만 요구사항에 적혀있는 것과 같이 자동차 엔진 전에 자동차를 구현하는 순으로 작성하였기 때문에 문제가 발생했다. 실행할 수 없는 상태인 자동차를 개발하게 되었는데(그렇게 되면 기능 요구사항 1을 만족하지 못하기도 하다) 엔진 없는 고철 덩어리 자동차로 만든채 커밋을 하였다. 다음에는 실제로 구현해서 실행할 수 있는 것인지 기능목록 작성시 확인하고 넘어가자
과도한 실제화
1주차의 문제의 대부분은 과도한 실제화로 인한 문제였다. 과연 2주차에서는 해결할 수 있었을까? 실제화를 최대한 방지하고자 SRP를 떠올리면서 구현하려고 했다. (SRP을 떠올리면 최대한 프로그래밍 관점에서 볼 수 있는거 같다.) 역활에 대해서도 생각해봤는데 프로그래밍에서의 역활을 제대로 표현하고자 하였다.
public class Gasoline implements Engine {
private final int moveDistance;
//중략
@Override
public int accelerate() {
return moveDistance;
}
}
엔진의 구현제인 가솔린이다. 엔진은 자동차에서 이동을 담당하는 곳이다. 실제 엔진을 생각해본다면 엔진에 대한 이동처리를 해야하지만 프로그래밍 관점에서 이동을 지원한다는 것으로 이동값을 반환한다고 생각할 수 있다.
랜덤은 누구의 역할일까?
자동차는 랜덤값을 통해 이동유무를 결정한다. 그러면 랜덤처리를 어디서 해야할까?
처리에 대한 책임을 크게 차 내부와 외부로 나눌 수 있다. 결론적으로 랜덤에 대한 처리는 차의 내부 엔진에서 처리하기로 결정했다. 랜덤은 결국 이동을 위한 중간 다리이다. 그러면 이동 처리에 역활을 가진 객체와 관련이 있어보이는데 바로 엔진이다. 자동차나 트랙이나 자동차가 어떤 랜덤값을 가지고 이동하는지 안하는지 알 필요 없다.
public class RandomEngine implements Engine {
private static int RANDOM_RANGE_START = 0;
private final int randomRangeEnd;
private final int accelerationThreshold;
private final int moveDistance;
public RandomEngine(int moveDistance, int randomRangeEnd, int accelerationThreshold) {
this.moveDistance = moveDistance;
this.randomRangeEnd = randomRangeEnd;
this.accelerationThreshold = accelerationThreshold;
}
@Override
public int accelerate() {
int number = Randoms.pickNumberInRange(RANDOM_RANGE_START, randomRangeEnd);
if (number >= accelerationThreshold) {
return moveDistance;
}
return 0;
}
}
마치 "고장난 엔진", "종종 고장나는 엔진" 이라고 생각하고 구현하였다.
OOP 원칙을 설계에 도움을 줄까
자동차는 추상화를 하기 적합한 형태이다. 기억을 더듬어 보면 다형성을 처음 배웠을때, 자동차를 예시로 시작했다.
1주차에서 요구사항에 대한 객체 분리를 하는데 어려움을 겪고, SRP(단일 책임 원칙)을 생각하면서 설계하면 더 좋은 설계를 할 수 있다는 것을 알았다. 다른 원칙도 도움을 주지 않을까라는 생각에서 출발하여 DIP(의존 역전 원칙)을 도입해보게 되었다. 직접 구현해보면서 DIP의 이점을 얻을 수 있는 구간이 있었는데, 바로 테스트이다.
테스트에서 랜덤에 대한 처리를 검증하기에는 문제가 있었다. 그래서 랜덤 처리 구간인 랜덤엔진 대신에 가솔린구현체를 통해 테스트를 진행하였다. 랜덤엔진에서 랜덤정보를 주입하여 반드시 성공또는 실패하도록 구현체를 만들 수 있었는데, 전자의 경우 단위테스트이고, 후자의 경우 통합테스트에 가깝기 때문에 테스트 종류가 다르다.
랜덤 로직 테스트
랜덤 로직은 어떻게 테스트해야 할까? 과제를 완성해도 테스트가 실패하는 경우가 있었는데, 랜덤에 따라 결과가 바뀌면서 출력또한 다르게 되는 것이다. 우테코 미션 제출 시스템 내부에서도 테스트를 여러번 돌리는 것으로 추정된다.
랜덤은 불안정한 로직이다. 랜덤에 대한 직접적인 테스트는 어렵다. 랜덤 로직과 관련 없게 테스트를 안정적으로 수행하는 방법으로 다음을 선택했다. (안정적으로 수행했다기 보다는 설계를 하면서 안정적인 형태를 가져가게 객체를 분리하게 되었다.)
- 랜덤 요소 직접 주입
- 캡슐화, 인터페이스를 통한 처리
입출력에 대한 책임
입출력에 대한 책임은 누구에게 있는 것인가? 1주차에도 입출력 관련 코맨트를 받았었다. 모든 입출력이 거이 하나의 메서드에서 실행되었다. 2주차에는 입출력에 대한 책임을 제대로 파악하고 main에 대부분의 입출력을 처리하였다. (리뷰어들이 지적해주실 바랐다.
결국 입출력은 분리해야하는 것이다.
출력에 대해 생각해본다면 크게 1. 출력 데이터를 결정하는것 2. 출력 하는 것 으로 생각해볼 수 있다. 전자는 데이터가 속한 객체와 후자는 출력 객체의 책임으로 생각 했었는데 전자 또한 전자를 위한 역활 객체가 하나 더 필요한 것이다.(데이터의 포맷이나 자료형이 변경되는 상황을 생각해보면 좋다)
입력은 1. 입력 받는 것 2. 입력 받은 데이터를 변환하는 것 3. 데이터를 검증하는 것 총 3개가 있다. 1의 경우 입력 객체, 2의 경우 변환 객체, 3은 데이터각 속한 객체의 책임으로 생각했다.
특히 3의 경우 리뷰하면서 다른 분들 중 대다수가 validate라는 객체를 선언한 것을 볼 수 있다. "검증은 아에 다른 역활인데 분리해야 하지 않을까요?" 라는 의도로 분리한 것으로 알고 있는데 나는 "데이터를 해당 객체 안에서만 사용할 텐데 검증을 다른 곳에서 굳이 해야돼요?" 의견이 였다. 리뷰를 하면서 생각해본 결과 검증에 대해 책임 없다고 생각해볼 수 있을 수도 있다. 검증에 대한 메소드만 생각해도 메소드가 데이터에 대한 처리를 담당하는 객체랑 관련이 없긴하다.
public Car(String name, Engine engine) {
validateName(name);
this.name = name;
this.engine = engine;
}
private void validateName(String name) {
if (name.length() > MAX_NAME_LENGTH) {
throw new IllegalArgumentException(INVALID_CAR_NAME);
}
}
위 메서드는 Car에 있는 내부 private 메소드이다. 데이터의 첫 진입점인 생성자에서 수행된다. 메서드를 자세히 보자 validateName은 이름을 검증한다. 해당 validateName가 Car의 상태를 사용하는가? (상수 제외) 아니다.
여담으로 과제 진행하면서 레드불 플러그태그 대회를 생각하면서 구현했다. 어떤 것들도 엔진이 될 수 있고, 자동차가 될 수 있다.
https://www.youtube.com/watch?v=2iK8HKirT0g
https://www.redbull.com/kr-ko/red-bull-flugtag-seven-memorable-moments