본문 바로가기
우테코/6기

테스트하기 어려운 코드, 전략 패턴을 사용하는 것이 늘 좋은 것일까?

by nak_honest 2024. 2. 21.

 

우테코에 들어가자마자 첫번째로 구현하게 된 미션은 자동차 미션이었다.

프리코스 때에도 이미 한번 풀어보았던 미션이었다.

 

 

https://nakhonest.tistory.com/entry/%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EA%B8%B0-%EC%96%B4%EB%A0%A4%EC%9A%B4-%EC%BD%94%EB%93%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EC%95%BC%ED%95%98%EC%A7%80%EB%9E%9C%EB%8D%A4%EC%97%90-%EB%8C%80%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8

 

테스트 하기 어려운 코드! 어떻게 해야하지?(랜덤에 대한 테스트)

프리코스 2주차 미션부터는 각 기능에 대한 테스트 코드를 작성해야 했다. 이에 따라 JUnit5와 AssertJ를 학습하고, TDD를 적용해보려 노력했다. 사실 이 라이브러리를 학습하고, 테스트 코드를 작성

nakhonest.tistory.com

 

프리코스 당시에는 위의 글 내용처럼 랜덤 숫자에 대한 테스트가 어려워 랜덤 숫자에 대한 부분을 자동차로부터 분리하였다.

이때 전략패턴을 이용해 랜덤 숫자와 자동차의 움직임을 분리하였다. 자동차가 움직이는 로직을 DriveStrategy 라는 인터페이스로 추상화하여 구현체를 주입받도록 구현하였다.

 

이를 통해 분명 다양한 방식으로 움직이는 자동차 객체를 손쉽게 만들 수 있게 되었다.

사용자가 입력한 숫자만큼 움직이게 한다던지, 랜덤으로 뽑은 숫자만큼 움직이게 하는 등 다른 방식으로 움직여야 하는 자동차가 필요할 때 DriveStrategy 인터페이스의 구현체만 갈아끼우면 되기 때문이다.

 

 

하지만 미션을 진행하고, 다양한 피드백을 받으면서 현재의 소프트웨어에서는 전략 패턴을 이용하는 것이 오버엔지니어링이라고 느껴졌다.

현재의 요구사항에서는 자동차를 움직이는 방식이 단 하나만 존재하는데, 전략 패턴을 이용해 코드의 복잡성만 증가시켰기 때문이다.

 

전략 패턴의 가장 큰 장점은 런타임에 구현체를 갈아 끼움으로써 객체의 행동을 바꿀 수 있다는 것인데, 이번 미션에서는 자동차의 행동이 하나밖에 존재하지 않으므로 과도한 추상화라고 느껴졌다. 물론 이후의 변화를 고려하는 것은 분명 좋은 습관이지만, 과하게 추상화하게 된다면 오히려 코드의 복잡성을 증가시키고 이는 유지보수성을 떨어 뜨리는 결과를 낸다고 생각하게 되었다.

 

현재는 겨우 객체 하나에 대해서만 추상화를 한 것이지만, 만약 클래스가 수천개가 존재하는데 이를 하나 하나 다 추상화한다면 소프트웨어는 훨씬 복잡하게 될 것이다.

 

 

그리고 엄밀하게 따지고 보면 사실상 이 코드는 전략 패턴도 아니라고 생각한다.

전략 패턴은 객체의 다양한 행동을 추상화하여, 이를 동적으로 갈아 끼울 수 있게 해주는 디자인 패턴이다.

 

하지만 현재 자동차의 움직임은 다양하지 않기 때문에 이를 추상화 한다고 하더라도 전략 패턴이라고 보기는 어렵다고 생각한다.

 

 

따라서 먼저는 자동차 객체에서 전략 패턴을 빼고, 주어진 거리만큼 움직이는 작은 책임만 부여하였다.

public class Car {
    private final String name;
    private final int position;

    private Car(String name, int position) {
        this.name = name;
        this.position = position;
    }

    public void move(int distance) {
        position += distance;
    }
}

 

이제 자동차는 어떠한 복잡한 로직도 없이 그저 주어진 거리만큼 움직이는 단순한 역할을 수행한다.

 

 

