TDD, AAA Pattern & State Verification
테스트를 먼저 작성하여 설계를 이끌어내는 TDD 순환 구조와, 테스트 코드의 물리적 가독성을 높이는 AAA 패턴 및 상태 검증의 수리적 수순을 다루는 학습 노드입니다.
Article
M
Me
hyunyoun's Blog
software-engineering-devopssoftware-engineeringdev-opsqaquality-assurancetddaaa-patternstate-verification11 min read
1. Overview
TDD와 AAA 패턴(TDD, AAA Pattern & State Verification)은 코드를 다 짠 뒤에 마지못해 테스트 코드를 끼워 맞추는 사후약방문(Afterthought)을 박살 내고, 테스트가 시스템의 뼈대와 설계를 강제로 이끄는 개발의 물리적 순서를 전복(Inversion)시키는 방법론을 해부합니다.
학습자는 실패하는 테스트를 먼저 짜고(Red), 어떻게든 통과하게 꼼수를 부린 뒤(Green), 코드를 아름답게 깎아내는(Refactor) TDD의 심장 박동을 뜯어봅니다. 나아가 모든 테스트 코드를 기승전결로 나누어 가독성의 극치를 달리는 **AAA 패턴 (Arrange-Act-Assert)**을 장악합니다. 마지막으로, 특정 메서드가 호출됐는지만 따지는 얄팍한 모키스트(Mockist) 방식을 버리고, 객체의 본질적인 상태 변화를 추적하여 튼튼한 리팩터링 내성을 확보하는 상태 검증(State Verification) 기반의 클래시스트(Classicist) 철학 역량을 확보합니다.
2. Scope & Boundaries
In-Scope
- Test-Driven Development (TDD): Red-Green-Refactor 사이클, 점진적 설계 진화, "동작하는 깨끗한 코드".
- AAA Pattern: Arrange (준비), Act (실행), Assert (단언/검증) 3단계 분할 원칙. Given-When-Then 매핑.
- Classicist vs Mockist: TDD의 두 학파. 상태 검증(State Verification)을 중시하는 시카고 학파(Classicist)와 행위 검증(Behavior Verification)을 중시하는 런던 학파(Mockist)의 차이.
- Test Brittleness 방어: 구현 세부 사항(Implementation Details)을 테스트하지 않고 공용 인터페이스(Public Interface)만 테스트하여 리팩터링 내성 확보.
Out-of-Scope
- Test Doubles (Mock, Stub)의 세부 문법: Mockito, Sinon 같은 라이브러리의 구체적 함수 사용법 09-04-01 Isolation Physics 영역에서 기학습됨.
- BDD(Behavior-Driven Development) 도구 연동: Cucumber 등을 이용한 기획-테스트 자동화 본 문서는 개발자 레벨의 TDD 설계론에 집중함.
Boundaries
- Test-First vs Test-Driven: TDD는 단순히 '테스트 코드를 먼저 짜는 것(Test-First)'이 아닙니다. 테스트 코드를 먼저 짰더니 "어? 이 클래스가 의존성이 너무 많아서 테스트 짜기가 빡센데? 파라미터를 좀 줄이고 의존성을 인터페이스로 빼야겠다(DI)"라며 코드의 설계(Design)가 변경되어야 비로소 TDD(Test-Driven, 테스트가 주도함)가 완성됩니다. 테스트 코드가 아키텍처의 부패를 감지하는 '탄광 속 카나리아' 역할을 한다는 점에 명확한 선을 긋습니다.
3. Counterexample
- 사후약방문 테스트 (Test After Development): 개발자가 1,000줄짜리 결제 로직을 다 짰습니다. 팀장이 "커버리지 80% 채워"라고 압박합니다. 개발자는 코드가 어떻게 도는지 이미 아니까, 통과할 게 뻔한 테스트 코드만 껍데기로 만듭니다. 사각지대(Edge Case)는 전혀 검증되지 않았으며, 코드는 1,000줄 그대로 스파게티 덩어리입니다. 코드를 다 짠 뒤에 짜는 테스트는 '설계를 개선'하는 TDD의 본질적 가치(Design Feedback)가 거세된 쓰레기 생산 과정입니다.
- 구현에 묶인 저주 (Implementation-Coupled Test): 개발자가 내부의
calculateTax()라는 Private 메서드를Reflection까지 써가며 억지로 테스트했습니다. 한 달 뒤 리팩터링을 하면서calculateTax()메서드명을calc()로 바꿨습니다. 비즈니스 로직은 멀쩡한데 테스트 코드 수십 개가 빨간불(Fail)을 뿜습니다. 공용 인터페이스(Public API)가 아닌 내부 구현 세부 사항(Implementation Details)을 검증하여, 개발자가 리팩터링을 할 때마다 테스트 코드를 뜯어고쳐야 하는 끔찍한 결합도(Brittleness)의 폭발입니다.
4. Prerequisites
- 테스트 피라미드와 격리 (Basic): Unit Test의 개념과 외부 의존성 차단(Isolation). (09-04-01 Testing Pyramid)
- 객체지향 설계 원칙 (Basic): 의존성 주입(DI)과 결합도(Coupling). (09-03-01 SOLID)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 심장 박동의 3단계 (Red-Green-Refactor)
- Why to Learn: 코드를 짜다가 길을 잃고 3시간 뒤에 "내가 지금 뭐 만들고 있었지?"라며 멘붕에 빠지는 현상을 막고, 3분 단위로 '작은 성취(Green)'를 이뤄내는 마이크로 매니지먼트를 뇌에 장착하기 위함입니다.
- What to Learn:
- Concepts: TDD Cycle, Red (실패하는 테스트), Green (가장 빨리 통과하는 쓰레기 코드), Refactor (중복 제거 및 설계 깎기).
- Skills: 덧셈 함수를 만들 때,
assertEquals(4, add(2, 2))라는 컴파일조차 안 되는 코드(Red)를 먼저 치고, 함수 내부에 냅다return 4;를 하드코딩해서(Green) 통과시킨 뒤, 마지막에return a + b;로 범용적 로직을 깎아내는(Refactor) 점진적 진화 수행하기.
- How to Learn:
- 1단계: Red (목적의 선언): 코드가 없습니다. 머릿속 비즈니스 요구사항("마일리지가 0 이하로 떨어지면 안 됨")을 테스트 코드로 번역합니다. 당연히 빨간불(Fail)이 뜹니다. 이 빨간불은 '내가 지금부터 10분 동안 달성해야 할 유일한 목표'가 됩니다.
- 2단계: Green (수단과 방법 가리지 않기): 우아한 패턴, 클린 코드 다 버립니다. 일단 무식하게
if/else로 떡칠해서 녹색불(Pass)만 봅니다. 작동하는(Working) 코드를 최단 시간에 확보하는 심리적 안전망입니다. - 3단계: Refactor (안전망 위에서의 춤): 이제 녹색불이 켜졌으니 내 뒤에는 '테스트'라는 튼튼한 그물이 있습니다. 더러운
if/else코드를 다형성(Polymorphism)으로 우아하게 바꿉니다. 바꾸고 났는데 여전히 녹색불입니까? 수술 성공입니다. 코드가 깨끗해집니다(Clean Code).
- Implement: TDD 사이클 타임(Cycle Time) 시뮬레이션.
Traditional: 기획 분석(1h) 코드 작성(3h) 수동 브라우저 테스트(1h) 버그 발견 어디서 터졌는지 디버깅(2h). (1 Cycle = 7시간).TDD: Red(3m) Green(5m) Refactor(2m). (1 Cycle = 10분). 버그의 생명주기를 10분 이내로 압살해버리는 피드백 루프의 가속도 렌더링.
Recommended
Core Topic 02: 가독성의 극치, AAA 패턴 (The AAA Pattern)
- Why to Learn: 남이 짠 100줄짜리 테스트 코드를 읽다가 눈알이 빠지는 고통을 없애고, 기승전결이 완벽하게 분리된 '문서로서의 테스트 코드'를 규격화하기 위함입니다.
- What to Learn:
- Concepts: Arrange (준비), Act (실행), Assert (단언), Given-When-Then, Test Fixture (테스트 환경).
- Skills: 데이터 세팅 코드, 메서드 호출 코드,
assertEquals코드가 뒤섞인 스파게티 테스트 코드를 3개의 블록(개행)으로 칼같이 찢고, 각 블록의 크기(Size)를 통제하기.
- How to Learn:
- 1단계: Arrange (무대 세팅):
User user = new User(1000); // 1000원 가진 유저 생성. 여기서 DB에 넣을 가짜 데이터, Mock 객체의 리턴값 등 모든 무대를 꾸밉니다. 제일 길어지기 쉬운 파트입니다. - 2단계: Act (방아쇠 당기기):
user.buyItem(500);. 우리가 검증하려는 진짜 타겟(SUT) 함수를 딱 한 번 호출합니다. 딱 한 줄이어야 합니다! 여기서 2~3개의 함수를 연달아 호출하면 테스트의 목적이 흐려집니다. - 3단계: Assert (결과 확인):
assertEquals(500, user.getBalance());. 액션의 결과가 내가 예상한 값과 정확히 일치하는지 심판을 내립니다. এই 3단계를 시각적으로 빈 줄(Blank line)로 띄워 읽기 쉽게 강제하는 규약을 뜯어봅니다.
- 1단계: Arrange (무대 세팅):
- Implement: 테스트 코드 가독성(Readability) 리팩터링 모사.
Bad:User u = new User(); u.add(10); if(u.get() == 10) System.out.println("OK"); else throw Error;.Good (AAA):// ArrangeUser user = new User(0);// Actuser.addPoint(10);// AssertassertEquals(10, user.getPoint());비즈니스 기획자의 Given-When-Then과 개발자의 AAA가 1<1로>1로> 매핑되는 마법 시각화.
Practical
Core Topic 03: 테스트가 설계하는 아키텍처 (Test-Driven Design)
- Why to Learn: TDD의 진짜 목적은 "테스트를 만드는 것"이 아니라, 테스트를 짜는 과정에서 내 시스템의 결합도(Coupling)가 쓰레기임을 조기에 발견하고 "테스트하기 쉬운 좋은 설계"로 강제 유도당하기 위함입니다.
- What to Learn:
- Concepts: Testability (테스트 용이성), Dependency Injection (의존성 주입), DIP, Hexagonal Architecture.
- Skills: "현재 시간을 기준으로 할인이 들어간다"는 로직을 짤 때, 코드 안에
new Date()를 쳐박아놓으면 테스트할 때 시간을 조작할 수 없다는 것(Untestable)을 깨닫고, 파라미터로Clock객체를 주입받는(DI) 형태로 설계를 뜯어고치기.
- How to Learn:
- 1단계: 테스트 불가능의 절망:
OrderService안에new MySQLDB()가 떡하니 박혀 있습니다. 테스트를 짤 때 Arrange 단계에서 DB 연결을 끊을 방법이 없습니다(격리 불가). "아, 내가 의존성 주입(DI)을 안 쓰고 강하게 결합해 놨구나!" - 2단계: 설계의 전복: 테스트를 통과하기 위해 어쩔 수 없이
OrderService의 생성자로DBInterface를 받게 고칩니다(의존성 역전). - 3단계: 강제된 클린 아키텍처: 코드를 먼저 짰다면 이 더러운 결합(Coupling)을 배포 후 버그가 터졌을 때 알았겠지만, TDD를 하면 코드를 짜기 전 테스트 세팅 단계에서 막혀버리므로, 숨쉬듯 아키텍처 결합도를 찢어발기게 되는 엔지니어링의 위대한 역학을 해부합니다.
- 1단계: 테스트 불가능의 절망:
- Implement: Testability 리트머스 테스트(Litmus Test) 렌더링.
Target: 룰렛 당첨 로직 (10% 확률로 벤츠 당첨).Test Attempt:Random.nextInt(10)하드코딩 되어있어, 100번 돌려도 벤츠 당첨 검증 불가(Untestable).Design Fix:Roulette(RandomGenerator gen)으로 생성자 주입. 테스트 시엔 무조건 당첨 번호를 뱉는FakeRandomGenerator주입. 확률과 우연(Nondeterministic)의 세계를 수학적 통제 하에 두는 설계 진화 시각화.
Advanced
Core Topic 04: 상태 검증과 리팩터링 내성 (State vs Behavior & Classicist)
- Why to Learn: "호출됐는가?"를 따지는 행위 검증(Mockist)에 빠져 모든 객체를 Mock으로 도배했다가, 내부 로직 한 줄 바꿨더니 테스트 수십 개가 다 터져버리는 끔찍한 결합(Brittleness)의 저주를 풀기 위함입니다.
- What to Learn:
- Concepts: State Verification (상태 검증), Behavior Verification (행위 검증), Refactoring Tolerance (리팩터링 내성), Black-box Testing vs White-box Testing.
- Skills: 외부 API 결제 연동(이건 진짜 Mocking 행위 검증 필요)과 내부 장바구니 계산 로직(이건 무조건 State 상태 검증)의 물리적 경계를 긋고, 내부 구현 세부 사항(Private 메서드 등)에 절대 Assert를 걸지 않는 통제력 발휘.
- How to Learn:
- 1단계: 모키스트의 함정 (Behavior): "장바구니 총액을 구하라"는 로직을 테스트합니다. "총액을 구할 때 1.
getPrices()를 호출했고, 2.sum()을 호출했네? Pass!" (행위 검증). 다음 날 최적화를 위해sum()대신fastSum()을 썼습니다. 결과값은 똑같은데 테스트는 Fail(왜sum안 불렀냐며 에러 뿜음)합니다. 리팩터링을 방해하는 '오지랖 넓은 테스트'입니다. - 2단계: 클래시스트의 반격 (State): 시카고 학파(Classicist)는 묵묵히 상태만 봅니다. "네가
sum을 부르든 곱하기를 하든 상관없고, 사과 3개면 총액3000원(상태)리턴하는 것만 볼게." 테스트 코드가 내부 구현(Implementation Detail)을 전혀 몰라서(Black-box), 코드를 엎어 쳐도 테스트 코드가 굳건히 버텨줍니다. - 3단계: 방탄 리팩터링 내성: 외부 세상과 소통하는 I/O 경계선 끝자락에만 최소한의 Mock(행위 검증)을 바르고, 핵심 비즈니스 도메인 내부는 철저히 State(상태 검증)로 몰아넣어 리팩터링 내성을 극대화시키는 테스트 아키텍처의 황금비를 뜯어봅니다.
- 1단계: 모키스트의 함정 (Behavior): "장바구니 총액을 구하라"는 로직을 테스트합니다. "총액을 구할 때 1.
- Implement: Implementation-Coupled(구현 결합) 테스트 파괴 모사.
SUT: 배열 정렬 함수sort().Mockist Test:verify(BubbleSortAlgorithm).sort()호출 검증.Action: 속도를 위해 알고리즘을QuickSort로 변경.Result: 정렬은 완벽히 되는데 테스트는 즉각 터짐(Brittleness 100%). 내부 과정(How)이 아닌 오직 외부로 드러난 결과(What)만 테스트해야 하는 본질적 통찰 렌더링.
7. Terminology
8. References
Primary
- [P1] CS2023 - Software Engineering (SE) - Software Verification and Validation (TDD)
- [P5] SFIA - Programming/Software Development (PROG) - Test-driven development
Secondary
- [Test-Driven Development: By Example] Kent Beck - The core of TDD (Red, Green, Refactor)
- [Unit Testing Principles, Practices, and Patterns] Vladimir Khorikov - AAA Pattern, State vs Behavior, Brittleness
Industry
- [MartinFowler.com] - Mocks Aren't Stubs (Classicist vs Mockist)
- [Google Engineering Practices] - Unit Testing (Test Behaviors, not methods)
9. Final Checklist
Primary
- 1,000줄짜리 더러운 레거시 코드를 다 짜놓고(Afterthought), 커버리지를 맞추기 위해 억지로 테스트 코드를 끼워 넣었을 때 절대 얻을 수 없는 '설계적 피드백(의존성 분리 등)'을 TDD의 Red-Green-Refactor 사이클이 어떻게 강제하는지 논증할 수 있는가?
- 테스트 코드 1줄부터 50줄까지 아무런 빈 줄(Blank line) 없이 빽빽하게 짜인 스파게티 테스트를, AAA(Arrange-Act-Assert) 패턴과 빈 줄을 활용해 3단계로 리팩터링하여 가독성을 극대화할 수 있는가?
Secondary
- 내부 비즈니스 로직(예: 할인율 계산)을 테스트할 때,
calcDiscount()함수가 내부적으로fetchConfig()함수를 호출했는지 따지는 행위 검증(Mockist)을 배제하고, 오직 최종 할인 금액 결과만 따지는 상태 검증(State Verification)으로 리팩터링 내성을 확보할 수 있는가? - TDD의 2단계인 Green(통과) 단계에서, 우아한 클린 코드나 디자인 패턴을 고민하지 않고
return 4;처럼 무식하게 하드코딩하여 1초 만에 녹색불을 켜는 것이 심리적 안전망 구축에 왜 필수적인지 심리학적 관점에서 설명할 수 있는가?
Industry
- 시스템 내부의 시간(Time)이나 난수(Random) 생성 로직 때문에 매번 결과가 달라지는 비결정론적(Nondeterministic) 코드를 마주했을 때, 외부에서
Clock이나RandomGenerator인터페이스를 주입(DI)받도록 설계 아키텍처를 뒤집어 테스트 가능성(Testability)을 100%로 끌어올릴 수 있는가? - 수백 개의 유닛 테스트가 돌아가는 CI(지속적 통합) 파이프라인에서, 테스트 코드들이 공용 DB(Shared Fixture)의 데이터를 서로 덮어쓰다 발생하는 동시성 간헐적 실패(Flaky Test)를 차단하기 위해, Arrange 단계에서 매번 깨끗한 인메모리 상태를 보장하는 격리 플로우를 설계할 수 있는가?