한 줄 정의
코드가 올바르게 동작함을 보장하는 궁극적인 방법은 테스트이고, 그중 개발자에게 빠른 피드백을 주며 리팩터링을 두렵지 않게 하는 것은 개발자가 직접 작성하는 단위 테스트입니다.
쉽게 말하면
요리사가 음식을 확인하는 방법은 두 가지입니다. 하나는 완성된 접시를 손님에게 내고 평가를 기다리는 것이고, 다른 하나는 소스를 졸이면서, 고기를 구우면서 단계마다 직접 간을 보는 것입니다.
손님 평가만 기다리면 피드백이 너무 늦고, “짜다”는 말을 들어도 소스가 문제인지 밑간이 문제인지 알 수 없습니다. 반면 단계마다 간을 보면 잘못된 지점을 즉시 알 수 있고, 레시피를 바꿀 때도 “이 소스 맛은 그대로인가”를 바로 확인할 수 있어 두렵지 않습니다. 손님 시식이 인수 테스트라면, 단위 테스트 는 개발자가 자기 손으로 하는 간 보기입니다.
왜 중요한가?
관리자는 애플리케이션이 외부에서 제대로 동작하는지에만 관심이 있어서 사용자 관점의 인수 테스트(acceptance test)만 생각하기 쉽습니다. 인수 테스트는 테스터나 테스트 엔지니어가 맡을 수 있어 개발자가 아예 필요 없기도 합니다.
하지만 인수 테스트는 시스템의 구체적인 요소가 올바르게 동작함을 보장하지 못하고, 개발 중에 필요한 빠른 피드백도 주지 못합니다. 이 빈자리를 채우는 것이 개발자가 직접 작성하는 단위 테스트입니다.
가변성 제한, 계약 명시, 표준 예외 같은 모범 사례는 프로그램이 올바르게 작동하도록 돕지만, 실제로 올바르게 작동하는지 확인하는 가장 좋은 방법은 결국 테스트입니다. 안정적인 비즈니스 애플리케이션이라면 최소한의 단위 테스트는 갖추어야 합니다.
핵심 내용
단위 테스트가 확인하는 것
n번째 피보나치 수를 계산하는 fib 함수가 처음 5개 위치에서 올바른 결과를 내는지 확인하는 단위 테스트입니다.
@Test
fun `fib works correctly for the first 5 positions`() {
assertEquals(1, fib(0))
assertEquals(1, fib(1))
assertEquals(2, fib(2))
assertEquals(3, fib(3))
assertEquals(5, fib(4))
}단위 테스트는 일반적으로 다음 세 종류를 확인합니다.
| 확인 대상 | 내용 |
|---|---|
| 일반적인 유스 케이스(happy path) | 요소가 쓰일 것으로 예상되는 일반적인 방법. fib 라면 작은 수 몇 개에 대한 결과 |
| 일반적인 에러 케이스 · 잠재적 문제 | 제대로 동작하지 않을 것으로 예상되거나 문제가 있다고 밝혀진 사례 |
| 엣지 케이스 · 잘못된 인수 | Int.MAX_VALUE 같은 매우 큰 수, 널 가능 타입의 null, 피보나치에 정의되지 않은 음수 위치 |
단위 테스트의 장점
개발 중에 구현한 요소가 어떻게 동작하는지 빠른 피드백을 주고, 테스트가 누적되므로 회귀 테스트(check for regression)도 쉬우며 수동으로 확인하기 어려운 케이스까지 검증할 수 있습니다. 그중 가장 큰 장점은 다음 세 가지입니다.
| 장점 | 왜 그런가 |
|---|---|
| 테스트된 요소를 더 신뢰할 수 있음 | 심리적인 면도 큽니다. 검증된 요소는 자신감 있게 사용할 수 있습니다 |
| 리팩터링이 두렵지 않음 | 테스트가 잘 된 프로그램은 점점 좋아지고, 테스트 없는 프로그램은 실수로 오류를 내고도 모를까 봐 레거시를 건드리지 못합니다 |
| 수동 테스트보다 훨씬 빠름 | 피드백 루프가 짧아져 개발이 빠르고 즐거워지며, 버그를 일찍 발견할수록 수정 비용이 줄어듭니다 |
테스트 주도 개발(TDD)
단위 테스트를 먼저 작성하고 그 테스트를 통과시키며 구현하는 방식입니다. 공식적으로는 세 단계를 반복합니다.
- 레드(Red): 실패하는 단위 테스트를 작성합니다
- 그린(Green): 그 테스트를 통과할 정도로만 프로덕션 코드를 작성합니다
- 리팩터(Refactor): 코드를 리팩터링해 정리합니다
단위 테스트의 단점과 반론
| 단점 | 그러나 |
|---|---|
| 작성에 시간이 걸림 | 장기적으로는 디버깅과 버그 추적 시간을 줄여 주고, 실행도 수동 테스트나 다른 자동화 테스트보다 훨씬 빠릅니다 |
| 코드를 테스트 가능하게 고쳐야 함 | 쉽지 않은 작업이지만, 그 과정이 훌륭하고 안정적인 아키텍처를 강제합니다 |
| 좋은 단위 테스트를 쓰기 어려움 | 개발과는 다른 종류의 기술이라 따로 배워야 합니다. 잘못 작성된 테스트는 득보다 실이 많으므로, 소프트웨어 테스팅이나 TDD 과정을 먼저 듣는 것도 유용합니다 |
효과적인 단위 테스트를 수행하고 단위 테스트가 가능한 코드를 작성하는 기술을 익히는 것이 가장 어렵지만, 숙련된 코틀린 개발자라면 반드시 습득해야 하는 기술입니다.
무엇을 테스트할 것인가
모든 코드를 똑같이 테스트할 필요는 없습니다. 다음에 중점을 둡니다.
- 복잡한 기능
- 시간이 지나면서 변경되거나 리팩터링될 가능성이 있는 부분
- 비즈니스 로직
- 공개 API의 일부
- 문제가 자주 발생할 가능성이 있는 부분
- 우리가 수정한 프로덕션 버그
Quote
테스트 작성이 힘들다고 그만두면 안 됩니다. 테스트는 애플리케이션 신뢰성과 장기적인 유지보수성에 대한 투자입니다.
비교 / 트레이드오프
| 기준 | 인수 테스트 | 단위 테스트 |
|---|---|---|
| 관점 | 사용자 · 외부에서 제대로 동작하는가 | 개발자 · 구체적인 요소가 올바른가 |
| 작성 주체 | 테스터, 테스트 엔지니어 (개발자 불필요) | 개발자 |
| 보장하는 것 | 애플리케이션 전체의 외부 동작 | 개별 요소의 정확성 |
| 개발 중 피드백 | 느림 | 빠름 |
내 생각
- 테스트 대상 목록은 “변경 빈도 × 실패 비용”으로 읽힙니다. 여섯 항목 모두 자주 바뀌거나 틀리면 비싼 곳이라, 스프링 애플리케이션에서는 도메인 규칙과 서비스 계층이 1순위이고 DTO 매핑이나 단순 위임 컨트롤러는 후순위입니다.
- “테스트 가능하게 고친다”의 실체는 의존성을 밖으로 밀어내는 일입니다. 현재 시각, 난수, 외부 API 호출을 생성자로 주입받게 바꾸면 테스트가 가능해지는데, 그 결과물이 바로 결합도 낮은 아키텍처라서 단점이 장점으로 뒤집힙니다.
- 프로덕션 버그는 재현 테스트부터 씁니다. 버그를 재현하는 테스트를 먼저 빨갛게 만들고 고치면 같은 버그가 다시 나지 않고, TDD를 전면 도입하지 않아도 레드-그린-리팩터를 가장 실용적으로 쓰는 방법입니다.
- “잘못 작성된 테스트”의 전형은 구현 세부에 결합된 테스트입니다. mock 호출 순서까지 검증하는 테스트는 리팩터링마다 깨져서, 이 아이템이 말하는 “리팩터링이 두렵지 않다”는 장점을 정확히 반대로 뒤집습니다.
- 피드백 루프 차이는 스프링에서 특히 큽니다. 순수 단위 테스트는 밀리초 단위지만
@SpringBootTest는 컨텍스트 로딩만 수십 초라, 비즈니스 로직을 프레임워크 없이 테스트할 수 있게 분리해 두어야 “수동보다 훨씬 빠르다”가 실제로 성립합니다.
관련 개념
- Ch11 테스트 개요 — 테스트를 “자신 있게 변경하는 토대”로 보는 같은 관점에서, 크기·범위 기준으로 작고 좁은 단위 테스트 중심의 스위트를 설계하는 이유를 다룹니다
- Ch12 단위 테스트 — 이 아이템이 “어렵다”고만 말한 좋은 단위 테스트의 기준을 구체화합니다. 공개 API로 상태를 검증해 리팩터링에 깨지지 않게 만드는 방법입니다
- 의존성 주입 — 코드를 테스트 가능하게 만드는 대표 수단으로, 협력 객체를 밖에서 주입받아야 단위 테스트에서 교체할 수 있습니다