“결제 수단 하나 추가하는 건데 왜 이렇게 오래 걸려요?”
이전 글 코드 냄새 #4에서 이어지는 내용입니다.
기획 쪽에서 자주 나오는 질문입니다. 개발자 입장에서는 대답하기 난처한 질문이기도 하고요.
실제로 고칠 곳이 열두 군데라서 그렇습니다. enum에 케이스 추가, switch 다섯 곳 수정, 아이콘 매핑 테이블, 문자열 상수, 분석 이벤트 이름, 서버 파라미터 변환, 테스트 픽스처.
각 수정은 두세 줄이고 어려운 건 없습니다. 어려운 건 열두 곳을 빠짐없이 찾는 일입니다.
마틴 파울러가 『리팩터링』에서 정리한 코드 냄새 목록에 이 상황의 이름이 있습니다. 산탄총 수술(Shotgun Surgery)입니다(저자 원문).
이름이 정확한 이유
산탄총은 한 발을 쏘면 탄이 넓게 흩어집니다. 변경 하나가 코드베이스 여기저기에 흩뿌려진 작은 수정으로 나타난다는 뜻이죠.
상처가 깊지는 않은데 개수가 많고 하나라도 놓치면 문제가 됩니다.
빠뜨렸을 때가 진짜 문제입니다. 열두 곳 중 열한 곳을 고치고 배포하면, 남은 한 곳이 특정 조건에서만 드러납니다.
결제 화면은 잘 되는데 영수증 화면에서만 “알 수 없는 결제 수단”이 뜨는 식이죠. 산탄총 수술의 비용은 수정 시간이 아니라 누락 확률입니다.
거울상이 하나 있습니다
같은 목록에 정반대 냄새가 있습니다. 발산적 변경(Divergent Change, 번역서에 따라 ‘뒤엉킨 변경’)입니다.
- 발산적 변경: 한 모듈이 여러 가지 서로 다른 이유로 자주 바뀝니다. 데이터베이스를 바꿔도 이 파일을 고치고 결제 규칙이 바뀌어도 이 파일을 고칩니다.
- 산탄총 수술: 한 가지 이유의 변경이 여러 모듈을 건드립니다(Shotgun Surgery 원문).
둘의 관계를 그림으로 그리면 축이 뒤집혀 있습니다.
| 발산적 변경 | 산탄총 수술 | |
|---|---|---|
| 관심사와 코드 | 한 곳에 여러 관심사 | 한 관심사가 여러 곳에 |
| 증상 | 이 파일이 계속 바뀐다 | 이 변경이 계속 퍼진다 |
| 처방 | 나눈다 | 모은다 |
처방이 정반대라는 점이 중요합니다. 코드 냄새 이야기가 대개 “쪼개라”로 흘러가는데, 산탄총 수술의 답은 합치는 것입니다.
무엇이 문제인지 잘못 짚으면 처방이 정확히 반대 방향으로 갑니다.
이 둘을 가르는 축은 하나입니다. 변경 이유.
단일 책임 원칙을 “한 가지 일만 해야 한다”가 아니라 “변경 이유가 하나여야 한다”로 정의하는 이유가 여기 있습니다. 일의 개수는 세는 사람마다 다르지만 변경 이유는 실제 커밋 기록으로 확인할 수 있습니다.
커밋 기록으로 실측하기
느낌 대신 데이터를 쓸 수 있습니다. 산탄총 수술은 늘 함께 바뀌는 파일들로 나타납니다.
이걸 변경 결합(change coupling) 또는 논리적 결합이라고 부릅니다.
import 관계로는 안 보이는 결합이라는 게 핵심입니다. 두 파일이 서로를 참조하지 않는데도 항상 같은 커밋에 등장한다면, 코드에 안 적힌 규칙이 둘을 묶고 있습니다.
최근 커밋에서 함께 바뀐 파일 쌍을 세어 보면 후보가 나옵니다.
git log --format='%H' --since=6.months.ago | while read c; do
git show --format= --name-only "$c" | grep '\.swift$' | sort | \
awk 'NR==FNR{a[NR]=$0;n=NR} END{for(i=1;i<n;i++)for(j=i+1;j<=n;j++)print a[i]" + "a[j]}'
done | sort | uniq -c | sort -rn | head -20
상위에 올라온 쌍이 정말 함께 있어야 할 코드인지 보면 됩니다.
물론 같이 바뀌는 게 자연스러운 쌍도 있습니다. 구현과 그 테스트 파일처럼요.
걸러 내고 남은 것들이 살펴볼 대상입니다.
모으는 방법
흩어진 조건 분기를 타입 하나로 모읍니다. 가장 흔한 형태입니다.
// 전: 결제 수단을 아는 switch가 프로젝트 여기저기에
func iconName(for method: PaymentMethod) -> String {
switch method {
case .card: return "creditcard"
case .transfer: return "building.columns"
}
}
func displayName(for method: PaymentMethod) -> String { ... }
func serverCode(for method: PaymentMethod) -> String { ... }
이 세 함수가 서로 다른 파일에 있으면 결제 수단이 추가될 때마다 세 곳을 찾아야 합니다. 한 타입으로 모으면 한 곳이 됩니다.
// 후: 한 곳만 보면 됩니다
extension PaymentMethod {
var iconName: String { ... }
var displayName: String { ... }
var serverCode: String { ... }
}
컴파일러에게 누락 탐지를 맡깁니다. Swift에서 switch는 모든 케이스를 다뤄야 합니다.
enum에 케이스를 추가하면 default가 없는 모든 switch가 컴파일 에러를 냅니다. 산탄총 수술을 없애지는 못하지만 누락을 런타임에서 컴파일 타임으로 옮겨 줍니다.
앞서 말한 진짜 비용이 누락 확률이었으니, 이것만으로도 성격이 달라집니다.
그래서 enum을 다루는 switch에 default: break를 습관적으로 넣으면 안 됩니다. 이 안전망을 스스로 끊는 셈이거든요.
문자열 키를 타입으로 바꿉니다. 분석 이벤트 이름, 사용자 기본값 키, 알림 이름이 문자열 리터럴로 흩어져 있으면 컴파일러가 도와줄 수 없습니다.
한 곳에 상수로 모으거나 타입으로 감싸면 추가·변경 지점이 하나가 됩니다.
설정을 데이터로 만듭니다. 결제 수단마다 아이콘·이름·코드가 필요하다면, 코드 대신 구조체 배열 하나로 정의할 수도 있습니다.
새 수단은 배열에 항목 하나 추가로 끝납니다.
모으기 어려우면 표시라도 남깁니다. 물리적으로 못 모으는 경우가 있습니다.
서버와 클라이언트에 같은 규칙이 있어야 한다든지요.
그럴 땐 각 지점에 서로를 가리키는 주석을 달거나, 값이 어긋나면 실패하는 테스트를 둡니다. 어차피 사람 기억에 의존해야 한다면 최소한 기억할 위치는 적어 두는 셈이죠.
나누기와 모으기 사이
여기서 앞 편들과 이어집니다. 코드 냄새 처방은 대체로 나누기와 모으기 두 방향뿐인데, 각 방향을 과하게 밀면 다른 냄새가 됩니다.
- 발산적 변경을 고치려고 나누다가 → 라비올리 코드
- 산탄총 수술을 고치려고 모으다가 → 갓 오브젝트
- 계층으로 나누다가 → 라자냐 코드
그래서 목표를 조각의 크기나 개수로 잡으면 안 됩니다. 목표는 하나입니다.
함께 바뀌는 것은 함께 두고, 따로 바뀌는 것은 따로 둔다. 응집도와 결합도가 결국 말하는 것도 이 문장입니다.
판단이 애매할 때 쓸 수 있는 질문이 있습니다. “가장 최근에 이 코드를 왜 고쳤는지” 세 번쯤 떠올려 보는 겁니다.
이유가 매번 달랐다면 나눌 차례고, 매번 같은 이유로 여러 곳을 고쳤다면 모을 차례입니다.
정리하면
- 산탄총 수술은 한 가지 변경이 여러 파일의 작은 수정으로 흩어지는 상태입니다.
- 실제 비용은 수정 시간이 아니라 누락 확률입니다.
- 정반대 냄새인 발산적 변경은 한 파일이 여러 이유로 바뀌는 상태고 처방도 정반대입니다. 하나는 모으고 하나는 나눕니다.
- 커밋 기록에서 늘 함께 바뀌는 파일 쌍을 뽑으면 후보를 실측할 수 있습니다.
- Swift에서는 enum과
switch의 전수 검사가 누락을 컴파일 에러로 바꿔 줍니다.default를 습관적으로 넣지 않는 게 그 안전망을 지키는 방법입니다.
다음 편은 이 모든 냄새를 한 단어로 부르게 된 은유를 다룹니다. 기술 부채.
워드 커닝햄이 말한 원래 뜻은 “지저분한 코드”가 아니었습니다.
출처 및 확인 기준
- Refactoring — Martin Fowler · 저자 원문 · 확인 2026-08-17 · 근거: Shotgun Surgery·Divergent Change 코드 냄새와 리팩터링 원칙
- The Shotgun Surgery Problem — Martin Fowler · 저자 원문 · 확인 2026-08-17 · 근거: 변경이 여러 모듈에 흩어지는 산탄총 수술 사례

![[코드 냄새 #5] 산탄총 수술 vs 발산적 변경 대표 이미지](/assets/images/posts/d4a472ee-9fec-4ab5-9b61-0e9875b3dae5/shotgun-surgery-scattered-changes.jpg)