하지만 우리의 자동차 미션에서는 자동차를 랜덤으로 움직일 수 있어야 한다.

랜덤 숫자를 생성해서 이 자동차가 움직이도록 하는 것은 누구의 역할이 될 수 있을까?

 

현실 세계에서는 랜덤으로 움직이는 자동차는 존재하지 않는다.

하지만 소프트웨어에서는 충분히 존재할 수 있다.

그러한 역할을 수행하는 객체를 만들기만 하면 될뿐이다.

 

 

따라서 RandomMovingCar 라는 객체를 만들기로 결정하였다.

그리고 이 객체에게 랜덤으로 자동차를 움직이게 하는 역할을 부여하였다.

 

이를 위해서 RandomMovingCar가 Car를 컴포지션 하도록 구현하였다.

그리고 테스트를 위해 랜덤 숫자를 생성하는 로직만 함수형 인터페이스로 주입받도록 구현하였다.

public class RandomMovingCar {
    private static final int MIN_POWER = 0;
    private static final int MAX_POWER = 9;
    private static final int POWER_THRESHOLD = 4;
    private static final int DRIVE_FORWARD_DISTANCE = 1;

    private final Car car;
    private final IntSupplier powerGenerator;

    public RandomMovingCar(Car car, IntSupplier powerGenerator) {
        this.car = car;
        this.powerGenerator = powerGenerator;
    }

    public void move() {
        int power = powerGenerator.getAsInt();

        if (power >= POWER_THRESHOLD) {
            car.move(DRIVE_FORWARD_DISTANCE);
        }
    }

    public static int generateRandomPower() {
        return Randoms.pickNumberInRange(MIN_POWER, MAX_POWER);
    }
}

여기서 상속이 아닌 컴포지션을 선택한 이유는 두 객체가 "is-a" 관계가 아니라고 생각하였기 때문이다.

 

Car는 주어진 거리만큼 움직인다. 하지만 RandomMovingCar는 랜덤값에 따라 "스스로" 움직인다. 즉 움직이는 거리를 스스로가 결정한다는 것이다.

 

나는 이 둘의 관계가 상속이 아닌 컴포지션이 되어야 한다고 생각하였다.

따라서 랜덤 숫자를 생성해서 움직일지 말지는 RandomMovingCar가 담당하고, 움직이는 것에 대한 책임은 Car에게 위임시키도록 구현하였다.

(자세한 내용은 "이펙티브 자바 아이템 18 상속보다는 컴포지션을 사용하라"을 참고하자.)

 

 

이렇게 전략패턴을 제거하고 컴포지션으로 구현하니 이전보다 코드의 복잡성이 줄어들게 되었다.

 

실제로도 이에 대한 생각을 공유하니 리뷰어 분께서 긍정적인 피드백을 남겨주셨다 :)

 

마무리

결론적으로 이를 통해 배운 점은,

현재 당장 필요하지 않은 부분까지 유지보수성을 챙기고 추상화하는 것은

소프트웨어의 복잡성만 증가시키게 될 수 있다는 것이었다.

 

즉 유지보수성, 복잡성, 성능 등은 트레이드-오프 관계일 때가 많기 때문에

상황에 맞게 그리고 현재 소프트웨어 요구사항에 맞게 적절하게 잘 선택해야 한다는 것이다.

 

 

그리고 사실 유지보수성을 챙기기 위해 쓰이게 되는 시간과 커뮤니케이션도 전부 비용이다.

지금은 우테코라는 플레이그라운드에 있기 때문에 이러한 비용을 덜 고려해도 큰 문제가 되지는 않지만,

실제 현업에 가서는 시간과 커뮤니케이션 비용도 늘 고려해야 할것이다.

 

이후의 변화를 고려하는 것은 분명 좋은 습관이지만, 과하게 추상화하고 유지보수성을 챙기는 것은 경계해야겠다는 것을 깨달을 수 있었다.

앞으로 개발을 할때 다양한 리소스를 고려하여 해당 상황에서 최적의 선택을 하는 연습을 게속 해 나가야겠다.

'우테코 > 6기' 카테고리의 다른 글

들풀의 그늘(레벨 2 글쓰기 미션)  (0) 2024.07.15