285. 시험 용이성 (Testability) - 관찰 가능성, 제어 가능성 향상 전술
핵심 인사이트 (3줄 요약)
- 본질: 시험 용이성(Testability)은 소프트웨어 시스템이 내부의 결함(Bug)을 얼마나 쉽고 정확하게 찾아낼 수 있도록 설계되었는지를 나타내는 아키텍처 품질 속성이다.
- 가치: 테스트가 어려운 시스템은 버그를 안고 배포될 수밖에 없으며, 자동화된 단위 테스트(Unit Test)를 불가능하게 만들어 CI/CD(지속적 통합/배포) 파이프라인 구축을 가로막는 최대의 장애물이 된다.
- 융합: 시험 용이성을 극대화하기 위한 아키텍처 전술은 시스템의 내부 상태를 들여다보는 **관찰 가능성(Observability)**과, 원하는 테스트 환경으로 시스템을 강제 조작할 수 있는 **제어 가능성(Controllability)**을 확보하는 기술로 귀결된다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 시험 용이성이란 개발된 시스템의 기능과 성능이 설계 명세(요구사항)대로 잘 작동하는지, 결함은 없는지를 '최소한의 시간과 비용으로' 입증할 수 있는 시스템의 능력이다.
-
필요성: 회원가입 시 핸드폰으로 인증번호 SMS를 보내는 기능이 있다. 이 기능 하나를 테스트하기 위해 개발자는 매번 소스 코드를 빌드하고, 톰캣(WAS)을 띄우고, DB에 테스트 유저를 넣고, 진짜 자기 핸드폰 번호를 입력해 문자가 오는지를 기다려야 한다. 만약 문자가 안 오면 통신사 문제인지, 서버 문제인지, DB 문제인지 도무지 알 길이 없다. 이것은 '시험 용이성'이 0점인 아키텍처다. 코드는 반드시 고립된 상태에서 0.1초 만에 기계적으로 테스트 가능(Testable)해야 한다.
-
💡 비유: 자동차 엔진을 점검할 때, 엔진을 차체에서 뜯어내지 못해 매번 운전사가 차를 타고 고속도로를 달려봐야만 고장 여부를 알 수 있다면 정비가 불가능합니다. 엔진에 진단 케이블을 꽂으면 즉시 모든 상태가 모니터(관찰 가능성)에 뜨고, 정비사가 외부 스위치로 엔진 RPM을 마음대로 조작(제어 가능성)할 수 있어야 훌륭하게 설계된 자동차입니다.
-
등장 배경 및 발전 과정:
- 수동 블랙박스 테스트의 한계: 과거에는 QA 팀이 배포된 화면을 직접 마우스로 수백 번 클릭하며(블랙박스) 테스트를 했다. 인건비가 폭증하고 사각지대가 발생했다.
- TDD와 단위 테스트의 부상: Junit, NUnit 등 프레임워크의 등장으로 코드 레벨에서 개발자가 스스로(화이트박스) 자동화 테스트를 작성하게 되면서, "테스트하기 좋은 코드가 좋은 아키텍처다"라는 패러다임 전환이 일어났다.
- 의존성 주입(DI)과 Mocking의 대중화: 테스트가 어려운 외부 시스템(DB, 외부 API)을 가짜 객체(Mock/Stub)로 대체하여 오직 내 로직만 고립시켜 테스트하는 제어 가능성 전술이 업계 표준으로 정착했다.
-
📢 섹션 요약 비유: 수박이 잘 익었는지 확인하려고 매번 수박을 다 쪼개서 먹어봐야 한다면 수박 장사를 할 수 없습니다. 겉을 통통 두드렸을 때 나는 소리(관찰 가능성)를 들을 수 있어야 팔기 좋은 수박입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
시험 용이성 시나리오 (6요소)
시험 용이성 요구사항은 다음 6가지 요소로 구체화된다.
- 자극원: 단위 테스트 담당 개발자, 시스템 통합 테스터 (QA)
- 자극: 시스템의 특정 모듈에 대한 테스트 스크립트 실행, 더미 데이터 주입
- 환경: 개발 환경, 스테이징(Staging) 환경, CI 서버(Jenkins 등) 빌드 타임
- 대상: 시스템의 개별 클래스(Unit), 여러 모듈이 합쳐진 서브시스템
- 응답: 시스템이 제어된 상태로 실행되고, 그 결과 상태값과 로직 수행 경로를 반환함
- 응답 척도: 코드 커버리지(Code Coverage) 80% 달성, 단위 테스트 실행 시간이 100ms 이내, 외부 DB 의존성 없이 테스트 성공
시험 용이성을 결정하는 핵심 2축 (Observability & Controllability)
소프트웨어가 테스트하기 쉽다는 것은, 테스트 스크립트가 대상 시스템(SUT, System Under Test)의 멱살을 잡고 마음대로 요리할 수 있어야 함을 뜻한다.
┌─────────────────────────────────────────────────────────────┐
│ 시험 용이성 (Testability)의 2대 축 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [테스트 스크립트] [대상 시스템 (SUT)] │
│ │
│ ──────────── 제어 (Control) ───────────▶ │
│ "이 변수는 100으로, DB 연결은 끊긴 상태로 돌려!" │
│ │
│ ◀─────────── 관찰 (Observe) ──────────── │
│ "현재 내부 계산 결과는 500이고, A 메서드를 통과했음" │
└─────────────────────────────────────────────────────────────┘
1. 제어 가능성 (Controllability)
테스트를 수행하기 위해 시스템을 내가 원하는 '특정 상태(State)'로 강제 세팅할 수 있는 능력이다.
- 만약 시스템 코드가
Calendar.getInstance()(현재 시간)를 무조건 사용한다면, "매월 1일 자정에 결제가 초기화된다"는 로직을 테스트하기 위해 개발자는 진짜 다음 달 1일까지 기다리거나 컴퓨터 시계를 돌려야 한다. (제어 불가) - 해결 전술:
DateProvider라는 인터페이스를 만들고 외부에서 시간을 주입받도록 짜면, 테스터가 가짜 시간 객체(Mock)를 꽂아 넣어 언제든 '1일 자정' 상황을 제어할 수 있다.
2. 관찰 가능성 (Observability)
테스트가 끝난 후, 시스템이 '기대하는 결과'를 냈는지 정확히 확인할 수 있는 능력이다.
-
만약 시스템이 에러가 났을 때
System.out.println("에러 발생")만 찍고 화면을 종료해 버린다면, 자동화 테스트 프로그램은 그 글자를 읽을 수가 없어서 테스트 통과 여부를 판단할 수 없다. (관찰 불가) -
해결 전술: 내부 상태값을 반환하는
Getter를 열어주거나, 명시적인 예외(Exception) 객체를 던지거나, 로그를 파일/DB 시스템 인터페이스로 넘기도록 분리하여 테스트 코드(AssertEquals)가 그 상태나 로그값을 명확히 끄집어내어 관찰할 수 있게 해야 한다. -
📢 섹션 요약 비유: 로봇청소기를 테스트할 때, 리모컨으로 청소기를 특정 방에 갖다 놓을 수 있어야 하고(제어 가능성), 투명한 먼지통이 달려있어 쓰레기를 얼마나 빨아들였는지 눈으로 볼 수 있어야(관찰 가능성) 테스트가 가능합니다.
Ⅲ. 융합 비교 및 다각도 분석
1. 단위 테스트(Unit Test)를 파괴하는 절대악: 의존성(Dependency)
시험 용이성을 끌어올리는 전술의 90%는 결국 외부 의존성을 잘라내는 것이다.
| 강결합 구조 (테스트 불가) | 의존성 주입 구조 (테스트 가능) | 아키텍처 전술 적용 |
|---|---|---|
Payment 클래스 안에서 new Database()를 직접 생성함. | Payment 생성자의 파라미터로 Database 인터페이스를 받음. | 의존성 주입 (Dependency Injection, DI) |
| 테스트 시 진짜 DB를 연결해야 함. (속도 수 초 소요, DB 꺼지면 실패) | 가짜 MockDatabase를 생성해 생성자로 밀어 넣음. (속도 0.001초 소요) | 모의 객체 (Mock / Stub) 기반의 제어 |
테스트 코드는 절대 진짜 데이터베이스나 진짜 외부 API, 진짜 파일 시스템을 건드려서는 안 된다. 테스트 환경에 따라 결과가 달라지는 비결정적(Non-deterministic) 테스트(Fliky Test)가 되어버리기 때문이다. 인터페이스로 결합도를 끊어내어 가짜 객체를 꽂아 넣을 수 있는 틈(Seam)을 만드는 것이 핵심이다.
과목 융합 관점
-
소프트웨어 공학 (SE): 객체 지향 원칙의 **DIP(의존성 역전 원칙)**와 **SRP(단일 책임 원칙)**가 잘 지켜진 코드는 구조적으로 변경용이성(Modifiability)이 높을 뿐만 아니라, 모의 객체(Mock) 주입이 쉬워져 시험 용이성(Testability)이 극대화되는 1석 2조의 동반 상승효과를 가진다.
-
클라우드 / DevOps: 코드를 푸시하면 CI(Continuous Integration) 서버가 수백 개의 단위 테스트를 10초 만에 돌리고 성공 시에만 배포를 진행한다. 만약 코드가 시험 용이성 전술이 빵점이라 진짜 DB를 켜야만 돌아간다면, CI 파이프라인 자체가 먹통이 되어 DevOps 도입이 불가능해진다.
-
📢 섹션 요약 비유: 수영 선수를 테스트하려고 매번 진짜 바다(DB)에 데려가서 상어가 나타나는지 봐야 한다면 테스트가 안 됩니다. 실내 수영장(Mock)을 만들어 물결의 세기(제어 가능성)를 마음대로 조절해야 선수의 수영 실력만 완벽히 고립시켜 테스트할 수 있습니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 강결합된 싱글톤(Singleton)과 정적(Static) 메서드의 저주: 개발 편의성을 위해 시스템 곳곳에서
OrderManager.getInstance().calculate()처럼 싱글톤 패턴을 직접 호출하거나 헬퍼 클래스의 정적 메서드를 남발했다. 테스트 코드를 짤 때 이 클래스들의 내부 상태가 이전 테스트에서 오염된 채로 그대로 남아있어(전역 변수), A 테스트는 성공하는데 B 테스트와 같이 돌리면 실패하는 귀신 같은 현상이 발생한다.- 아키텍트의 해결책: 전역 상태(Global State)나 정적(Static) 호출은 시험 용이성을 파괴하는 최악의 안티패턴이다. 싱글톤을 직접 호출하는 코드를 모조리 뜯어내어 **IoC 컨테이너(Spring 등)**가 관리하는 빈(Bean)으로 전환하고, 생성자 주입(Constructor Injection) 방식으로 바꿔야 한다. 그래야 매 테스트마다 깨끗한(Mocking 된) 새 의존성 객체를 주입하여 완전한 멱등성(Idempotency)을 보장할 수 있다.
-
시나리오 — 비동기(Async) 로직의 관찰(Observe) 불가능성 문제: 결제 완료 후 백그라운드 스레드를 띄워 메일을 발송한다(
CompletableFuture.runAsync()). 단위 테스트에서 결제 메서드를 호출하고 바로 "메일 발송됨"을Assert로 확인하려 하지만, 테스트 코드는 메일 스레드가 끝나기도 전에 종료되어버려 무조건 실패하거나 성공을 장담할 수 없다.- 아키텍트의 해결책: 멀티스레드/비동기 로직은 제어와 관찰이 가장 까다롭다. 비동기 작업을 수행하는
ExecutorService를 내부에 하드코딩하지 말고 외부에서 주입받도록 아키텍처를 열어두어야 한다. 테스트 시에는 스레드 풀 대신, 호출한 그 스레드에서 즉시 동기식으로 돌아가게 하는 '가짜 동기화 실행기(SyncTaskExecutor)'를 주입하여 비동기 타이밍 이슈를 원천 제거하고 관찰 가능성을 통제해야 한다.
- 아키텍트의 해결책: 멀티스레드/비동기 로직은 제어와 관찰이 가장 까다롭다. 비동기 작업을 수행하는
도입 체크리스트
- 기술적: 소스 코드의 "코드 커버리지(Code Coverage, 예: JaCoCo)"를 측정하고 있는가? 단순히 실행된 줄(Line) 수를 넘어,
if (a && b)의 a가 true일 때와 false일 때 등 분기 커버리지(Branch Coverage)까지 테스트 스크립트가 샅샅이 훑고 지나가도록 관찰/제어 가능한 구조인가? - 설계적: 테스트 코드를 먼저 짜고(Test-Driven Development, TDD) 비즈니스 로직을 구현하는 방식을 고민해 보았는가? TDD는 개발자로 하여금 코드를 짤 때부터 "어떻게 Mock을 꽂아 넣을까"를 강제하므로, 결과적으로 도출되는 아키텍처가 시험 용이성 100점을 달성하게 되는 최고의 기법이다.
안티패턴
-
내부 메서드(Private Method)에 대한 집착적 테스트: 내부 동작을 하는
private메서드를 억지로 테스트하겠다고 자바 리플렉션(Reflection)을 써서 접근 제어자를 강제로 풀거나 패키지를 열어버리는 행위. 캡슐화를 깨뜨리며, 나중에 리팩토링 시 테스트가 모조리 깨져버린다. 테스트는 무조건 외부로 열린public인터페이스의 입력과 반환값(행위)만 관찰해야 한다. -
📢 섹션 요약 비유: 텔레비전(객체)이 잘 켜지는지 테스트할 때, 리모컨 버튼(Public 메서드)을 누르고 화면이 나오는지(결과)만 보면 됩니다. 굳이 TV 뒷판을 뜯어 전선(Private 메서드)에 전류가 흐르는지 일일이 찍어보면, 나중에 TV 내부 부품을 업그레이드할 때 테스트 도구까지 다 버려야 합니다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 하드코딩 및 강결합 아키텍처 (AS-IS) | 시험 용이성 전술 (DI, Mock) 적용 (TO-BE) | 개선 효과 |
|---|---|---|---|
| 정량 | 수작업 QA 테스트로 릴리즈 전 2주일 소요 | CI 빌드 시 1분 만에 수만 개 자동화 테스트 완료 | 배포 전 테스트 리드타임 99% 단축 |
| 정량 | 운영 배포 후 치명적 런타임 버그 발견율 10% | 개발/빌드 단계에서 95% 이상 버그 사전 차단 | 운영 장애 건수 획기적 감소 및 품질 비용 절약 |
| 정성 | 두려워서 레거시 코드 리팩토링 불가능 | 테스트 코드가 방어망(Safety Net)이 되어 마음껏 수정 | 기술 부채 해결 및 지속적 리팩토링 문화 정착 |
미래 전망
- AI 융합 (자동화된 테스트 생성): GitHub Copilot이나 LLM이 아키텍처 코드를 분석하여, 코드 커버리지 100%를 달성하는 엣지 케이스(Edge Case) 파라미터 조합과 Mock 객체 생성 코드를 자동으로 찍어내는(Test Generation) 시대가 열리고 있다. 인간은 시스템이 "무엇을 관찰해야 하는지(Assertion)" 규칙만 AI에게 승인해 주면 된다.
- 분산 시스템의 관찰 가능성(Observability) 극대화: 마이크로서비스(MSA)의 단위 테스트 한계를 넘어, 시스템이 운영에 배포된 후 수십 개의 서비스를 관통하는 에러를 추적하기 위해 '분산 추적(Distributed Tracing, 예: Jaeger/Zipkin)', 메트릭, 로그를 3위 일체로 구성하는 대규모 인프라 레벨의 관찰 가능성 전술이 현대 클라우드의 표준이 되었다.
참고 표준
- xUnit 프레임워크: 단위 테스트의 표준 명세이자 관찰(Assert) 및 제어 인프라를 제공하는 툴 생태계.
- TDD (Test-Driven Development): Kent Beck이 주창한, 시험 용이성을 강제로 끌어올리기 위한 익스트림 프로그래밍(XP)의 핵심 실천법.
시험 용이성(Testability)은 좋은 설계가 갖는 필연적인 부산물(By-product)이다. 즉, 객체 지향적으로 잘 짜여 응집도가 높고(SRP), 결합도가 낮으며(인터페이스/DI 사용), 정보 은닉이 철저한 시스템은 별도의 노력을 들이지 않아도 테스트하기가 믿을 수 없을 정도로 쉽다. 반대로 테스트 코드를 짜다가 "왜 이렇게 복잡하고 안 짜지지?"라는 고통을 느낀다면, 그것은 시스템 설계(아키텍처)가 어딘가 심각하게 썩어있다는 경고음(Code Smell)이다. 기술사는 이 경고음을 절대 무시하지 않고 아키텍처의 칼을 대는 용기를 가져야 한다.
- 📢 섹션 요약 비유: 시험 용이성은 의사가 환자를 진찰할 때 청진기 하나만 대보면 어디가 아픈지 투명하게 다 들리는 맑은 몸 상태를 만드는 것과 같습니다. 수술(리팩토링)을 할 때에도 어디를 째야 피가 안 나는지(부작용) 명확히 보이므로, 평생 건강하게 진화할 수 있는 소프트웨어가 됩니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 의존성 주입 (DI, Dependency Injection) | 대상 객체 내부의 제어 불가능한 외부 자원(DB, API)을 밖에서 꽂아 넣어, 제어 가능성(Controllability)을 100%로 만드는 핵심 전술. |
| 모의 객체 (Mock / Stub) | DI를 통해 주입되는 가짜 부품. 항상 성공하거나 항상 예외를 던지도록 테스터가 마음대로 조종(제어)할 수 있는 테스트 전용 장난감. |
| 변경용이성 (Modifiability) | 코드가 유연하여 부품(모듈) 교체가 쉬워야 변경용이성이 높은데, 부품 교체가 쉬우면 가짜 부품(Mock) 교체도 쉬우므로 둘은 영혼의 단짝이다. |
| 관찰 가능성 (Observability) | 테스트 결과를 확인하기 위한 '상태나 로그 노출 수준'을 뜻하며, 최근 MSA 분산 모니터링 체계의 핵심 용어로 스케일 업 되었다. |
| TDD (Test-Driven Development) | 테스트 스크립트를 먼저 짜야만 실제 로직을 짤 수 있게 함으로써, 개발자가 무의식적으로 시험 용이성 아키텍처 전술을 적용하도록 세뇌하는 기법. |
👶 어린이를 위한 3줄 비유 설명
- 자동차 장난감을 새로 만들었는데, 건전지를 넣고 전원 스위치를 누르면 무조건 앞으로 100m를 혼자 달려가 버린다고 상상해 봐요. (통제 불가)
- 만약 장난감 배를 까서 바퀴가 진짜 도는지 눈으로 볼 수 있고(관찰 가능성), 바퀴 속도를 리모컨으로 1단, 2단 조절할 수 있다면(제어 가능성) 방구석에서도 쉽게 고장 났는지 확인할 수 있죠.
- 이렇게 프로그램이 잘 만들어졌는지, 언제든지 내가 원하는 상황을 만들어 테스트하기 쉽게 뼈대를 짜는 기술을 **'시험 용이성 전술'**이라고 한답니다!