Concept/테스트코드

내부 구조에 기대지 않고 외부에 드러나는 동작을 확인하자

devstep 2025. 4. 24. 19:00

자동차들이 "주어진 횟수만큼 이동 전략을 수행했는가?"에 대한 기능을 검증하려고 했다.

이는 자동차들이 매 회차마다 move()를 호출하는지를 확인하는 테스트로,
해당 로직은 자동차 경주 게임의 흐름을 제어하는 Game 클래스에서 수행된다.

따라서 테스트의 대상은 Game 클래스이며, 각 클래스의 역할은 다음과 같이 정리할 수 있다.

  • RaceCount는 단순히 반복 횟수를 관리하고
  • Cars는 자동차들을 한 번 움직이는 동작만 담당하며
  • Game은 Game은 RaceCount와 Cars를 조합하여 경주를 진행한다.

즉, raceCount 횟수만큼 cars.move()를 호출하는 책임은 Game 클래스에게 있다.

 

 

✅ 검증할 코드 

그런데 막상 검증할 코드를 볼 때는 Game.start() 내부에서 raceCount.play()가 호출되기도 하고, raceCount가 회차를 관리하는 클래스라  "raceCount.play()가 몇 번 호출되었는지를 검증해야 하는 것 아닌가"라는 생각이 들었다. 

class Game 
public List<RaceRoundResult> start() {
    List<RaceRoundResult> responses = new ArrayList<>();

    while (!raceCount.isDone()) {
        List<Position> positions = raceCount.play(() -> cars.move());
        responses.add(
                new RaceRoundResult(
                        positions.stream()
                                .map(PositionResponse::new)
                                .collect(Collectors.toList())
                )
        );
    }

    return responses;
}

 

그래서 관련 의문을 gpt에게 물어봤고 아래와 같은 결론을 얻었다. 

테스트는 외부에서 보이는 행동(기능)을 검증하는 것이 이상적입니다.

  • cars.move()는 "자동차가 이동했다"는 결과가 바깥에 드러나는 행위
  • raceCount.play()는 "게임 내부에서 횟수를 줄이기 위해 호출된 유틸 메서드" → 내부 구현에 가까움

 

정리하면,  테스트하려고 했던 기능은 "자동차들이 주어진 횟수만큼 이동 전략을 수행했냐"는 것이다. 그것에 대한 실질적인 메서드는 cars.move()이다. raceCount.play()는 Game.start()의 내부 사항일 뿐이다.

 


✅ 근거 자료  

"내부 구조에 기대지 않고 외부에 드러나는 동작을 확인하자"는 관점을 답변으로 준 gpt에게 이런 내용은 어디서 기반한 것인지 물어봤다.

이런 내용은 "좋은 테스트란 무엇인가?", "테스트의 유지보수성과 신뢰성", "객체지향 설계에서 테스트를 어떻게 바라보는가?"와 관련돼 있어요.


그리고 책도 추천받았다. 

📚 이런 내용을 다루는 대표적인 책들

1. 《테스트 주도 개발(Test-Driven Development: By Example) - Kent Beck》

  • 테스트의 목적은 동작을 검증하는 것이라는 철학이 반복해서 나와요.
  • 테스트는 외부에서 보이는 인터페이스에 집중해야 한다는 메시지가 명확히 나와요.
  • 내부 구현 변경에도 테스트가 깨지지 않도록 작성하는 것이 중요하다고 강조합니다.

✅ 우리가 이야기한 "내부 구현 확인 테스트는 피하자"는 핵심 철학이 이 책의 중심이에요.


2. 《클린 코드(Clean Code) - Robert C. Martin》

  • 테스트 코드는 "설계에 대한 두 번째 사용자" 라고 표현합니다.
  • 구현 방식이 바뀌어도 테스트가 깨지지 않아야 한다는 주장을 펼쳐요.
  • 특히, 구현 세부사항을 테스트하지 마라는 챕터가 있어요.

3. 《xUnit Test Patterns - Gerard Meszaros》

  • 테스트 코드 자체의 패턴과 안티패턴에 대해 매우 상세하게 설명해요.
  • 특히 다음 개념들이 중요하게 다뤄져요:
    • Behavior Verification (행동 검증) vs State Verification (상태 검증)
    • 테스트가 구현 세부사항에 너무 의존적이면 brittle test (잘 깨지는 테스트)가 됨

RaceCount.play() 호출 횟수를 확인하는 건 행동 검증처럼 보이지만,

실제로는 구현 방식 검증이라서 잘 깨지는 테스트가 될 수 있다는 예시로 여겨져요.


📖 관련 분야

  • TDD (Test-Driven Development)
  • 객체지향 설계와 테스트
  • 유닛 테스트 원칙 (SOLID와 맞물림)
  • 테스트 유지보수 전략

 


✅ 추가로 한 고민

그럼 굳이 Game.start()에서 RaceCount.play()를 호출하지 않고,
Cars.move()를 호출하면서 그 안에서 RaceCount.reduce()를 실행해도 되는 게 아닐까라는 생각이 들었다.