*객체 지향 개발* 에서 *테스트 를 학습* 한다는 것 의 *의미* — *JUnit 을 외우는 일* 이 아니라 *생각 의 형태 를 바꾸는 일**
“테스트 를 학습 한다” — 처음 듣는 신입 에게 이 말 은 JUnit 의 어노테이션 을 외우는 일 처럼 들린다.
@Test,assertEquals,Mockito.when().thenReturn()의 문법 학습.그러나 *9 년 을 지나고 보면 이 단어 의 *진짜 의미 는 전혀 다른 영역 에 있다. *테스트 를 배운다 는 것은 내 코드 의 *형태 자체 를 바꾸는 학습. 내 객체 가 *어떻게 *남과 대화 하는지, 어디서 *경계 가 깨지는지, 무엇이 *내 책임 이고 무엇이 *남의 책임 인지 를 눈에 보이는 형태 로 드러내는 학습*.
즉 *테스트 의 학습 은 객체 지향 의 학습 의 다른 이름.
이 글은 테스트 를 *어떻게 쓰는지 가 아니라 테스트 를 *학습 하는 것 이 내 머릿 속 에서 *무엇을 바꾸는가 * 에 대한 고찰. 9 년 의 실전 에서 내가 *오해 했다가 *교정 한 7 가지 의미 를 밀도 있게 풀어 낸다.
함께 보면 좋은 자매편 :
TL;DR — 한 줄 결론
“테스트 를 학습 한다” 의 진짜 의미 는 JUnit 의 문법 학습 도, 커버리지 의 수치 학습 도 아니다. 내 객체 가 *외부 의존 으로 부터 얼마나 분리 되어 있는지, *내 객체 의 *책임 이 *얼마나 좁고 분명 한지, *내 객체 의 *공개 API 가 *얼마나 정직 한지 — 이 세 가지 의 *피드백 을 *실시간 으로 받는 *훈련. 테스트 가 *쓰기 어렵다 는 것은 기술 의 부족 이 아니라 설계 의 신호. 그 신호 를 *듣는 귀 를 *기르는 일 이 테스트 학습 의 본질.
1. *오해 1 — *“테스트 = JUnit 의 문법”**
처음 3 년차 까지 의 내 *오해. JUnit / Mockito / AssertJ 의 문법 을 *능숙 해지면 테스트 를 잘 한다 고 믿었다.
1.1 *문법 의 *얕은 학습**
@Test
void test() {
// given
Order order = new Order(...);
when(repo.findById(1L)).thenReturn(Optional.of(order));
// when
OrderDTO result = service.getOrder(1L);
// then
assertEquals(order.getId(), result.getId());
}
이런 테스트 를 *수백 개 작성 했다. 모든 메서드 마다 *대응 테스트 1 개. 커버리지 88 %. PR 통과. 완료.
1.2 *문법 학습 의 *세 가지 함정**
- Mock 의 의미 를 모르고 *Mockito 만 안다. 왜 mock 하는지 가 아니라 어떻게 *when().thenReturn() 을 쓰는지* 만 안다.
- given/when/then 의 형식 은 따르되, 그 안에 들어 갈 *내용 의 *수준 은 고민 없음. 복사 붙여 넣기 의 의례.
- 테스트 가 *깨지면 *수정 한다. 왜 깨졌나 가 통찰 의 기회 인데 그저 *맞는 답 을 *재 주입.
이 단계 에서 나 의 코드 는 *변하지 않는다. 테스트 가 *나 의 *코드 의 형태 를 바꾸지 못한다.
1.3 벗어 나는 첫 신호
“이 테스트 가 *너무 *쓰기 힘들다” 가 반복 되는 순간. Mock 이 5 개 이상 필요 한 단위 테스트. given 블록 이 *30 줄. 어디 부터 손 댈지 막막함.
예전 의 나 — “내 Mockito 가 *서툴러서 그렇다”. *교재 를 다시 본다.
지금 의 나 — “내 *클래스 가 *너무 많은 일 을 한다”. *내 *경계 가 *흐릿하다. 테스트 가 *내게 *설계 의 빚 을 경고 하고 있다.
이 *번역 의 차이 가 학습 의 *진정한 시작.
2. *오해 2 — *“테스트 = *안전망”**
4 ~ 5 년차 의 내 *진화 한 오해. 테스트 는 *리팩토링 의 안전망 이라는 부분 적 으로 옳은 인식.
2.1 *안전망 의 *효용**
- 큰 리팩토링 시 기능 깨짐 *즉시 탐지.
- 수정 의 *심리 적 안전감.
- 수년 후 *후임 자 의 *암묵 적 문서.
이 시야 만 으로 도 코드 의 안전성 은 극적으로 개선. 이게 *대부분 의 회사 의 *공식 입장. “테스트 잘 짭시다, 안전 하게 갑시다”.
2.2 *그러나 *안전망 의 *한계**
테스트 를 *안전망 으로 만 보는 시야* 는 코드 가 작성 된 후 의 *수동 적 검증 에 머문다. 테스트 가 *코드 의 *형태 를 결정 하지 못한다*.
- 코드 작성 → 테스트 작성 (사후)
- 테스트 가 *어렵 으면 *테스트 를 *우회 (private 노출, reflection, helper). 코드 는 그대로.
- 결과 — *테스트 가 *코드 의 *모양 의 *증명 이 아니라 코드 의 *현재 모양 의 *기념 비.
2.3 *이 시점 의 *조용한 진실**
안전망 시각 에서 의 *테스트 는 코드 보다 *늘 *2 단계 늦은 추격자. 시니어 개발자 의 *진짜 실력 이 테스트 학습 의 *다음 단계 부터 시작 된다.
3. *오해 3 — *“테스트 = 커버리지 의 수치”**
조직 차원 의 흔한 오해. PR 의 통과 조건 으로 Jacoco 80 %.
3.1 *수치 의 *유혹**
- 측정 가능, 자동화 가능, 보고 가능.
- 관리자 의 *주간 보고 에 깔끔 한 숫자.
- 통과 / 불통과 의 *명확한 기준.
3.2 *수치 의 *조용한 거짓말**
@Test
void test() {
service.doSomething(); // 호출 만 함
// assertion 없음
}
커버리지 — 100 %. 실제 검증 — 0 %.
또는 :
@Test
void test() {
when(externalApi.call()).thenReturn(MOCK_RESPONSE);
Result result = service.process();
assertEquals(MOCK_RESPONSE, result); // 사실상 mock 의 무한 반사
}
무엇 도 검증 안 된 *tautology. 그러나 *커버리지 + 통과.
3.3 *수치 의 *함정 의 *진짜 위험**
개발자 가 *커버리지 를 *맞추는 작업 으로 학습 의 방향 이 왜곡 됨. 진짜 테스트 의 가치 가 수치 의 *형식 적 충족 으로 환원.
9 년 의 경험 — 커버리지 80 % *수치 가 코드 품질 의 *명확한 지표 가 아니라는 사실 의 반복 적 확인. 수치 의 의미 는 그 안에 든 *테스트 의 *내용 으로 만 결정 됨*.
학습 의 진화 — “몇 % 인가” 가 아니라 “이 테스트 들 이 *진짜 무엇 을 검증 하는가” 의 질문 으로 전환*.
4. *전환점 — *“테스트 가 *설계 의 *피드백 이다”**
6 ~ 7 년차 의 결정 적 발견. GOOS 책 (Growing Object-Oriented Software, Guided by Tests, Freeman/Pryce) 을 통과한 후.
4.1 *설계 피드백 의 *정의**
테스트 가 *쓰기 *어렵다 면 *기술 의 부족 이 아니라 *설계 의 *건강 의 약점**.
| 테스트 의 증상 | 설계 의 진단 |
|---|---|
| Mock 이 5 개 이상 필요 | 클래스 가 *너무 많은 의존성 — 단일 책임 위반 |
| given 의 *셋업 30 줄 | 생성자 가 *너무 많은 일 — Builder 또는 *분리 필요* |
| private 메서드 를 *직접 테스트 하고 싶다 | 그 *내부 로직 이 *별도 클래스 의 *공개 책임 — 클래스 추출 필요 |
| static 메서드 / new 키워드 가 테스트 막는다 | 의존성 *제어 불가 — 생성자 주입 누락 |
| 테스트 가 *내부 구현 알아야 통과 | 공개 API 가 *내부 노출 함 — 캡슐화 약화 |
| 동일 테스트 가 *여러 곳 에 *복사 됨 | 공통 추상 의 누락 — 추상화 의 *실패 |
이 표 의 모든 진단 의 *치료 가 테스트 가 아니라 *프로덕션 코드 의 *재 설계.
4.2 *Listen to your tests**
GOOS 의 유명 한 구호. 테스트 의 *통증 은 *진단 *. *진단 을 *듣고 *처방 은 *프로덕션 코드 의 *재 구성.
이 한 줄 의 *철학 이 테스트 의 의미 를 영구 적으로 바꾼다.
4.3 내 *settlement 프로젝트 의 *경험적 증거**
settlement 의 *Outbox Publisher 의 첫 버전 — 테스트 시 *Kafka mock + DB mock + Clock mock + Transaction mock + Metrics mock = 5 개 mock. 테스트 30 줄.
기능 동작. 그러나 *읽기 어렵. 수정 어렵.
GOOS 적 진단 — “5 개 mock = 5 개 의 협력자 = 단일 책임 위반”.
재 설계 — Publisher 를 *3 개 클래스 로 분리 :
OutboxLoader(DB 만)KafkaSender(Kafka 만)OutboxOrchestrator(둘 의 조정 + Metrics + Clock)
각 클래스 의 테스트 — *mock 1 ~ 2 개. 명확한 책임. 주석 거의 없어도 *읽힘.
프로덕션 코드 의 *품질 향상 의 *직접 적 인 *원동력 이 *테스트 의 통증.
5. *깊이 — *“테스트 = *설계 의 *시간 적 압축”** (TDD)
테스트 가 *설계 피드백 이면 그 피드백 을 *코드 *작성 전 에 받자 — TDD 의 본질.
5.1 *Kent Beck 의 *Red-Green-Refactor**
- Red — *실패 하는 테스트 작성 (원하는 동작 의 선언).
- Green — *최소한 의 코드 로 통과 시킴.
- Refactor — *동작 보존 + 설계 개선.
표면 적으로 *순서 의 문제. 진짜 의미 는 *순서 의 문제 가 아니다.
5.2 *TDD 가 *진짜 학습 시키는 것**
- 원하는 *공개 API 를 코드 쓰기 전에 *상상 한다. 호출 자 의 시점 으로 내 객체 를 보는 *훈련.
- 최소 단위 의 변화 — 한 번에 *한 책임 만 추가. 작은 단위 의 *결정 의 *축적.
- Refactor 를 *별도 단계 로 제도화 — 설계 의 *지속 적 개선 이 부차 적 활동 이 아닌 *중심 단계.
5.3 *Ian Cooper 의 *경고**
“TDD, where did it all go wrong?” 강의 의 결정 적 통찰 :
Kent Beck 의 *Test 는 행위 의 단위 (unit of behavior) 였다. 우리 가 *오해 하고 클래스 / 메서드 단위 로 테스트 하기 시작 했다. *결과 — *과도한 Mock + 내부 구조 결합 + 리팩토링 의 *불가능.
올바른 *TDD 의 단위 — 시스템 의 *경계 (port) 또는 기능 의 *행위. 예 :
- Order 의 *결제 처리 행위 (입력 → 출력 + 부수 효과).
- 클래스 A 의 *메서드 m 이 아니라.
이 한 줄 의 의미 의 차이 가 테스트 의 *모양 과 *유지 보수성 을 *전혀 다른 차원 으로 바꾼다*.
5.4 *Mockist vs Classicist 의 *고전 적 분기**
| Mockist (London 학파) | Classicist (Chicago 학파) |
|---|---|
| 모든 협력자 를 *mock | 실제 객체 사용, 외부 경계 만 mock |
| 상호 작용 (호출 의 순서 / 횟수) 검증 | 최종 상태 (output / state) 검증 |
| 완전한 격리 | 작은 통합 (sociable test) |
| Mockito / EasyMock 적극 | POJO 의 *new + assertion 중심 |
양 학파 의 *오랜 논쟁. 대답 — *둘 의 *조합. 외부 경계 (DB, Kafka, 외부 API) 는 *mock. 내부 도메인 객체 는 *실제 사용.
이 균형 이 내 *settlement / sparta 의 *테스트 패턴. 내 *9 년 의 *경험 적 수렴.
6. *경계 — *“테스트 = *시스템 의 *경계 의 *공식 문서”**
8 ~ 9 년차 에서 깨닫는 다음 층.
6.1 *Port / Adapter 의 *경계 가 *테스트 의 단위**
Hexagonal Architecture 의 시각 :
- 도메인 모델 — 외부 와 격리. 순수 한 Java 객체. 테스트 가 *극도로 간단. Mock 0 개.
- Application Service (포트) — 도메인 의 조정. 외부 의존 은 *인터페이스 로 추상화. 테스트 시 *내부 도메인 은 실제 + 외부 포트 만 mock.
- Adapter (DB, Kafka, REST) — 각각 별도 통합 테스트 (Testcontainers 등 활용).
결과 — 테스트 가 *시스템 의 *경계 의 *공식 적 인 *문서. 어디 가 *내 도메인 책임 이고 어디 가 *외부 책임 인지 코드 보다 *명료.
6.2 *settlement 의 *ArchUnit — *경계 의 *컴파일 시 강제**
나의 settlement 에선 *ArchUnit 으로 3 가지 규칙을 컴파일 차원 에서 강제:
- Domain 패키지 가 *Spring 의 어떤 클래스 도 *의존 못 함.
- Application 의 *비즈니스 로직 이 *JPA 직접 사용 못 함.
- Adapter 간 *교차 의존 금지.
이게 테스트 의 *최고 형태 중 하나. 런타임 의 *행위 검증 이 아니라 구조 의 *불변식 검증. 팀 의 *수십 명 의 *우연한 위반 을 *컴파일 시 차단.
6.3 *Specification 으로 서 의 *테스트**
“테스트 가 코드 의 *동작 을 *증명 한다”* 보다 “테스트 가 *동작 의 *정의” 다 의 시각.
- 테스트 = *기대 의 *공식 적 *언어.
- 프로덕션 코드 = *그 기대 의 *실행 가능 한 *구현.
- 그 둘 의 *동시 진화 가 *시스템 의 본질.
BDD (Behavior-Driven Development) 의 given-when-then 의 진짜 의미 가 이 시각. 비즈니스 의 *언어 가 테스트 의 *형식 으로 직접 표현*.
7. *철학 — *“테스트 = *자기 의 *과거 + 미래 의 자아 와 의 대화”**
가장 깊은 층. 9 년 의 사고 추적 이 비로소 알게 한 시점.
7.1 *과거 의 자아 의 *의도 의 보존**
몇 달 후 의 내가 *이 코드 를 본다. 왜 *이렇게 분기 했는지 기억 못 함. git blame 봐도 *그 당시 의 *전체 맥락 은 재현 불가.
그러나 *테스트 가 있으면 :
- 입력 의 *특정 패턴 에서 출력 의 *특정 패턴 이 의도 적 임 을 밝힘.
- 각 분기 의 *존재 이유 가 *별도 테스트 로 *기록.
- 변경 시 *내가 *모르고 깨면 *과거 의 *내가 즉시 경고.
테스트 가 *시간 을 *가로지르는 *내 *의도 의 *서명.
7.2 *미래 의 협력자 의 *학습 곡선 단축**
내 동료 가 *내 코드 를 *읽는다. 프로덕션 코드 만 보면 *what 은 알아도 *why 는 모름.
테스트 가 :
- 공개 API 의 *대표 적 사용 패턴 을 코드 형태로 *전시.
- 예외 적 케이스 의 *명시 적 처리 가 각자 의 테스트 로 *분리 되어 *읽기 쉬움.
- 변경 의 *영향 범위 가 *깨지는 테스트 의 *분포 로 자동 추적.
테스트 가 *동료 에 대한 *시간 적 친절.
7.3 *나 자신 의 *집중 의 *외주화**
Kent Beck 의 덜 알려진 통찰 :
“테스트 의 *진짜 가치 는 *내가 *코드 작성 중 에 기억 해야 할 것 의 외주. 내가 *지금 이 한 줄 에 집중 할 수 있는 전제 조건.”*
테스트 가 *내 *전체 시스템 의 *불변식 을 *지키는 동안, 나는 *지금 *이 한 줄 의 부분 적 문제 에 *완전 집중 가능. 인지 부하 의 분산.
이게 *9 년 이 *알려준 *가장 깊은 의미. 테스트 의 학습 = *집중 의 *경제학 의 학습.
8. *학습 의 *단계 의 *정리**
이 글 의 7 가지 의 의미 를 학습 의 단계 로 다시 정리.
| 단계 | 의미 | 기간 |
|---|---|---|
| 1 | JUnit 의 문법 | 0 ~ 6 개월 |
| 2 | 테스트 = 안전망 | 6 개월 ~ 3 년 |
| 3 | 커버리지 의 *수치 | 조직 따라 *수년 정체 가능 |
| 4 | 테스트 = *설계 피드백 (GOOS) | 4 ~ 6 년 (책 / 멘토 / 사고 가 촉발) |
| 5 | TDD — *설계 의 *시간 적 압축 | 5 ~ 8 년 (Ian Cooper 의 교정 포함) |
| 6 | 경계 의 *공식 문서 (Hexagonal / ArchUnit) | 7 ~ 9 년 |
| 7 | 시간 적 자아 와 의 *대화 | 지속 적 *내면 화 |
기간 은 *대략 적. 조직 / 멘토 / 본인 의 *호기심 의 *3 차 함수.
중요 한 한 가지 — 위 의 단계 가 *순차 적 *덮어 쓰기 가 아니라 *누적 적 *층 위. 8 년차 도 *문법 사용. *수치 의 *함정 도 *반복 적 으로 *재 발견. 학습 은 *나선 형.
9. *추천 *학습 자원**
9.1 책
- “Growing Object-Oriented Software, Guided by Tests” (Freeman/Pryce, 2009) — OOP + 테스트 의 *가장 깊은 합본. 내 *6 년차 의 *전환 점.
- “Test-Driven Development by Example” (Kent Beck, 2002) — 원본. 짧지만 *밀도 ↑.
- “Working Effectively with Legacy Code” (Michael Feathers, 2004) — 테스트 없는 코드 를 *어떻게 *테스트 가능 한 *상태로 끌어 올리는지. 현실 의 *대부분 의 *상황.
- “xUnit Test Patterns” (Gerard Meszaros, 2007) — 테스트 코드 의 *디자인 패턴 의 *백과 사전.
- “Refactoring” (Martin Fowler, 2nd ed., 2018) — 테스트 와 *불가분 의 *짝.
9.2 강의 / 영상
- Ian Cooper — “TDD, where did it all go wrong?” (2017 NDC London). 25 분 의 *교정.
- Sandi Metz — “The Magic Tricks of Testing” (RailsConf 2013). 언어 가 Ruby 지만 *원리 는 보편.
- Kent Beck — “Test Desiderata” 시리즈. 짧은 12 편 의 *깊은 명상.
9.3 *훈련 의 *습관**
- 매주 *한 개 *kata (CodingDojo.org).
- 기존 코드 의 *“이 테스트 *왜 *쓰기 어렵나?” 5 분 *자문.
- 팀 코드 리뷰 시 *“이 테스트 가 *몇 개 의 *Mock 을 요구 하는가?”* 를 공식 적 *질문 으로 추가.
10. *결론 — *7 가지 의미 의 *동시 적 *진실**
“객체 지향 개발 에서 *테스트 를 *학습 한다는 것* * 의 의미 :
- JUnit 의 *문법 을 *익히는 것 — 맞다, 그러나 *시작 일 뿐.
- 코드 의 *안전망 을 *만드는 것 — 맞다, 그러나 *수동 적 임.
- 커버리지 의 *수치 를 채우는 것 — 대부분 *함정.
- 설계 의 *피드백 을 *듣는 것 — 진짜 의 시작.
- 설계 를 *시간 적 으로 *압축 하는 것 (TDD) — 훈련 가능 한 *기량.
- 시스템 의 *경계 의 *공식 문서 화 — 조직 차원 의 *효과.
- 시간 적 자아 + 미래 동료 와 의 *대화 의 *제도화 — 가장 깊은 의미.
이 7 가지 가 *동시 적 *진실. 어느 하나 가 *다른 것 을 *대체 하지 않는다. 각자 의 깊이 의 층 위 가 내 *경험 의 *9 년 동안 *서로 *자랐다.
테스트 를 *학습 한다 는 것은 결국 *내 *객체 가 *어떻게 *살아 있는지 를 나 자신 에게 *반복 적으로 *질문 하는 *훈련. 그 질문 의 *형식 이 *JUnit 의 *어노테이션 이라는 사소한 사실 에 너무 일찍 만족 하지 말기.
다음 으로 *권 하는 읽기**
- Sandi Metz — “Practical Object-Oriented Design in Ruby” (POODR). OOP 의 *깊이 자체. 언어 와 무관 한 *통찰.
- Robert C. Martin — “Clean Code” 의 테스트 챕터. F.I.R.S.T 원칙 (Fast, Independent, Repeatable, Self-validating, Timely).
- Hexagonal Architecture 의 원작 — Alistair Cockburn 의 *2005 글. 경계 의 *철학.
- 자매편 — 내 *5/29 글 TDD / Mockito / 가치 / Spring 의 역사 — 역사 적 시점 의 보충.
다음 글 — 이 7 가지 의 의미 가 *내 *settlement / sparta-msa / lemuel-xr 의 실제 PR 들 에 *어떻게 적용 되는지 의 3 부 케이스 시리즈 — 곧.