소프트웨어의 품질은 코드 그 자체만으로 보장되지 않습니다. 오히려 그것을 둘러싼 테스트 전략과 의존성 관리, 그리고 설계 철학이 품질의 기초를 이룹니다.
이번 글에서는 단위 테스트와 통합 테스트를 구분하는 기준, 프로세스 외부 의존성을 어떻게 다뤄야 하는지, 로깅은 어떻게 테스트해야 하는지, 그리고 불필요한 추상화(YAGNI 원칙 위반)까지 폭넓게 다루어 보겠습니다.
1. "식별할 수 있는 동작인가, 구현 세부 사항인가?"를 기준으로 판단하라
테스트 대상이 되는 코드를 구분할 때 가장 중요한 기준은 다음의 질문입니다.
“이 기능은 외부에서 식별할 수 있는가, 아니면 구현의 세부 사항에 불과한가?”
이 기준을 통해 테스트는 다음과 같이 구분됩니다.
| 구분 | 설명 | 테스트 대상 여부 |
| 식별 가능한 동작 (Observable Behavior) |
사용자 또는 외부 시스템에서 확인 가능한 결과 | ✅ 테스트 |
| 구현 세부 사항 (Implementation Detail) |
내부 처리 방식, 외부에서 관찰 불가 | ❌ 테스트하지 않음 |
2. 로깅: 진단용인가, 지원용인가?
로깅은 그 자체로 기능이 될 수도 있고, 단지 디버깅 도구일 수도 있습니다. 이를 명확히 구분해야 합니다.
지원 로깅 (Support Logging)
- 운영팀, 고객지원팀, 외부 사용자 등이 실제로 사용하는 로그
- 예: 인증 실패, 결제 요청, 개인정보 변경 기록 등
- 비즈니스 요구사항에 명시되어야 하며, 반드시 테스트
- DomainLogger와 같은 명시적 도메인 객체로 다룰 것
진단 로깅 (Diagnostic Logging)
- 개발자가 디버깅 목적으로 작성한 로그
- 예: log.debug("여기까지 실행됨")
- 순수한 구현 디테일에 해당 → 테스트하지 말 것
- 진단 로깅이 많아지면 노이즈가 증가하고 유지보수가 어려워짐
💡 정리된 판단 기준
“로그를 개발자 외의 누군가가 보거나 시스템 외부로 전송하는가?” → 그렇다면 테스트하라.
그렇지 않다면 테스트하지 마라.
3. 통합 테스트 vs 단위 테스트: 무엇을 어떻게 테스트해야 할까?
| 항목 | 단위 테스트 | 통합 테스트 |
| 대상 | 도메인 모델, 알고리즘 | 컨트롤러, 외부 API, 데이터베이스 등 |
| 속도 | 빠름 | 느림 |
| 비용 | 낮음 | 높음 |
| 목적 | 다양한 로직 시나리오와 예외 검증 | 주요 흐름과 시스템 통합 검증 |
| 리팩토링 내성 | 낮음 | 높음 |
| 유지보수성 | 높음 | 낮음 |
4. 테스트 피라미드: 비용과 가치에 따른 테스트 설계 전략
테스트 피라미드(Test Pyramid)는 다음과 같은 절충을 의미합니다.
- 단위 테스트는 많아야 하고, 빠르고 비용이 낮아야 합니다.
- 통합 테스트는 적어야 하며, 느리고 비싸지만 전체 시스템의 일관성을 보장합니다.
📌 통합 테스트에서 검증할 것
- 대표 흐름 하나
- 단위 테스트에서 커버할 수 없는 예외 상황
📌 단위 테스트에서 커버할 것
- 가능한 모든 비즈니스 조건, 실패 상황
5. 프로세스 외부 의존성: 관리와 비관리로 나누어라
외부 의존성을 다룰 때, 단순히 "외부에 있는 것"만으로 판단해서는 안 됩니다. 접근 통제 여부에 따라 아래와 같이 나뉩니다.
| 구분 | 설명 | 예시테스트 | 전략 |
| 관리 의존성 (Managed Dependency) |
애플리케이션 내부에서만 접근 가능 | RDB, 로컬 파일, 내부 Redis | 실제 인스턴스를 사용하고, 최종 상태를 검증 |
| 비관리 의존성 (Unmanaged Dependency) |
다른 애플리케이션에서도 접근 가능 | Kafka, SMTP, 외부 API | Mock 객체로 대체하고, 전송 여부만 검증 |
혼합된 의존성의 예시: 외부에서 접근 가능한 데이터베이스
- 외부에서 보는 부분만 비관리 의존성으로 간주하고 Mock 처리
- 나머지는 관리 의존성으로 다루고, 상호작용 대신 최종 상태를 검증
예: PostgreSQL DB가 있고, 외부 서비스가 이 DB 일부 테이블을 참조함 →
그 테이블은 비관리 의존성, 나머지는 관리 의존성.
6. 구현이 하나뿐인 인터페이스는 추상화가 아니다
"추상화는 다양성을 위해 존재한다. 구현이 하나뿐이면, 인터페이스는 추상화가 아니다."
- 인터페이스는 다양한 구현을 위한 설계 수단입니다.
- 구현이 하나뿐인 경우, 그 인터페이스는 오히려 불필요한 간접 계층이 됩니다.
- 이는 YAGNI (You Ain't Gonna Need It) 원칙 위반입니다.
예외: 비관리 의존성에 대해 목(Mock)을 주입하기 위한 목적일 때만 의미가 있습니다.
→ 이 경우에만 인터페이스를 허용합니다.
7. DomainLogger의 도입
- 지원 로깅은 비즈니스 요구사항의 일부입니다.
- 따라서 로깅 요구사항을 명시적으로 코드에 반영해야 합니다.
- 도메인 모델에서 발생한 이벤트를 DomainLogger로 전달해 기록하고, 이 로거를 테스트할 수 있어야 합니다.
설계 예:
public class DomainLogger {
public void logUserCreated(User user) {
// 실제 로깅 또는 외부 저장소 전송
}
}
이렇게 구현하면, 테스트에서는 DomainLogger를 Mock으로 대체하여 동작을 검증할 수 있습니다.
8. 테스트 전략 요약 가이드
| 상황 | 전략 |
| DB에 데이터가 저장되었는지 검증해야 한다면 | ✅ 실제 인스턴스로 통합 테스트 |
| Kafka로 메시지를 보냈는지만 확인해야 한다면 | ✅ 목(Mock)으로 대체 |
| 디버깅용 로그 | ❌ 테스트하지 않음 |
| 감사용 로그 | ✅ 도메인 이벤트 + DomainLogger 패턴으로 테스트 |
| 인터페이스 하나에 구현체 하나 | ❌ YAGNI 위반. 단순화할 것 |
| 구현 디테일 테스트 | ❌ 리팩토링 내성 떨어뜨림. 피할 것 |
마치며
좋은 테스트 전략은 단순히 테스트의 개수가 아니라, 그 테스트가 어떤 책임을 검증하는가에 달려 있습니다.
외부 의존성과의 상호작용을 무조건 테스트하거나 무조건 무시하는 것이 아니라,
"관측 가능한 동작인가?"라는 질문을 중심에 두고 판단해야 합니다.
또한, 모든 기능을 미리 대비한 인터페이스 설계보다는 실제로 필요한 순간에 추상화하는 것이 더 유지보수에 강한 설계가 됩니다.
테스트는 설계의 거울입니다. 좋은 테스트 전략은 궁극적으로 좋은 설계를 유도합니다.
'기타' 카테고리의 다른 글
| 메모 도구 정리 obsidian vs edrawmind (2) | 2025.08.01 |
|---|---|
| [단위테스트] 원칙과 패턴 (9장) - 목 처리에 대한 모범 사례 (0) | 2025.04.22 |
| [단위 테스트] 원칙과 패턴 (7장) - 단위테스트를 위한 리팩터링 (0) | 2025.04.18 |
| [단위 테스트] 원칙과 패턴 (6장) - 단위 테스트 스타일 (0) | 2025.04.17 |
| [단위 테스트] 원칙과 패턴 (5장) - 목과 테스트 취약성 (0) | 2025.04.16 |
