테스트 코드는 설계 도구다 - TDD로 깨달은 진짜 개발

묘니·2025년 7월 17일

TL;DR

이론적으로는 테스트 코드가 중요하다고 배우지만
실무에서는 '일단 구현부터'가 현실적인 제약이다.
테스트 코드는 정말 효율적일까? 정말 필요한 테스트 코드란 뭘까?


테스트 코드, 정말 필요한가?

"테스트 코드 짜느라 시간 다 쓰는 거 아냐?"

동료의 이 한 마디에 순간 말문이 막혔다.

머릿속엔 반박할 말들이 떠올랐지만 정리되지 않은 생각들이 목에 걸렸고, 결국 아무 말도 하지 못했다.

그 순간 깨달았다.
정말 테스트가 중요하다고 생각한다면, 왜 필요한지 설명할 수 있어야 하는 거 아닌가?

  • 왜 테스트를 짜야 하는지
  • 어떤 테스트가 진짜 의미가 있는지
  • 어디까지 짜면 과하고, 어디까지는 꼭 필요한지

나 스스로도 모르면서, 그게 당연히 필요하다는 듯 생각하고 있었다는 생각에 반성이 되었다.

의미 없는 테스트 vs 좋은 테스트

❌ 의미 없는 테스트

@Test
void isUserSaved() {
    userRepository.save(new User(...));
}

이 테스트는 실패할 가능성이 거의 없다.
실패하지도 않고, 테스트의 의도조차 파악하기 어렵다.

그냥 save()를 잘 호출했어.
...그래서 뭐?

이런 테스트는 시간만 잡아먹고, 얻는 건 거의 없고,
나중엔 테스트 코드 고치느라 더 고생하게 된다.

"이런 건 디버깅으로 한 번에 확인하는 게 낫지 않냐."

테스트코드 반대론자의 이 말에 반박할 수 없는 테스트 코드인 거다.

✅ 좋은 테스트

그렇다면 좋은 테스트는 뭘까?

좋은 테스트는 단순한 확인용 코드가 아니다.

  • 요구사항을 문서화하고
  • 코드의 의도를 명확하게 보여주는 것
@Test
void returnsUserInfo_whenUserExists() {
    // given
    userService.signUp(new SignUpCommand("test123", "테스트"));

    // when
    var info = userService.getMyInfo("test123");

    // then
    assertThat(info.name()).isEqualTo("테스트");
}

이 테스트는 내부 구현을 몰라도 뭘 기대하는지 명확하다:

  • 어떤 조건에서 (회원가입 후)
  • 어떤 입력을 주면 (사용자 ID)
  • 어떤 결과를 기대하는지 (사용자 정보 반환)

사양서(specification)처럼 동작하고,
실패했을 때도 원인을 빠르게 파악할 수 있다.

실전에서 테스트가 없을 때 생긴 일

🧪 TTS(Text-to-Speech) API 구현 사례

요구사항 (당시 내가 이해한 것):
텍스트 입력외부 서비스에 TTS 요청오디오(.wav) 파일 응답

  1. 클라이언트로부터 텍스트를 입력받고
  2. 외부 TTS 서비스에 요청해서 오디오 파일을 생성한 뒤
  3. 그 파일을 클라이언트에 반환하는 API를 만들었다

테스트도 했고, 직접 호출해서 들었을 때도 잘 나왔다.
문제 없어 보였다.

그런데 실제 클라이언트 연동에서는 오디오가 재생되지 않았다.

분명 같은 파일인데, 뭐가 문제였을까?

❌ 문제 상황

파일 확장자는 .wav라서 문제 없어 보였다.
근데 내부의 sample rate가 달랐다.

외부 TTS 서비스는 8kHz로 생성했고,
클라이언트는 16kHz만 지원했다. 결국 재생 불가.

부랴부랴 샘플레이트 확인하고, 호환 가능한지 검증하는 로직을 추가했다.

🤔 진짜 문제는 "테스트할 수 없는 구조"

수정은 어렵지 않았다.
근데 문제는 그걸 테스트할 수 없었다는 점이다.

public class TtsService {
    public byte[] convertTextToSpeech(String text) {
        ExternalTtsClient externalTtsClient = new ExternalTtsClient();
        return externalTtsClient.call(text);
    }
}

이런 구조에서는:

  • 매번 실제 외부 TTS 서버에 요청해야 하고
  • 샘플레이트 테스트도 수동으로 들어봐야만 했다

아주 비효율적인 테스트만 가능했던 것이다.

구조 개선: 테스트 가능한 방향으로

Step 1. 실패 케이스부터 테스트 작성

class TtsServiceTest {

    @Test
    void shouldConvertToTargetSampleRateIfMismatched() {
        when(mockTtsClient.call("안녕하세요"))
            .thenReturn(createWavFile(8000));

        AudioResponse result = ttsService.convertTextToSpeech("안녕하세요", 16000);

        assertThat(result.getSampleRate()).isEqualTo(16000);
    }

    @Test
    void shouldNotConvertIfSampleRateMatches() {
        when(mockTtsClient.call("테스트"))
            .thenReturn(createWavFile(16000));

        AudioResponse result = ttsService.convertTextToSpeech("테스트", 16000);

        assertThat(result.getSampleRate()).isEqualTo(16000); // 변환 없음
    }

    @Test
    void shouldHandleExternalServiceFailure() {
        when(mockTtsClient.call("에러"))
            .thenThrow(new TtsServiceException("외부 서비스 오류"));

        assertThrows(TtsProcessingException.class, () -> {
            ttsService.convertTextToSpeech("에러", 16000);
        });
    }
}

Step 2. 테스트를 통과하는 구조로 설계

public class TtsService {
    private final TtsClient ttsClient;
    private final AudioConverter audioConverter;

    public TtsService(TtsClient ttsClient, AudioConverter audioConverter) {
        this.ttsClient = ttsClient;
        this.audioConverter = audioConverter;
    }

    public AudioResponse convertTextToSpeech(String text, int targetSampleRate) {
        try {
            byte[] raw = ttsClient.call(text);
            int actualSampleRate = WavUtils.extractSampleRate(raw);

            if (actualSampleRate != targetSampleRate) {
                return audioConverter.convertToTargetSampleRate(raw, targetSampleRate);
            }

            return new AudioResponse(raw, actualSampleRate);

        } catch (TtsServiceException e) {
            throw new TtsProcessingException("TTS 처리 중 오류", e);
        }
    }
}

개선 효과

  • 외부 의존성(Mock 처리 가능)
  • 다양한 실패 케이스 시뮬레이션 가능
  • 빠른 피드백 루프
  • 클라이언트 사양에 맞춘 샘플레이트 처리 가능
  • 설계가 깔끔해지고 테스트가 쉬워짐

✨ 테스트는 설계 도구다

TDD는 테스트를 먼저 작성하는 게 핵심이 아니다.
중요한 것은 바로 테스트가 설계를 이끌도록 만드는 것이다.

TTS 사례에서 다룬 것 처럼 TDD와 테스트코드는

  • 실패 케이스를 미리 고민하게 만들고
  • 테스트 가능한 구조로 유도하고
  • 빠르게 검증하고 수정할 수 있게 해준다

결국 테스트를 잘 짠다는 건,

단순히 코드를 검증하는 게 아니라
책임을 명확히 나누고 구조를 설계하는 일이다.

이제 누군가 테스트코드가 왜 중요하냐고 묻는다면 설득력있게 답변할 수 있게 되었다!

0개의 댓글