어디까지를 유닛 테스트로 검증하고 어디부터를 결합 테스트로 검증해야 하는가 - 경계를 긋는 방법과 실무 판단표
· 小村 豪 · 테스트, 유닛 테스트, 결합 테스트, 테스트 설계, Windows 개발, C# / .NET
테스트 설계 이야기에서 매번 수수하게 어려운 것이 어디까지를 유닛 테스트에 밀어 넣고 어디부터를 결합 테스트로 올릴 것인가입니다.
여기서 위험한 것은,
- 빠르게 돌리고 싶으니까 뭐든지 유닛 테스트로 한다
- 실물에 가까우니까 뭐든지 결합 테스트로 한다
라는 양극단입니다.
전자는 mock투성이가 되어 실전에서 망가지는 포인트를 놓치기 쉽고, 후자는 느리고 망가지기 쉬운 테스트군이 되기 쉽습니다.
실무에서 봐야 할 축은 조금 더 분명합니다.
- 확인하고 싶은 것은 자기들의 로직인가, 밖과의 연결인가
- in-memory의 fake로 바꿔도 의미가 떨어지지 않는가
- DB / 파일 / HTTP / DI / 설정 / 프레임워크 / OS의 동작이 본론인가
- 대량의 입력 패턴을 고속으로 돌리고 싶은가
이 4가지가 보이면 유닛 테스트와 결합 테스트의 경계는 꽤 긋기 쉬워집니다.
이 글에서는 2026년 3월 시점에 참조할 수 있는 Microsoft Learn과 Martin Fowler의 공개 정보를 전제로, 유닛 테스트와 결합 테스트의 경계를 실무 쪽으로 정리합니다.123
1. 먼저 결론
꽤 거칠게, 하지만 실무에서 쓰기 쉽게 말하면 이렇습니다.
- 순수 로직은 유닛 테스트
- 접속·배선·변환·환경 차이는 결합 테스트
- 어느 쪽이든 검증할 수 있다면 우선 유닛 테스트
- 결합 테스트는 넓고 무겁게 하기보다 경계를 좁게 한정
한마디로 하면 유닛 테스트는 「판단의 테스트」, 결합 테스트는 「접속의 테스트」입니다.
금액 계산, 상태 전이, 입력 검증, 승인 조건, 예외의 분류처럼 외부 자원 없이 의미가 완결되는 것은 유닛 테스트로 치우치는 편이 빠르고 망가지기 어렵고 입력 패턴도 두껍게 돌릴 수 있습니다.
한편으로 SQL 실행, JSON / CSV의 직렬화, 라우팅, 모델 바인딩, DI 등록, 파일 락, 권한, COM 등록, 32bit / 64bit, STA / MTA 같은 「연결된 순간에 배신하는 것」은 결합 테스트 쪽에 두는 편이 안전합니다.
Microsoft Learn의 Integration tests in ASP.NET Core에서도 통합 테스트는 중요한 인프라 시나리오로 좁히고, 유닛 테스트로 끝낼 수 있다면 그쪽을 고르도록 정리되어 있습니다.
2. 이 글에서 말하는 유닛 테스트와 결합 테스트
여기서는 다음과 같이 정리합니다.
| 레벨 | 무엇을 확인하는가 | 전형적인 구성 |
|---|---|---|
| 유닛 테스트 | 분리된 1개 책임의 올바름 | fake / mock / stub을 쓰고 외부 자원을 끊는다 |
| 결합 테스트 | 복수 컴포넌트의 접속과 인프라나 프레임워크를 포함한 동작 | 실 DB, 실 파일, 실 serializer, 실 host, 실 pipeline 등 |
| E2E / 기능 테스트 | 앱 전체의 사용자 플로 | 배포된 앱, 복수 서비스, 실 브라우저나 실 프로세스 |
.NET의 유닛 테스트 정리에서는 좋은 유닛 테스트는 fast / isolated / repeatable이며, 파일 시스템이나 DB 같은 외부 요인에 의존하지 않는 것으로 설명되어 있습니다. 자세한 것은 Unit testing best practices for .NET이 이해하기 쉽습니다.
또한 결합 테스트는 「별도 프로세스나 별도 서버를 반드시 쓰는 무거운 테스트」만 가리키는 것은 아닙니다.
같은 프로세스 내에서도 복수의 실 컴포넌트를 연결하고 프레임워크나 인프라의 실제 동작을 확인한다면 그것은 결합 테스트 쪽입니다.
예를 들어 ASP.NET Core의 controller action을 유닛 테스트할 때 대상은 action 본체의 판단으로 좁히고, routing, model binding, filters 같은 프레임워크 쪽의 상호 작용은 결합 테스트에서 다룬다는 구분이 공식에서도 제시되어 있습니다. 자세한 것은 Unit test controller logic in ASP.NET Core를 보면 정리하기 쉽습니다.
3. 한 장으로 보는 판단표
우선 가장 실무에서 쓰기 쉬운 표를 둡니다.
| 확인하고 싶은 것 | 주력으로 할 테스트 | 보충 |
|---|---|---|
| 금액 계산, 할인, 상태 전이, 입력 검증 | 유닛 테스트 | 입력 패턴을 두껍게 돌리고 싶다 |
| 예외의 분류, 에러 메시지 선택, 리트라이할지의 판단 | 유닛 테스트 | 실 I/O 없이 의미가 완결된다 |
| Repository의 SQL / ORM 변환, transaction | 결합 테스트 | 실 DB나 실 provider의 동작이 본론 |
| JSON / XML / CSV의 serialize / deserialize | 결합 테스트 | wire format의 어긋남은 fake로는 찾기 어렵다 |
| 라우팅, 모델 바인딩, 필터, middleware | 결합 테스트 | 프레임워크와의 접속 확인 |
| WPF / WinForms의 ViewModel이나 Presenter의 상태 전이 | 유닛 테스트 | UI를 올리지 않아도 의미가 있다 |
| 실제의 Binding, Dispatcher, control lifecycle, message loop | 결합 테스트 or UI 테스트 | 프레임워크와 스레드의 동작이 주제 |
| 파일 패스, 권한, 락, 공유 폴더, 개행 코드, 문자 코드 | 결합 테스트 | OS와 파일 시스템의 실 동작이 필요 |
| COM 등록, 32bit / 64bit, STA / MTA, DLL 로드 | 결합 테스트 | 환경 차이와 프로세스 경계가 주제 |
| 앱 전체의 기동, 주요 유스케이스의 통과 확인 | E2E / 스모크 | 개수는 적어도 된다 |
보는 방식의 요령은 어느 테스트가 「운영에서 망가지는 이유」에 가장 가까운가입니다.
코드의 둘 장소가 아니라 줄이고 싶은 불확실성으로 정하는 편이 흔들리지 않습니다.
4. 유닛 테스트에 가져야 할 것
유닛 테스트에 맞는 것은 외계를 제거해도 의미가 남는 책임입니다.
예를 들어 다음과 같은 것입니다.
- 업무 규칙
- 분기
- 상태 전이
- 입력 검증
- 에러 분류
- 리트라이 방침의 결정
- ViewModel / Presenter의 상태 변화
- 변환 로직 그 자체
특히 조합이 많은 것일수록 유닛 테스트로 치우치는 가치가 높습니다.
예를 들어,
- 쿠폰 있음 / 없음
- 재고 있음 / 없음
- 초회 주문 / 재주문
- 관리자 / 일반 사용자
- 정상값 / 경계값 / 부정값
처럼 분기 조건이 늘수록 결합 테스트로 전부 돌리는 것은 무거워집니다.
여기는 유닛 테스트로 세세하게 쪼개는 편이 합리적입니다.
또한 유닛 테스트에서는 외부 요인을 제어 가능하게 해 두는 것이 중요합니다.
- 현재 시각은 주입한다
- GUID나 난수는 바꿀 수 있게 한다
- sleep으로 기다리지 않는다
- 실 DB나 실 파일을 건드리지 않는다
- 실 네트워크로 나가지 않는다
이쯤이 지켜지면 테스트는 꽤 안정됩니다.
4.1. 유닛 테스트에서 mock이 너무 늘어날 때
유닛 테스트를 쓰려고 했더니,
- mock이 7개 필요
- setup이 길다
- arrange가 본체보다 길다
- 무엇을 확인하고 싶은지 안 보인다
가 된다면 대체로 다음 중 하나입니다.
- 그 클래스가 책임 과다
- 정말은 결합 테스트에서 확인해야 할 배선을 유닛 테스트에 밀어 넣고 있다
mock은 외계를 끊기 위한 도구이지, 실물과의 접속이 올바르다는 것을 증명하는 도구가 아닙니다.
여기를 잘못 잡으면 「전부 green인데 실전에서 떨어진다」가 일어나기 쉬워집니다.
5. 결합 테스트로 올릴 4가지 경계
결합 테스트로 올려야 할 장소는 대체로 포맷, 배선, 환경, 시간의 4가지로 정리할 수 있습니다.
5.1. 포맷의 경계
여기서 말하는 포맷은 다음 같은 것입니다.
- JSON / XML / CSV
- DB의 schema와 mapping
- nullable / precision / timezone
- enum이나 날짜의 시리얼라이즈
- 문자 코드나 BOM
- 개행 코드
Martin Fowler도 serialize / deserialize가 들어가는 경계는 결합 테스트 후보로 들고 있습니다. 자세한 것은 The Practical Test Pyramid가 참고가 됩니다.
예를 들어,
- DTO를 JSON으로 했더니 필드 이름이 달랐다
- CSV의 인용 부호나 개행이 망가졌다
decimal이 반올림되었다- DB에서
DateTimeOffset의 취급이 어긋났다 null과 빈 문자열의 취급이 상정과 달랐다
같은 불량은 유닛 테스트만으로는 빠지기 쉽습니다.
5.2. 배선의 경계
배선의 경계는 예를 들어 다음입니다.
- DI 등록
- 설정의 bind
- 라우팅
- 모델 바인딩
- 필터
- middleware
- host의 기동
- 이벤트 배선
- WPF의 Binding이나 command 접속
여기는 「자기 함수가 올바른가」가 아니라 복수의 실 부품이 올바르게 연결되어 있는가가 본론입니다.
ASP.NET Core에서는 controller action의 유닛 테스트는 action의 판단으로 좁히고, routing이나 model binding, filters는 결합 테스트 쪽에서 보는 정리가 공식에 있습니다.
Web이 아니어도 사고방식은 같고, 데스크톱 앱에서도 ViewModel의 상태 전이는 유닛 테스트, 실제의 XAML Binding이나 Dispatcher를 포함하는 동작은 결합 테스트 쪽입니다.
5.3. 환경의 경계
Windows 개발에서는 여기가 꽤 중요합니다.
- 파일 권한
- 공유 폴더
- 파일 락
- 임시 파일에서의 rename
- 관리자 권한
- 서비스 기동 권한
- COM 등록
- 32bit / 64bit
- STA / MTA
- DLL의 로드 위치
이쯤은 OS나 실행 환경의 조건 그 자체가 주역입니다.
in-memory fake로는 의미가 꽤 떨어지므로 결합 테스트로 잡는 편이 안전합니다.
특히 기존 Windows 소프트웨어나 COM / ActiveX를 포함한 구성에서는 로직보다 먼저 등록, bitness, 스레드 모델, 권한에서 구르는 일이 평범하게 있습니다.
이런 실패는 유닛 테스트가 아니라 환경을 포함한 결합 테스트가 줍는 영역입니다.
5.4. 시간의 경계
또 하나 놓치기 쉬운 것이 시간과 병행성입니다.
- timeout
- cancellation
- retry의 실 동작
- timer 구동
- 백그라운드 처리의 정지
- race condition
- shutdown 시의 종료 순서
여기서 중요한 것은 판단과 실 동작을 나누는 것입니다.
예를 들어,
- 몇 번까지 retry할 것인가
- 어느 예외를 retry 대상으로 할 것인가
는 유닛 테스트로 충분합니다.
한편으로,
- 실제로 timeout이 듣는가
- cancellation이 전파되는가
- timer와 비동기 처리가 충돌했을 때 망가지지 않는가
- 종료 시에 핸들이나 태스크가 깔끔하게 닫히는가
는 결합 테스트 쪽입니다.
6. 흔한 판단 실수
6.1. Repository를 mock해서 만족해 버린다
Repository 주변을 전부 mock으로 통과시켜도,
- SQL이 올바른가
- transaction이 듣는가
- schema와 일치하고 있는가
- mapping이 어긋나지 않는가
- 문자 코드나 precision이 망가지지 않는가
는 모릅니다.
Repository는 로직의 테스트 대상이라기보다 경계의 접속점인 경우가 많습니다.
그 경우는 유닛 테스트보다 결합 테스트의 비중을 올리는 편이 실태에 맞습니다.
6.2. Controller / Endpoint의 유닛 테스트에서 프레임워크까지 보려고 한다
controller action의 유닛 테스트에서 보고 싶은 것은,
- 조건 분기
- 반환값의 선택
- 의존 서비스의 호출 구분
정도입니다.
한편으로,
- route가 맞는가
- model binding이 통과하는가
- filter가 듣는가
- middleware를 통과한 결과 어떻게 보이는가
는 결합 테스트 쪽입니다.
여기를 섞으면 무엇이 망가졌는지 알기 어려워집니다.
6.3. 결합 테스트에서 입력 패턴을 총당한다
결합 테스트는 실물에 가까운 만큼 아무래도 느려집니다.
그래서 분기의 총당은 유닛 테스트, 경계의 대표 케이스는 결합 테스트로 나누는 편이 이득입니다.
Microsoft Learn의 통합 테스트 해설에서도 DB나 파일 시스템에 대해서는 전 패턴을 결합 테스트로 돌리는 것이 아니라 read / write / update / delete 같은 대표적인 시나리오로 좁히는 방향이 권장됩니다.
6.4. CI에서 외부 서비스의 운영계를 그대로 두드린다
이것은 피하는 편이 안전합니다.
결합 테스트는 「실물다움」이 중요하지만 그렇다고 매번 운영 SaaS나 운영 API를 두드릴 필요는 없습니다.
Fowler도 외부 서비스는 로컬에 세우고, fake를 두거나 전용의 test instance를 쓰는 방향을 권장합니다.
실무에서는,
- 로컬 DB
- 임시 디렉토리
- test host
- 전용의 test environment
- 계약을 고정한 fake service
의 조합이 다루기 쉽습니다.
7. 실무에서의 추천 구성
비율에 절대의 정답은 없습니다.
다만 꽤 범용적으로 쓸 수 있는 것은 다음 3층입니다.
| 층 | 주력 | 무엇을 둘까 |
|---|---|---|
| 코어 층 | 유닛 테스트를 두껍게 | 업무 규칙, 상태 전이, 입력 검증, 에러 분류 |
| 경계 층 | 좁은 결합 테스트를 둔다 | DB, 파일, HTTP, serializer, DI, 설정, COM, 권한 |
| 전체 층 | 소수의 스모크 / E2E | 기동 확인, 주요 플로, 중대 장애의 재발 방지 |
감각적으로는 수로 두꺼워지는 것은 유닛 테스트, 경계의 짙음으로 두꺼워지는 것은 결합 테스트입니다.
추천하는 진행 방식은 이렇습니다.
- 우선 앱의 경계를 열거한다
- 로직을 외계에서 끊을 수 있는 형태로 치우친다
- 경계마다 「최소 1개의 happy path」와 「대표적인 failure path」를 둔다
- 전체의 통과는 개수를 좁힌다
- 버그가 나오면 그 버그를 최소 비용으로 재현할 수 있는 층에 테스트를 추가한다
마지막 5가 중요합니다.
- 규칙의 오류라면 유닛 테스트를 추가한다
- SQL / binding / 설정 / 권한 / 등록의 오류라면 결합 테스트를 추가한다
- 기동이나 배포를 포함하는 장애라면 스모크나 E2E를 추가한다
이 늘리는 방식을 하면 테스트의 책임이 흔들리기 어려워집니다.
8. 헷갈릴 때 마지막에 보는 5가지 질문
마지막으로 헷갈릴 때의 체크용으로 5가지 질문으로 정리합니다.
- in-memory의 fake로 바꿔도 확인하고 싶은 의미가 남는가
- 남는다면 유닛 테스트 쪽입니다.
- 망가졌을 때 의심하는 것은 로직이 아니라 접속이나 설정이 아닌가
- 그렇다면 결합 테스트 쪽입니다.
- DB / 파일 / serializer / DI / route / model binding / OS / 권한 / bitness / thread가 주제가 아닌가
- 그렇다면 결합 테스트 쪽입니다.
- 대량의 입력 패턴을 고속으로 돌리고 싶은가
- 그렇다면 유닛 테스트 쪽입니다.
- 그 테스트가 떨어졌을 때 무엇을 고치면 되는지 바로 알 수 있는가
- 알 수 없다면 테스트의 층이 섞여 있습니다.
이 5가지 질문으로 정리하면 「어쩐지 실물에 가까우니까 결합 테스트」, 「어쩐지 빠르니까 유닛 테스트」라는 거친 결정 방식을 피하기 쉬워집니다.
9. 정리
유닛 테스트와 결합 테스트의 경계는 코드의 둘 장소가 아니라 어떤 불확실성을 줄이고 싶은가로 정하는 것이 가장 실무적입니다.
요점을 정리하면 다음과 같이 됩니다.
- 유닛 테스트는 판단의 테스트
- 결합 테스트는 접속의 테스트
- 분기의 총당은 유닛 테스트
- 포맷, 배선, 환경, 시간은 결합 테스트
- 전체의 통과 확인은 소수의 스모크 / E2E로 잡는다
가장 피하고 싶은 것은,
- mock으로 실물과의 접속까지 증명한 기분이 된다
- 결합 테스트에서 전 분기를 돌리려 한다
- 유닛 테스트와 결합 테스트의 책임을 섞는다
3가지입니다.
헷갈린다면 그 불량은 「판단」이 망가지는 것인가, 「접속」이 망가지는 것인가를 먼저 보세요.
이 1가지 질문으로 꽤 많은 케이스는 정리할 수 있습니다.
10. 관련 기사
- Windows 앱 개발에서의 최소한의 보안을 지키기 위한 체크리스트
- Windows 앱을 정말로 싱글 바이너리로 할 수 있는 범위는 어디까지인가
- Windows의 관리자 특권이 필요하게 되는 것은 언제인가
- Reg-Free COM이란 무엇인가
11. 참고 자료
-
Microsoft Learn, Integration tests in ASP.NET Core ↩
-
Microsoft Learn, Unit testing best practices for .NET ↩
-
Martin Fowler, The Practical Test Pyramid ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 샌드박스로 Windows 앱 개발의 검증을 빠르게 하는 방법 - 관리자 권한 문제, 클린 환경, 권한 부족・리소스 부족의 재현을 실무용으로 정리
Windows Sandbox로 Windows 앱의 클린 환경 검증을 빠르게 하는 실무 노하우를 정리합니다. .wsb 파일을 용도별로 나누고, 입력은 읽기 전용・출력만 쓰기 가능으로 분리하며, 표준 사용자나 메모리 부족, GPU 없는 상태의 재현까...
자체 제작 logger를 피할 수 없을 때, 정말 필요한 최소 요건은 무엇인가: 실무 요건과 통합 테스트 관점
자체 제작 logger를 만들 때 처음부터 정해 두고 싶은 형식·필수 항목·flush·순환·실패 처리 등 최소 요건과, 실제 파일과 스레드를 쓰는 통합 테스트의 v1 체크리스트를 정리해, 장애 시에 신뢰할 수 있는 로그 기반을 굳히는 길을 보여 ...
상정하지 않은 예외가 발생했을 때의 체크리스트 - 앱을 종료시킬지 계속할지, 먼저 보는 판단표
상정 외 예외 시 앱을 종료할지 계속할지를 실패 단위 격리·공유 상태 회복·외부 부작용 설명·네이티브 경계 건전성의 네 축으로 판단하는 흐름을 표와 플로차트로 정리한 글입니다. 독자는 catch 가능 여부가 아니라 불변 조건 회복 가능성으로 가르...
Windows 앱에서 「관리자 권한이 필요한 처리만」을 분리하는 구체적인 방법
Windows 앱에서 UI는 asInvoker로 두고 관리자 처리만 helper EXE로 분리하는 broker 설계를 runas 기동, 명명 파이프 ACL, 클라이언트 PID 검증, 고정 operation allowlist까지 .NET 8 코드로...
Windows 앱에서 설정 파일에 기밀 정보를 평문으로 저장하지 않기 위한 베스트 프랙티스
Windows 데스크톱 앱에서 자격 증명이나 토큰을 평문 설정 파일에 두지 않기 위한 현실적인 설계를 정리합니다. DPAPI/ProtectedData의 사고 방식, CurrentUser와 LocalMachine의 차이, 로그 누출 회피까지 실무 ...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크