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

2025. 4. 24. 19:00·Concept/테스트코드

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

이는 자동차들이 매 회차마다 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()를 실행해도 되는 게 아닐까라는 생각이 들었다.

저작자표시 비영리 변경금지 (새창열림)

'Concept > 테스트코드' 카테고리의 다른 글

[TDD] 테스트 코드 작성  (0) 2025.07.18
id가 없는 상태의 객체 생성을 위한 편의 생성자  (0) 2025.04.25
테스트주도개발(TDD) 시작하기  (3) 2024.12.22
'Concept/테스트코드' 카테고리의 다른 글
  • [TDD] 테스트 코드 작성
  • id가 없는 상태의 객체 생성을 위한 편의 생성자
  • 테스트주도개발(TDD) 시작하기
devstep
devstep
웹 백엔드 개발자
  • devstep
    개발 여정
    devstep
  • 전체
    오늘
    어제
    • 분류 전체보기 (91)
      • Java (24)
      • Spring Framework (17)
        • Spring (14)
        • JPA (3)
      • Database (8)
        • RDBMS공통 (1)
        • MySQL (6)
        • Redis (0)
        • Oracle (1)
      • Concept (13)
        • 테스트코드 (4)
        • 클린코드 (2)
        • 성능테스트 (4)
        • 설계 (1)
        • 인증 (1)
        • REST API (1)
      • git (2)
      • Intellij (4)
      • Computer Science (3)
        • 네트워크 (1)
        • 자료구조 (1)
        • 보안 (1)
      • Essay (18)
        • Learning Essay (10)
        • WIL (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    tdd
    seed
    linux
    부하테스트도구
    비대칭암호화
    applicationcontext
    테스트코드
    JMeter
    부하테스트
    블록암호화
    단위테스트
    JavaMemoryModel
    성능테스트
    aggregate
    storageEngine
    bean
    nginx
    springsecurity
    클린코드
    DDD
    인텔리제이실행에러
    보안
    대칭암호화
    nofile
    innodb
    component
    자바메모리모델
    JVM
    ClusteredIndex
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
devstep
내부 구조에 기대지 않고 외부에 드러나는 동작을 확인하자
상단으로

티스토리툴바