Coderoad

프리코스 2주 차를 돌아보며

2023-11-03 at 우아한테크코스 category

프리코스 2주 차 미션 - 자동차 경주 게임

1주 차 미션이 끝나고 회고를 작성 중이었던 10월 26일 목요일 오후 3시, 2주 차 미션에 대한 안내가 메일로 도착했습니다. 1주 차 미션을 무사히 완료한 프리코스 참가자들에게 주어진 2주 차 미션은 '초간단' 자동차 경주 게임이었습니다. 물론, 요구 사항에 적힌 내용은 전혀 간단하지 않았습니다. 1주 차보다 주어진 요구 사항도 많아졌고 신경 써야 하는 부분이 더 늘어났습니다. 그래도 1주 차 미션 이후, 프리코스 미션 진행 방식에 감이 잡힌 덕분에 구현 계획을 빠르게 세울 수 있었습니다.

먼저, 구현 기능 목록을 작성하고 이를 바탕으로 클래스들을 구현하기 시작했습니다.

function_list
구현 기능 목록을 가장 먼저 작성

확실히 1주 차와 다르게 머릿속에 있던 클래스 설계도를 문서화 해두니 구현하면서 혼동이 오는 일도 없었고, 더 빠르게 구현할 수 있었습니다. 예상외로 자동차 경주 게임 자체는 구현이 정말 빨리 끝났습니다.

구현은 빠르게 끝났지만...

그러나 정말 큰 난관은 자동차 경주 게임 구현이 아니었습니다. 2주 차에 추가된 요구 사항들이야말로 저에게 주어진 진정한 미션이었습니다. 가장 먼저 저를 괴롭혔던 요구 사항은 기능 단위 커밋이었습니다. 구현이 정말 빨리 끝났기 때문에, 오히려 기능 단위 커밋을 어떻게 해야 할지 애매했습니다. 이미 모든 기능이 1차적으로 완성된 상황에서 공통된 역할과 책임으로 묶인 모델 단위로 커밋을 해야 하는 건지, 아니면 정말 '기능'을 담당하는 메소드 단위로 커밋을 해야 하는 건지 고민이 많았습니다.

car_model_commit
모델(객체) 단위로 커밋했던 2주 차 미션

그래서 저는 기능 단위 커밋을 '역할과 책임 단위 커밋'으로 제 나름대로 해석했습니다. 객체지향 프로그래밍이 역할과 책임을 가지는 자율적인 객체들의 협력을 통해 사용자가 원하는 결과를 도출하는 것이므로, 객체의 역할과 책임이 곧 기능이라고 생각했습니다. 더군다나, SOLID단일 책임 원칙을 지키기 위해서는 한 모델(객체)이 하나의 책임(기능)만 가지고 있어야 하므로, 기능 단위 커밋은 결국 모델 단위로 커밋하는 것과 같다고 볼 수 있습니다.

실제로 제가 설계한 Car 모델은 자동차의 이름과 이동거리 정보를 가공한 데이터를 다른 객체에 제공하는 책임(기능)을 가지고 있습니다. 상세하게는, 현재 상황에 대한 데이터, 다른 Car 객체와의 이동거리 비교 데이터, 자동차 이름에 대한 데이터를 제공합니다. 이렇게 기능 단위 커밋 요구 사항은 지킬 수 있었습니다.

진짜 미션, 테스트 코드 작성.

기능 단위 커밋은 그래도 스스로 판단해서 해낼 수 있었지만, 테스트 코드 작성은 정말 막막했습니다. 테스트를 직접 구현해봤던 경험도 거의 없었고, 그나마 직접 구현했던 테스트들도 클론 코딩이었으니, 어쩌면 당연한 일이었습니다. 그래도 JUnit의 대략적인 사용법은 알고 있었기 때문에 주어진 테스트 예제와 이전에 구현해봤던 테스트들을 다시 살펴보며 테스트 구현을 시작했습니다.

그러나, 이전의 테스트 코드들은 저에게 큰 도움이 되지 못했습니다. 처음으로 구현하기 시작한 테스트부터 정말 난감했습니다. 자동차 경주 게임은 우테코에서 제공한 Randoms 라이브러리의 난수를 통해 자동차의 전진과 정지를 결정하게 됩니다. 문제는, 발생할 난수를 예측할 수 없으니 자동차가 전진할지, 정지할지 알 수 없었고, 당연히 테스트도 불가능했습니다. 그래서 저는 어떻게 하면 난수가 포함된 기능을 테스트할 수 있을지 고민하기 시작했습니다.

