이론적으로는 테스트 코드가 중요하다고 배우지만
실무에서는 '일단 구현부터'가 현실적인 제약이다.
테스트 코드는 정말 효율적일까? 정말 필요한 테스트 코드란 뭘까?
"테스트 코드 짜느라 시간 다 쓰는 거 아냐?"
동료의 이 한 마디에 순간 말문이 막혔다.
머릿속엔 반박할 말들이 떠올랐지만 정리되지 않은 생각들이 목에 걸렸고, 결국 아무 말도 하지 못했다.
그 순간 깨달았다.
정말 테스트가 중요하다고 생각한다면, 왜 필요한지 설명할 수 있어야 하는 거 아닌가?
나 스스로도 모르면서, 그게 당연히 필요하다는 듯 생각하고 있었다는 생각에 반성이 되었다.
@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("테스트");
}
이 테스트는 내부 구현을 몰라도 뭘 기대하는지 명확하다:
사양서(specification)처럼 동작하고,
실패했을 때도 원인을 빠르게 파악할 수 있다.
요구사항 (당시 내가 이해한 것):
텍스트 입력→외부 서비스에 TTS 요청→오디오(.wav) 파일 응답
테스트도 했고, 직접 호출해서 들었을 때도 잘 나왔다.
문제 없어 보였다.
그런데 실제 클라이언트 연동에서는 오디오가 재생되지 않았다.
분명 같은 파일인데, 뭐가 문제였을까?
파일 확장자는 .wav라서 문제 없어 보였다.
근데 내부의 sample rate가 달랐다.
외부 TTS 서비스는 8kHz로 생성했고,
클라이언트는 16kHz만 지원했다. 결국 재생 불가.
부랴부랴 샘플레이트 확인하고, 호환 가능한지 검증하는 로직을 추가했다.
수정은 어렵지 않았다.
근데 문제는 그걸 테스트할 수 없었다는 점이다.
public class TtsService {
public byte[] convertTextToSpeech(String text) {
ExternalTtsClient externalTtsClient = new ExternalTtsClient();
return externalTtsClient.call(text);
}
}
이런 구조에서는:
아주 비효율적인 테스트만 가능했던 것이다.
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);
});
}
}
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);
}
}
}
TDD는 테스트를 먼저 작성하는 게 핵심이 아니다.
중요한 것은 바로 테스트가 설계를 이끌도록 만드는 것이다.
TTS 사례에서 다룬 것 처럼 TDD와 테스트코드는
결국 테스트를 잘 짠다는 건,
단순히 코드를 검증하는 게 아니라
책임을 명확히 나누고 구조를 설계하는 일이다.
이제 누군가 테스트코드가 왜 중요하냐고 묻는다면 설득력있게 답변할 수 있게 되었다!