심
고친 사람 github-actions[bot]
심은 코드를 고치지 않고 그 부분의 동작만 바꿔 끼우게 해 줍니다. 테스트는 이 틈으로 진짜 부품 대신 가짜를 끼웁니다. 그래서 테스트가 없던 오래된 코드도 크게 뜯지 않고 테스트로 감쌀 수 있습니다. 한글로 심이라 적는 shim 은 다른 낱말입니다(상세 끝 절).
쉽고 빠른 이해
무엇인가 — 코드를 안 고치고 동작을 갈아 끼우는 틈입니다. 주문 클래스가 결제 부품을 밖에서 넘겨받으면, 테스트는 거기에 가짜 결제 부품을 넘깁니다.
왜 — 주문 코드가 진짜 결제를 직접 부르면 테스트할 때마다 진짜 돈이 나갑니다. 그렇다고 테스트를 붙이려고 코드부터 고치면, 고치는 동안 잘못을 잡아 줄 테스트가 없습니다.
어떻게 도나
- 바꾸고 싶은 호출 하나를 고릅니다
- 그 호출이 무엇을 부를지 바깥에서 정하게 만듭니다
- 운영 코드는 바깥에서 진짜를, 테스트는 가짜를 고릅니다
대가 — 틈마다 한 단계를 거쳐 부르게 됩니다. 코드를 읽는 사람은 실제로 무엇이 불리는지 바깥까지 거슬러 올라가야 압니다. 그래서 결제·데이터베이스·현재 시각처럼 테스트를 느리게 하거나 결과를 흔드는 부품을 부르는 곳에만 틈을 냅니다.
상세
이 절은 심이 무엇인지부터 정합니다. 그다음 주문과 결제 코드 하나를 들고 틈이 없는 모양과 틈이 난 모양을 견줍니다. 마지막에 틈을 내는 때가 셋으로 갈린다는 것과 틈을 안 내는 편이 나은 경우를 봅니다.
바꿔 끼우는 틈
벽에 붙은 콘센트를 떠올려 보세요. 전기 기구를 바꿀 때 벽을 뜯지 않습니다. 플러그만 뽑아 다른 것을 꽂습니다.
프로그램에도 이런 지점이 있습니다. 그 지점을 고치지 않고도 거기서 일어나는 동작을 바꿀 수 있는 곳을 심이라고 합니다. 영어 seam 은 옷감 두 장이 맞붙는 솔기, 곧 이음매를 뜻합니다.
틈이 없는 코드부터 봅니다. 주문을 처리하는 OrderService 가 결제할 때 카드 결제 부품을 안에서 직접 만듭니다.
class OrderService {
void checkout(Order o) {
var pg = new CardGateway(); // 틈 없음
pg.charge(o.total());
}
}
checkout 을 부르면 언제나 CardGateway 가 돕니다. 테스트에서 이 줄을 다른 것으로 바꿀 방법이 없습니다. 바꾸려면 checkout 본문을 고쳐야 합니다.
틈이 난 코드와 활성화 지점
이번에는 결제 부품을 인터페이스로 받습니다. 인터페이스는 「charge 를 부를 수 있다」는 약속만 적은 타입입니다. 이 약속을 지키는 클래스라면 무엇이든 넘길 수 있습니다.
interface PaymentGateway {
void charge(int amount);
}
class OrderService {
private final PaymentGateway pg;
OrderService(PaymentGateway pg) {
this.pg = pg;
}
void checkout(Order o) {
pg.charge(o.total()); // 여기가 심
}
}
이제 pg.charge 호출이 심입니다. checkout 본문은 한 글자도 안 바꿔도 여기서 무엇이 돌지가 달라질 수 있습니다.
무엇이 돌지를 실제로 정하는 곳은 따로 있습니다. 이곳을 활성화 지점이라고 부릅니다. 심 하나에는 활성화 지점이 하나씩 딸립니다. 위 코드에서는 OrderService 를 만들며 생성자에 무엇을 넘기느냐가 활성화 지점입니다.
flowchart TD
A["운영 코드"] -->|"CardGateway 를 넘긴다"| E["OrderService 생성자 · 활성화 지점"]
B["테스트 코드"] -->|"FakeGateway 를 넘긴다"| E
E --> S["pg.charge 호출 · 심"]
S --> R["넘겨받은 쪽의 charge 가 돈다"]
그림처럼 운영과 테스트는 같은 생성자로 모입니다. 심인 pg.charge 는 어느 쪽이 들어왔는지 모른 채 넘겨받은 것을 부릅니다.
테스트 쪽에서 넘기는 가짜는 이렇게 생겼습니다. 돈을 보내는 대신 받은 금액을 적어 두기만 합니다.
class FakeGateway implements PaymentGateway {
int last;
public void charge(int amount) {
last = amount;
}
}
테스트는 이 가짜를 넘기고 주문을 처리한 뒤 적힌 금액을 봅니다.
var fake = new FakeGateway();
var order = new Order(12000);
new OrderService(fake).checkout(order);
int got = fake.last; // 12000
진짜 돈은 한 푼도 안 나갔습니다. 그런데도 checkout 이 주문 금액으로 결제를 불렀다는 것은 확인했습니다. 진짜 부품 대신 끼우는 가짜를 테스트 더블이라고 합니다. 심은 끼울 틈입니다. 테스트 더블은 그 틈에 끼우는 물건입니다.
테스트 없는 코드를 고치는 순서
심이라는 말이 가장 많이 쓰이는 곳은 레거시 코드, 곧 테스트가 없는 오래된 코드입니다. 레거시 코드는 고치기가 두렵습니다. 고친 뒤에 어디가 깨졌는지 알려 줄 테스트가 없기 때문입니다.
테스트를 먼저 붙이면 될 것 같습니다. 그런데 거기서 막힙니다. 앞의 첫 번째 OrderService 처럼 진짜 결제나 데이터베이스를 안에서 직접 부르는 코드는 그대로는 테스트에 못 올립니다. 테스트를 붙이려면 코드를 고쳐야 합니다. 그런데 코드를 안전하게 고치려면 테스트가 있어야 합니다.
심이 이 고리를 끊습니다. 처음에는 틈 하나만 내는 아주 작은 수정을 합니다. 생성자에 인자 하나를 더하는 정도입니다. 이 정도 수정은 동작을 안 바꾸는 것이 눈으로 확인될 만큼 기계적입니다.
틈이 생기면 거기에 가짜를 끼워 테스트를 붙입니다. 그 뒤의 큰 수정은 테스트가 지켜 줍니다.
동작은 그대로 두고 구조만 바꾸는 일을 리팩터링이라고 합니다. 심을 내는 수정은 테스트 없이 하는 리팩터링이라 되도록 작게 잡습니다.
바꿔 끼우는 때에 따른 세 갈래
틈은 코드 안에만 있지 않습니다. 소스 코드가 실행되기까지는 여러 단계를 거칩니다. 어느 단계에서 바꿔 끼우느냐에 따라 심이 셋으로 갈립니다.
먼저 낱말 셋을 짚습니다. 전처리기는 컴파일 전에 소스 글자를 바꿔 쓰는 도구입니다. C 의 #define 이 대표입니다. 이 정의를 빌드 명령에서 넘기거나 테스트 빌드만 읽는 헤더에 두면, 같은 소스가 테스트 때만 다른 코드로 바뀝니다.
링커는 따로 컴파일된 조각들을 하나로 묶는 도구입니다. 묶을 때 함수 이름이 가리키는 실제 코드를 찾아 잇습니다. 자바에는 따로 묶는 단계가 없습니다. 대신 실행 중에 클래스를 처음 쓸 때 클래스패스에 적힌 경로에서 찾아 잇습니다.
| 갈래 | 바꾸는 때 | 활성화 지점 | 예 |
|---|---|---|---|
| 전처리 심 | 컴파일 전 | 빌드 명령에서 넘기는 정의 · 테스트 빌드만 읽는 헤더 | #define charge fake_charge 로 함수 이름을 가짜 함수로 바꾼다 |
| 링크 심 | 조각을 묶을 때 | 빌드 설정 · 클래스패스 | 테스트를 돌릴 때만 클래스패스 앞쪽에 가짜 CardGateway 클래스를 둔다 |
| 객체 심 | 실행 중 | 객체를 만드는 곳 | 위의 생성자에 가짜를 넘긴다 |
전처리 심과 링크 심은 코드 본문을 안 건드립니다. 대신 어느 정의를 넘길지, 어느 헤더를 읽을지, 어느 클래스를 앞에 둘지를 빌드 명령과 빌드 설정이 정합니다. 활성화 지점이 거기 있으니 소스만 읽어서는 안 보입니다.
객체 심은 다형성에 기댑니다. 다형성은 같은 호출이 받는 객체에 따라 다른 코드를 부르는 성질입니다. 활성화 지점도 소스 안에 보입니다. 그래서 객체 지향 언어에서는 이 갈래를 가장 많이 씁니다.
심을 내는 흔한 방법
객체 심을 내는 가장 흔한 방법은 의존성 주입입니다. 부품이 쓰는 다른 부품을 스스로 만들지 않고 바깥에서 받아 쓰게 하는 방식입니다. 위 예처럼 생성자로 받으면 생성자 주입이라고 합니다. 의존성 주입은 심을 냅니다. 동시에 활성화 지점을 바깥으로 빼 줍니다.
파이썬이나 자바스크립트처럼 실행 중에 함수를 바꿔 넣을 수 있는 언어도 있습니다. 테스트가 모듈의 함수를 가짜로 바꿔치기하는 것을 몽키 패칭이라고 합니다. 이 두 언어에서는 모듈의 함수를 부르는 곳이 이미 심인 셈입니다.
그래서 코드에 따로 틈을 내지 않아도 됩니다. 활성화 지점은 바꿔치기하는 테스트 코드 쪽에 있습니다. 대가는 무엇이 바뀌었는지 테스트 코드를 봐야만 안다는 것입니다.
틈을 안 내는 편이 나은 곳
심은 공짜가 아닙니다. 틈마다 한 단계를 거쳐 부르게 됩니다. pg.charge 를 읽는 사람은 실제로 어떤 charge 가 도는지 알려면 생성자를 부른 곳까지 거슬러 올라가야 합니다.
그래서 바꿔 끼울 까닭이 없는 부분에는 틈을 내지 않습니다. 값만 담는 객체나 계산만 하는 함수는 늘 같은 답을 곧바로 냅니다. 테스트에서도 진짜를 그대로 쓰면 됩니다.
틈이 필요한 곳은 테스트를 느리게 하거나 결과를 흔드는 부품입니다. 네트워크 너머의 서비스, 데이터베이스, 현재 시각, 난수가 그렇습니다. 이 넷을 부르는 호출이 심을 낼 첫 후보입니다.
같은 소리로 적는 shim
shim 은 원래 기계 부품 사이 틈을 메우는 얇은 쐐기를 뜻합니다. 소프트웨어에서는 두 코드 사이에 끼워 호출을 받아 넘기는 얇은 코드를 말합니다. 옛 함수 getUser() 를 지우지 않고 남겨 두고, 그 안에서 새 함수 findUser() 를 부르게 하는 코드가 한 예입니다.
seam 은 바꿔 끼울 수 있는 틈입니다. shim 은 틈에 끼워 넣어 어긋남을 메우는 코드입니다. 한글로는 둘 다 심이라 적어서 헷갈리기 쉽습니다. 이 항목의 심은 앞의 seam 입니다.
관련 항목
심에 끼우는 가짜
테스트 더블 · 목 객체 · 스텁 · 페이크 객체 · 테스트 스파이 · 더미 객체
심을 내는 설계 기법
의존성 주입 · 생성자 주입 · 인터페이스 · 다형성 · 의존성 역전 원칙 · 결합도
심이 생기는 빌드 단계
전처리기 · 매크로 · 컴파일러 · 링커 · 정적 링크 · 동적 링크 · 클래스패스
심으로 테스트를 붙이는 코드와 작업
레거시 코드 · 리팩터링 · 단위 테스트 · 특성 테스트 · 테스트 하네스 · 테스트 가능성
코드에 틈을 따로 내지 않고 바꿔치기하는 수단
심과 소리가 같은 호환 기법
shim · 폴리필 · 어댑터 패턴 · 하위 호환
다른 이름: seam · Seam