구글링을 통해 열심히 학습한 결과, 제가 생각하는 난수에 대한 테스트 방식은 다음과 같습니다.

  • 난수를 생성하는 기능을 별도의 클래스로 분리해서 자동차의 전진은 해당 클래스로부터 난수를 매개변수로 받아 수행. (메소드 시그니처 수정)
  • 난수를 생성하는 외부 라이브러리를 신뢰하고, 주어진 범위 안에서 난수가 생성될 것임을 전제로 테스트. (외부 라이브러리 신뢰)

저는, 위의 방식들을 통해 테스트를 구현하고자 기존에 구현을 완료했던 모델들을 다시 살펴봤습니다.

먼저, 테스트 코드 작성 전, Car 모델의 자동차의 이동을 담당하는 메소드입니다.

public void moveCar() {
    int moveCondition = Randoms.pickNumberInRange(lowerBound, upperBound);
    if (moveCondition >= MOVE_CRITERIA.value) {
        distance++;
    }
}

매번 자동차를 움직이려고 시도할 때마다, 난수를 생성하고 난수가 조건에 부합하면 자동차의 이동거리를 증가시켰습니다. 이렇게 메소드를 작성해도 자동차 경주 게임이 작동하는 데는 문제가 없었습니다. 그러나, 이 메소드는 치명적인 문제가 있었습니다. 제가 제어할 수 없는 부분이 코드 깊숙이 존재한다는 점입니다. (메소드 내부에 있기 때문에 외부에서 난수를 원하는 대로 설정할 방법이 없습니다.)

또한, Car 모델에 '자동차의 데이터를 제공한다.'라는 책임에 '이동을 위한 난수를 생성한다.'는 책임까지 부여됐습니다. 즉, 단일 책임 원칙이 위반된 것입니다. 테스트를 위해 로직을 수정한다는 것은 좋지 못한 방식이라고 하지만, 이 부분은 좋은 객체지향 프로그래밍을 위해서라도 고쳐야 했습니다.

public void moveCar(int moveCondition) {
    if (moveCondition >= MOVE_CRITERIA.value) {
        distance++;
    }
}

앞서 알아본 난수에 대한 테스트 방식을 적용해 메소드의 시그니처를 수정하고 난수의 생성을 다른 클래스로 분리했습니다. 이전과 다르게 자동차는 직접 난수를 생성하는 것이 아닌, 외부로부터 난수를 전달받아 조건에 맞춰 전진 혹은 정지하게 되었습니다. 덕분에 난수 라이브러리와 상관없이 자동차의 이동에 관련된 테스트를 구현할 수 있게 됐습니다.

난수 생성 라이브러리가 주어진 범위 내의 난수를 제대로 생성하는지도 테스트 해봐야 하지 않을까 생각했지만, 외부 라이브러리는 의도대로 동작한다는 전제하에 자동차 경주 게임을 구현했기 때문에, 여기까지 신경 쓸 필요는 없다고 생각했습니다.

개인적인 의견으론, 제가 직접 구현하지 않은 코드를 테스트하는 것은 불필요한 작업인 것 같습니다.

이렇게 테스트 구현에 2배 정도 더 시간을 쏟고 나서야 주어진 요구 사항에 맞게 2주 차 미션을 완료했습니다.

개발 습관을 돌아보게 된 2주 차 미션

테스트 구현도 어떻게든 해내고 미션을 제출하고 나니 정말 많은 생각이 들었습니다. '과연 내가 제대로 테스트를 구현한 걸까?', '이런 사소한 것까지 테스트 코드를 작성하는 게 맞을까?' 등, 누군가 속 시원하게 알려주는 사람도 없다 보니 제출하고 나서도 해냈다는 느낌이 크게 들지 않았습니다. 또한, 테스트를 먼저 작성한 것이 아닌, 이미 구현된 객체들을 테스트해 테스트를 위한 코드 수정도 발생했습니다.

테스트를 위해 서비스 로직을 수정한다는 것이, 주객 전도된 것 같았습니다. 분명 내 코드를 신뢰하기 위해 테스트를 구현하는 것이라고 공부했는데, 테스트가 제대로 동작하도록 내 코드를 수정하고 있었습니다. 그래도 좋은 경험이었다고 생각합니다. 그동안 제가 얼마나 테스트를 등한시했는지, 돌아가면 장땡이라는 안일한 생각으로 개발했는지 되돌아보게 되었습니다. 좋은 코드와 더불어 좋은 테스트도 중요하다는 것을 알았으니 앞으로는 테스트 주도 개발에 대해서 더 공부해볼 생각입니다.

Repository

자동차 경주 게임 - hangillee

hangillee

Personal blog by hangillee.

Road to good developer.