본문 바로가기
우아한테크코스

[우아한테크코스] 레벨1 - 자동차 경주게임에서 인터페이스를 통해 테스트 가능한 코드 만들기

by 무늬 2020. 4. 16.

 

 

우아한테크코스(이하 우테코) 레벨1의 2주차 미션은 자동차 경주게임이였습니다. 프리코스 때의 2번째 미션과 동일한 내용이였는데요. 우테코에서 배운 내용을 토대로 프리코스 때 제출한 코드를 보다 객체지향적으로 리팩토링 해보겠습니다.

 

 

 

자동차 경주게임의 요구사항 중 한가지는 '1~9까지의 랜덤한 정수를 뽑아 4이상일 경우 전진한다.'입니다.

아래의 코드는 프리코스 때 제출한 코드의 일부 입니다.

public class Cars {
    public void play() {
        for (Car car : cars) {
            if (getRandomNumber() >= 4) {
                car.proceed();
            }
        }
    }

    private int getRandomNumber() {
        return new Random().nextInt(MAX_OF_RANDOM_NUMBER + 1);
    }
}

 

이 코드의 문제점은 자동차가 전진하는 방식이 바뀔 때 Cars 코드를 변경해야 한다는 점입니다. 그리고 getRanmdomNumber()의 결과값이 랜덤하게 결정되기 때문에 테스트코드 작성도 어렵습니다.

 

 

 

우선 변경이 일어날 것으로 예상되는 자동차가 전진하는 방식을 추상화합니다. 추상화를 구현하는 방법은 여러가지가 있지만 여기서는 interface를 활용해보겠습니다.

public interface MoveStrategy {
    boolean canMove();
}

 

 

 

그리고 랜덤하게 뽑은 숫자가 4이상일 경우 전진하는 방식을 MoveStrategy의 구현체로 만듭니다.

public class RandomStrategy implements MoveStrategy {
    public boolean canMove() {
    	return getRandomNumber() >= 4;
    }
    
    private int getRandomNumber() {
        return new Random().nextInt(MAX_OF_RANDOM_NUMBER + 1);
    }
}

 

 

 

위의 RandomStrategy 코드는 원래 Cars에 있던 코드입니다. 이제 RandomStrategy라는 별도의 클래스로 분리했으니 Cars에는 이 부분을 삭제하고, RandomStrategy를 주입받아 이 기능을 실행합니다.

public class Cars {
    public void play() {
        for (Car car : cars) {
        	MoveStrategy moveStrategy = new RandomMoveStrategy();
            if (moveStrategy.canMove()) {
            	car.proceed();
            }
        }
    }
}

 

 

 

하지만 RandomStrategy가 아닌 다른 MoveStrategy의 구현체를 사용할 경우 여전히 Cars에서는 변경이 일어납니다. 그래서 Cars의 생성자에서 MoveStrategy를 주입받도록 코드를 수정하겠습니다.

public class Cars {
    private List<Cars> cars;
    private MoveStrtegy moveStrategy;
    
    public Cars(List<Cars> cars, MoveStrategy moveStrategy) {
      this.cars = cars;
      this.moveStrategy = moveStrategy;
    }

    public void play() {
        for (Car car : cars) {
            if (moveStrategy.canMove()) {
            	car.proceed();
            }
        }
    }
}

 

아래는 Cars 객체를 생성하는 코드입니다. 여기서 MoveStrategy의 구현체를 생성 및 주입하고 있습니다.

public class RacingCarApplication {
    public static void main(String[] args) {
        List<Cars> carValues = Arrays.asList(...) // Car객체의 생성 코드는 생략
        MoveStrategy moveStrategy = new RandomStrategy();
        Cars cars = new Cars(carValues, moveStrategy);
    }
}

위와 같은 구조의 경우, 자동차가 전진하는 방식이 바뀌더라도 Controller에서 주입하는 인스턴스만 변경되고 domain의 코드는 변경되지 않습니다.

 

 

 

그리고 테스트코드 작성 시 랜덤한 결과를 반환하는 RandomStrategy 대신, 항상 전진하는 결과를 반환하는 AlwaysMoveStrategy 같은 구현체를 만들어 활용할 수 있습니다.

public class AlwaysMoveStrategy implements MoveStrategy {
    public boolean canMove() {
    	return true;
    }
}

다만 위의 구현체는 오직 테스트를 위해 만들어진 클래스이기 때문에, production 패키지가 아닌 test패키지에 넣어줍니다.

변경에 유연하게 대처할 수 있도록 코드의 구조를 변경하다 보니 자연스럽게 테스트하기 쉬운 코드가 되었습니다. 혹시 코드를 작성하다가 테스트하기 어렵다고 느껴진다면, 그 코드가 객체지향적이지 않을 수 있다는 말이 다시금 떠오르네요.

 

 

 

자동차 경주게임은 객체지향프로그래밍, 전략패턴, 테스트코드 등 여러가지 주제를 생각할 수 있는 미션이었습니다. 미션을 수행할 당시에는 새롭게 접한 개념들이 많아 어렵게 느껴졌었는데, 글로 풀어내다보니 머릿속이 정리되는 것 같아 뿌듯하네요. 다음 미션에서는 본 내용들을 좀 더 자연스럽게 사용할 수 있기를 기대해봅니다!