31 lines
5.8 KiB
Markdown
31 lines
5.8 KiB
Markdown
# [[단위 테스트 (Unit Tests)]]
|
|
|
|
## 📌 Brief 소감
|
|
단위 테스트(Unit Tests)는 개별 함수, 메서드 또는 클래스 등 소프트웨어의 가장 작은 구성 단위(Unit)를 외부 의존성으로부터 격리하여 동작을 검증하는 빠르고 자동화된 테스트 기법이다 [1-3]. 이는 기능 변경이나 리팩토링 시 기존의 동작이 손상되지 않았음을 보장하는 필수적인 안전망 역할을 수행한다 [4-6]. 이상적인 단위 테스트는 스스로 결과를 검증(self-checking)할 수 있어야 하며, 소프트웨어 테스트 자동화 피라미드의 가장 하단에 위치하여 전체 테스트 스위트의 대다수를 차지해야 한다 [1, 7, 8].
|
|
|
|
## 📖 Core Content
|
|
|
|
* **리팩토링의 필수 전제 조건**
|
|
리팩토링을 시작하기 전에는 반드시 견고한 단위 테스트 스위트가 구축되어 있어야 한다 [5, 6]. 테스트 코드가 없는 소프트웨어는 단순히 '레거시 코드'로 취급되며, 단위 테스트라는 안전망 없이는 대규모 변경 시 회귀 버그(regression bug)를 피하기 어렵다 [9, 10]. 또한, 새로운 버그 리포트를 받았을 때 가장 먼저 해야 할 일은 해당 버그를 드러내는 단위 테스트를 작성하여, 추후 동일한 문제가 발생하지 않도록 방지하는 것이다 [11-13].
|
|
* **테스트의 구조와 모범 사례**
|
|
단위 테스트는 데이터 설정(Arrange), 메서드 호출(Act), 예상 결과 검증(Assert) 혹은 'Given, When, Then' 구조를 따르는 것이 읽기 쉽고 일관성 있는 테스트를 작성하는 데 유리하다 [14, 15]. 테스트 대상을 선정할 때는 단순한 Getter/Setter 등 사소한 코드는 생략하고, 시스템이 실패할 수 있는 경계 조건(boundary conditions)이나 에러 조건 등 잠재적 위험이 있는 곳에 집중하여 테스트 효율성을 높여야 한다 [16-19].
|
|
* **테스트 대역(Test Doubles)의 활용**
|
|
데이터베이스, 파일 시스템, 외부 네트워크 호출 등은 단위 테스트의 속도를 늦추고 복잡성을 더한다. 이를 방지하기 위해 목(Mock)이나 스텁(Stub)과 같은 가짜 객체를 사용하여 외부 협력자를 대체함으로써, 컴포넌트를 격리하고 예측 가능한 환경에서 빠르게 테스트를 실행할 수 있다 [20-22]. 모든 협력자를 격리하는 '고립된(Solitary)' 방식과, 속도 저하가 없는 일부 협력자를 포함하는 '사교적인(Sociable)' 방식으로 나뉠 수 있다 [20].
|
|
* **테스트 대상의 캡슐화 보존**
|
|
단위 테스트는 구현 세부 구조가 아닌 관측 가능한 동작(observable behavior)과 공개 인터페이스(public interface)를 검증해야 한다 [23, 24]. 클래스의 내부 구현(private 메서드)을 직접 테스트해야 한다는 충동이 든다면, 이는 해당 클래스가 '단일 책임 원칙(SRP)'을 위반하여 너무 많은 일을 하고 있다는 설계 문제의 신호일 가능성이 높으므로 클래스 분리를 고려해야 한다 [25-27].
|
|
* **레거시 코드와 AI 활용**
|
|
테스트가 없는 레거시 코드를 변경할 때는 코드를 직접 변경하지 않고 동작을 우회할 수 있는 '접점(Seams)'을 파악하여 테스트를 주입하는 것이 중요하다 [28]. 최근에는 대규모 언어 모델(LLM) 기반의 AI 코딩 도구를 활용해 기존 시스템의 동작을 포착하는 단위 테스트를 자동으로 대량 생성하고, 이를 가드레일로 삼아 리팩토링을 수행하는 접근법이 높은 브랜치 및 구문 커버리지를 달성하며 효과를 입증하고 있다 [29-31].
|
|
|
|
## ⚖️ Trade-offs & Caveats
|
|
|
|
* **테스트의 경직성(Brittleness) 문제**
|
|
단위 테스트를 프로덕션 코드의 내부 구조와 너무 가깝게 결합하여 작성하면, 단순히 내부 설계를 개선하는 리팩토링 작업 중에도 테스트가 깨지는 문제가 발생한다 [32]. 이는 개발자가 코드를 수정할 때마다 깨진 테스트를 고쳐야 하는 번거로움을 야기하며, 결국 단위 테스트가 안전망이라는 본연의 가치를 잃고 유지보수 부담으로 전락할 수 있다 [24].
|
|
* **단위 테스트의 범위 한계**
|
|
단위 테스트는 개별 단위의 논리를 검증하는 데는 탁월하지만, 외부 시스템이나 독립된 서비스, 데이터베이스와의 통합 과정에서 발생하는 결함을 찾아낼 수는 없다 [33, 34]. 따라서 단위 테스트에만 의존해서는 안 되며 상위 수준의 통합 테스트(Integration Tests) 및 엔드투엔드(E2E) 테스트와 병행해야 한다 [33, 35].
|
|
* **레거시 코드 딜레마(Legacy Code Dilemma)**
|
|
기존 코드에 안전하게 단위 테스트를 추가하려면 코드의 의존성을 분리해야 하는데, 의존성을 끊기 위해 코드를 변경하려면 다시 단위 테스트가 필요한 모순적인 상황에 처하게 된다 [36]. 이 과정에서 컴파일러를 활용한 매우 작고 안전한 리팩토링을 수행하거나, 'Sprout/Wrap' 기법 등을 이용해 최소한의 변경만으로 코드를 확장해야 하는 기술적 제약이 따른다 [36, 37].
|
|
* **과도한 테스트의 역효과 및 AI의 가치 정렬 편향**
|
|
소프트웨어의 모든 조합이나 단순 기능까지 100% 테스트하려는 압박은 오히려 개발자를 좌절시켜 꼭 필요한 테스트조차 작성하지 않게 만드는 역효과를 초래할 수 있다 [17, 38]. 또한 AI를 사용해 단위 테스트를 생성할 때, AI 모델은 커버리지 수치만 높이고 실제 결함 검증력은 떨어지는 비효율적인 테스트를 다수 생성하는 편향성(value misalignment)을 보일 수 있어, 변이 테스트(mutation testing)를 통한 개발자의 엄격한 필터링이 필요하다 [39, 40].
|
|
|
|
---
|
|
*Last updated: 2026-05-03* |