테스트 주도 개발 실전 강의 교재¶
2장 — 타락한 객체¶
원서: 켄트 벡 『Test-Driven Development: By Example』 1부(화폐 예제) 대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → 따라하기(Red-Green-Refactor) → 핵심 교훈 → 현업 예제 → 함정 → 체크리스트 → 퀴즈(정답 분리) 전제 환경: Java 17+, JUnit 5
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 1장에서 드러난 부작용(자기 자신을 변질시키는 연산) 문제를 테스트로 잡아 고친다.
- 연산이 새 객체를 반환하게 만들어 Money를 값 객체(Value Object)로 진화시킨다.
- "작은 테스트 하나가 설계를 밀어붙인다"를 다시 한 번 체감한다.
0.2 큰 그림 — 1장이 남긴 빚을 갚는 장¶
1장 끝: five.times(2) → five 자신이 10으로 변함 (값이 타락)
2장 목표: five.times(2) → '새로운' 10짜리 Dollar를 반환, five는 5 그대로
비유 — "자판기": 동전(5)을 넣으면 새 음료(10)가 나옵니다. 넣은 동전이 음료로 둔갑하지 않습니다. 마찬가지로 숫자 5에 2를 곱하면 새로운 10이 나올 뿐, 5 자체가 10이 되지는 않습니다. 값은 그렇게 다뤄야 자연스럽습니다.
0.3 현업에서 왜 중요한가¶
Money,좌표,기간같은 값은 변하지 않아야 동시성·예측가능성이 좋아집니다(이펙티브 자바 아이템 17 불변).- 자바의
Integer/String/BigDecimal이 모두 이 방식(연산은 새 값을 반환)으로 동작합니다.
1. 할 일 목록 갱신¶
1장에서 새 항목이 보였습니다. 목록을 다듬습니다.
2. 부작용을 드러내는 테스트 (RED)¶
비유 — "같은 동전으로 두 번 사보기"¶
값이 변질되는지 확인하려면, 같은 객체로 연산을 두 번 해보면 됩니다. 첫 연산이 원본을 망쳤다면 두 번째가 틀어집니다.
@Test
void 곱셈() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(10, product.amount);
product = five.times(3); // five가 그대로 5여야 15가 나온다
assertEquals(15, product.amount);
}
1장의 구현(times가 amount를 바꿈)이라면, 첫 times(2) 이후 five.amount가 10이 되어 두 번째에서 30이 나옵니다 → 실패(RED). 이 테스트가 부작용을 폭로합니다.
테스트의 형태도 바뀌었습니다. 이제
times는 값을 반환하는 형태로 쓰입니다("어떻게 쓰고 싶은가"를 테스트가 먼저 선언).
3. 새 객체를 반환하게 만들기 (GREEN)¶
비유 — "기존 그릇을 비우지 말고, 새 그릇에 담기"¶
연산은 입력을 건드리지 않고 결과를 새 그릇에 담아 돌려줍니다.
class Dollar {
int amount;
Dollar(int amount) { this.amount = amount; }
Dollar times(int multiplier) {
return new Dollar(amount * multiplier); // 자신을 바꾸지 않고 새 Dollar 반환
}
}
테스트 초록. five는 끝까지 5를 유지하고, times는 매번 새 Dollar를 만들어 줍니다.
4. 무엇이 좋아졌나 — "값 객체(Value Object)"의 탄생¶
이 작은 변경으로 Dollar는 값 객체의 성질을 갖기 시작했습니다.
- 불변: 한 번 만든
Dollar의 값은 바뀌지 않는다. - 연산은 새 값을 반환:
Integer,String,BigDecimal처럼. - 부작용 없음: 같은 객체를 여기저기 공유해도 안전(동시성에 강함).
Dollar a = new Dollar(5);
Dollar b = a.times(2); // a는 그대로, b는 새 값
// 자바 표준 값들과 같은 사용감:
BigDecimal x = BigDecimal.valueOf(5);
BigDecimal y = x.multiply(BigDecimal.valueOf(2)); // x 불변, y는 새 값
다음 장으로: 값 객체는 "값이 같으면 같은 객체"로 취급되어야 자연스럽습니다. 그런데 지금
new Dollar(10)과new Dollar(10)은equals를 재정의하지 않아 서로 다른 객체로 취급됩니다(클린코드/이펙티브 자바 관점에서 미완성). 3장 "모두를 위한 평등"에서equals를 추가합니다.
핵심 교훈¶
- 부작용은 같은 객체로 연산을 반복해 보면 드러난다 — 그것을 테스트로 고정하라.
- 값을 다루는 연산은 자신을 바꾸지 말고 새 객체를 반환하라(불변, 이펙티브 자바 17).
- 작은 테스트 하나가 설계를 "값 객체"라는 더 나은 방향으로 밀어붙였다.
- 자바의
Integer/String/BigDecimal이 이미 이 패턴이다 — 우리 도메인 값도 그렇게.
현업 예제 — 도메인 값 객체로¶
// 가변 Money (위험): 공유 시 한 곳의 변경이 다른 곳에 번진다
class MutableMoney { long amount; void add(long v){ amount += v; } }
// 불변 Money (권장): 연산은 새 값을 반환
record Money(long amount, String currency) {
Money times(int multiplier) { return new Money(amount * multiplier, currency); }
Money plus(Money other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException("통화 불일치");
return new Money(amount + other.amount, currency);
}
}
교차 연결: 리팩터링 3.11(기본형 집착) 처방인 "기본형을 값 객체로", 이펙티브 자바 17(불변)·아이템 50(방어적 복사)과 한 흐름입니다. TDD는 이 설계를 테스트가 이끌어낸 결과로 도달합니다.
함정 / 주의¶
- 불변이라고 무조건 좋은 건 아니다. 다단계 누적 연산이 매우 잦은 핫패스라면 중간 객체 생성 비용을 고려하세요(이펙티브 자바 17의 가변 동반 클래스 — 예:
StringBuilder). - 테스트의 의도를 좁게 유지하라. 이번 테스트는 "부작용 없음"을 확인하는 것이지, 다른 기능을 섞지 마세요(클린코드 9장: 테스트 당 개념 하나).
- 아직
equals가 없어 값 비교가 안 됩니다. 다음 장의 빚으로 목록에 남겨둡니다.
체크리스트 (값 객체 만들기)¶
- 연산이 자기 자신을 변경하지 않고 새 객체를 반환하는가
- 필드가 변하지 않는가(불변)
- 같은 객체를 두 번 연산해도 결과가 일관적인가(부작용 없음 테스트)
- (다음 장) 값이 같으면 같다고 볼 수 있도록
equals를 준비했는가
퀴즈¶
- 객체의 "부작용(값 변질)"을 드러내는 테스트는 어떻게 설계하면 되는가?
times가 새Dollar를 반환하도록 바꾸면 어떤 설계 성질을 얻는가?- 이 변경이 자바의 어떤 표준 클래스들과 같은 사용감을 주는가?
- 2장이 끝난 뒤에도 남은 "빚"은 무엇이며, 어느 장에서 갚는가?
- 불변 값 객체가 동시성에 강한 이유는?
정답·해설¶
- 같은 객체로 연산을 두 번 수행해 봅니다. 첫 연산이 원본을 변질시켰다면 두 번째 결과가 틀어지므로(예:
five.times(2)후five.times(3)), 부작용이 테스트로 잡힙니다. - 값 객체(불변)의 성질을 얻습니다. 입력을 바꾸지 않고 결과를 새 객체로 돌려주어, 공유·재사용이 안전해집니다(이펙티브 자바 17).
Integer,String,BigDecimal등. 이들도 연산이 원본을 바꾸지 않고 새 값을 반환합니다.equals가 아직 없어 "값이 같으면 같다"가 성립하지 않습니다. 3장(모두를 위한 평등)에서equals를 추가해 갚습니다.- 생성 후 상태가 절대 바뀌지 않으므로, 여러 스레드가 동시에 읽어도 경쟁 상태가 발생하지 않기 때문입니다.
다음 장 예고 — 3장: 모두를 위한 평등¶
값 객체의 다음 의무는 "값이 같으면 같은 객체"입니다. equals를 테스트로 이끌어 추가하고(이펙티브 자바 아이템 10과 직결), 이어 4장에서 hashCode와 캡슐화(필드 private화)로 나아갑니다.