# 잘못된 추상화보다 중복을 선호하라 (2016)

> Clean Markdown view of GeekNews topic #30705. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30705](https://news.hada.io/topic?id=30705)
- GeekNews Markdown: [https://news.hada.io/topic/30705.md](https://news.hada.io/topic/30705.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-06-22T10:00:05+09:00
- Updated: 2026-06-22T10:00:05+09:00
- Original source: [sandimetz.com](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction)
- Points: 7
- Comments: 5

## Topic Body

- **잘못된 추상화**보다 코드 중복이 훨씬 싸며, **성급한 공통화가 장기 유지보수 비용을 키운다**고 봄  
- 처음에는 합리적이던 추출도 요구사항이 조금씩 달라지면 **매개변수와 조건문**이 붙어 원래 의도를 흐림  
- 공통 추상화가 여러 아이디어를 떠안기 시작하면 코드는 조건 중심 절차로 변하고, 새 기능을 넣을수록 더 깨지기 쉬워짐  
- 기존 코드에 들인 노력을 지키려는 **매몰비용 오류**를 경계하고, 필요하면 추상화를 호출부로 다시 인라인해 실제로 필요한 코드만 남겨야 함  
- 잘못된 추상화가 드러났다면 중복을 다시 도입해 현재 요구사항의 공통점을 새로 관찰하고, 그다음에 다시 추출하는 편이 더 빠름  
  
---  
  
### 잘못된 추상화가 만들어지는 흐름  
- “**duplication is far cheaper than the wrong abstraction**”라는 문장은 RailsConf 2014 발표의 일부였지만 이후에도 계속 회자됨  
- 흔한 실패 경로는 다음과 같음  
  - 개발자 A가 **중복**을 발견함  
  - 중복을 메서드나 클래스로 추출하고 이름을 붙여 새 추상화를 만듦  
  - 호출부의 반복 코드를 새 추상화 호출로 대체함  
  - 시간이 지나 거의 맞지만 완전히 같지는 않은 새 요구사항이 등장함  
  - 개발자 B가 기존 추상화를 유지하려고 매개변수를 추가하고, 값에 따라 다른 경로를 타는 조건문을 넣음  
  - 이후 새 요구사항마다 매개변수와 조건문이 늘어나며 코드 이해가 점점 어려워짐  
- 한 번 만들어진 코드는 **보존해야 할 투자**처럼 보이기 쉬움  
  - 이미 들인 노력을 아깝게 여기는 심리가 작동함  
  - 코드가 복잡하고 이해하기 어려울수록, 그만큼 중요하고 오래 걸렸을 것처럼 느껴져 버리기 어려워짐  
  - 이는 **매몰비용 오류**와 연결됨  
  
### 중복으로 되돌아가 다시 추출하기  
- 잘못된 추상화 위에서 새 요구사항을 계속 구현하면 공유 코드는 조건문 중심으로 변하고, 기능을 추가할수록 더 불안정해짐  
- 이때 빠른 길은 더 밀어붙이는 것이 아니라 **뒤로 돌아가는 것**임  
  - 추상화된 코드를 각 호출부에 다시 인라인해 중복을 재도입함  
  - 각 호출부에서 전달하던 매개변수를 기준으로 실제 실행되는 코드만 확인함  
  - 해당 호출부에 필요 없는 코드를 삭제함  
- 인라인 과정은 추상화와 조건문을 함께 제거하고, 각 호출부를 자신에게 필요한 코드만 가진 상태로 줄임  
- 같은 추상화를 호출하는 것처럼 보였던 코드도 실제로는 각 호출부가 상당히 **고유한 코드 경로**를 실행하고 있었을 수 있음  
- 이전 추상화를 완전히 제거한 뒤에야 중복을 다시 관찰하고, 현재 요구사항에 맞는 새 추상화를 추출할 수 있음  
- 매개변수와 조건 경로가 공유 코드에 계속 추가되고 있다면 그 추상화는 더 이상 맞지 않을 가능성이 큼  
  - 처음에는 맞는 추상화였을 수 있음  
  - 요구사항이 바뀌면서 더 이상 같은 형태로 유지하기 어려워졌을 수 있음  
- 잘못된 추상화에서는 중복을 다시 도입하는 일이 후퇴가 아니라 **더 나은 전진**이 됨

## Comments



### Comment 60141

- Author: dieafterwork
- Created: 2026-06-22T13:48:03+09:00
- Points: 1

이게 이분법적인 해석이 필요한 주제인지는 잘 모르겠습니다.

### Comment 60132

- Author: hanje3765
- Created: 2026-06-22T12:24:33+09:00
- Points: 1

오 공감갑니다.  
정리되지 않은건 정리하면 되지만  
정리되어 있는건 뒤집는데 비용이 더 크게 들어가는 것 같습니다.

### Comment 60128

- Author: jimmy2056
- Created: 2026-06-22T11:38:17+09:00
- Points: 1

ponytail 이 올라놨는데 바로 이런글이 ㅎㅎ

### Comment 60123

- Author: shakespeares
- Created: 2026-06-22T11:00:57+09:00
- Points: 1

항상 대립각이네요.

### Comment 60110

- Author: neo
- Created: 2026-06-22T10:00:07+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=48620090) 
- **단일 진실 공급원(single source of truth)** 원칙은 항상 지켜야 한다고 봄  
  서로 달라지면 버그가 되는 중복 코드라면 리팩터링해야 함. 그렇지 않으면 미래 개발자가 버그가 터지기 전까지 알아채기 어려운 **장거리 결합**이 생김  
  다만 그 원칙을 위반하지 않는다면 추상화는 편의일 뿐이고, 불편해지기 시작했다면 제 역할을 못 하는 것이라 쓸 이유가 없음. 함수가 맞춤 동작을 위해 여러 플래그를 필요로 한다면 잘못된 추상화이거나 단일 책임 원칙 위반일 가능성이 큼  
  정말 많은 맞춤화가 필요하다면 인자로 함수/펑터를 받는 방식이 자주 좋음. 예를 들어 `solve(f:double -> double, max_iters = 99, x_abs_tol = 1e-15, x_rel_tol = 1e-15, ...)` 대신 `solve(f:double -> double, stopping_criteria: StoppingCriteriaClass)`처럼 만들 수 있음
  - 글의 핵심은 아직 **진실 공급원**이 몇 개인지 명확하지 않은 경우를 다룬다는 데 있음  
    코드의 두 지점이 같은 알고리즘을 쓰는지, 아니면 살짝 다른 버전인지, 더 중요하게는 같은 이유로 바뀔 것인지가 불분명함  
    제목의 격언은 서로 다른 것을 억지로 같게 만드는 편이, 같은 것을 중복했다가 나중에 다르게 만드는 것보다 더 고통스럽다고 말하며 나도 맞다고 봄. 후자는 같은 변경을 두 번 하거나 추상화를 도입하는 리팩터링을 하면 되지만, 전자는 추상화에 계속 덧붙이거나 되돌려야 함  
    특히 **지역성(locality)** 을 깨뜨리는데, 변경할 때 정말 중요한 속성은 이것뿐임. 그냥 이 변경만 하고 시스템의 무관한 부분에 부작용이 날지 걱정하고 싶지 않음
  - 극심한 압박 때문에 소프트웨어가 **두 개의 진실 공급원**으로 밀려났다면, 두 소스가 맞지 않으면 main에 병합되지 않는 CI 테스트를 추가하는 방법이 꽤 쓸 만함  
    대표적으로 pyproject.toml / requirements.txt 동기화가 실제로 최선인 경우가 있고, 더 넓게도 적용 가능할 듯함. 전제는 이미 단일 진실 공급원이 불가능할 정도로 일이 틀어졌다는 것이며, 치료보다는 피해 감소에 가까움
  - “서로 달라지면 버그”라는 기준은 아주 좋은 경험칙임  
    한 시점에 두 코드 조각이 비슷해 보여서 과도하게 추상화했다가 나중에 갈라지는 일을 자주 겪었음
  - 이론적으로는 맞지만, 현실에서는 어떤 중복이든 무조건 피하려는 사람이 많음  
    특히 주니어 개발자는 **중복**이 모든 악의 근원인 것처럼 대하는 경우가 있음

- 가끔 이 문제를 생각함. 최근 개인 프로젝트에서 RTS 유닛용 **2D 스프라이트**를 다루다 마주쳤는데, 유닛 스프라이트는 스프라이트시트에 일관된 방식으로 들어 있었음: 8방향에 5개 스프라이트, 그중 3방향은 미러링하고, 순서는 stand, move, attack, die였음  
  그래서 action + direction을 받아 재생할 스프라이트 배열을 주는 로더를 만들었음  
  그런데 방향성이 없는 폭발 스프라이트, 4방향과 2개 미러링만 있는 시체 스프라이트, 게다가 첫 네 개를 제외하면 오크와 인간이 대부분 공유하는 경우가 나옴  
  이 모든 것의 공통 추상화가 대체 뭔지 잠깐 고민했지만, 결국 로딩 코드 일부만 분리하고 UnitLoader, CorpseLoader, EffectLoader를 만든 뒤 넘어갔음. 세 로더가 조금씩 같은 것을 다루므로 더 나은 추상화가 있을 수는 있지만, 나중에 발견하면 됨. 지금 복잡한 EverythingLoader를 만들어 모든 경우를 처리하려 하기보다, 나중에 그때 중복을 제거하는 편이 더 쉬움
  - “사물은 가능한 한 단순해야 하지만, 그보다 더 단순해서는 안 된다”는 말을 좋아함  
    프로그래밍에서는 일반화를 통해 코드를 단순화하려는 본능이 있지만, 현실은 지저분해서 종종 지나치게 단순화함. 본문처럼 시간이 지나 새 요구사항이 생기면 **너무 이른 단순화**였다는 게 드러남  
    “성급한 추상화는 많은 구림의 근원이다”라는 격언이 될 만함
  - 이미 공통 추상화는 분리되어 있을 가능성이 큼. 단일 스프라이트의 픽셀을 로드하고 표시하는 코드가 그것임  
    그 위 단계인 스프라이트시트 배치 해석과 재생 모드 처리는 여러 변형이 있고, 모든 경우에 맞는 공통 추상화가 없을 수 있음  
    보이지 않는 추상화를 억지로 만들거나 불완전한 추상화에 맞추려는 것보다, 지금처럼 하는 쪽을 선호함. 추상화가 완전히 명확하고 필요가 분명해질 때까지 기다리는 건 좋은 일임  
    DRY의 반대편 해독제로 **WET**가 있음. 모든 것을 두 번/세 번 작성하라는 뜻임. 더 중요하게는 실제로 입증된 사용 사례, 보통은 먼저 중복으로 드러난 것에 대해서만 추상화해야 한다고 봄. 아직 없는 미래 사용 사례를 위해 쓴 코드는 실제로 가진 것을 추상화하는 데 자주 방해가 되고, 그런 일이 벌어질 때마다 웃기기도 함
  - 이 방식이 맞음. 게임 만들기는 원래 재미있어야 함  
    어렵고 지루한 일은 프로젝트의 마지막 10%에 도달했을 때 해도 됨  
    게다가 중복이 만들어낸 “버그”가 플레이어가 좋아하는 재미있는 기능이 될 때도 있음

- OOP를 쓰던 시절에는 추상화 때문에 고생했지만, 거의 순수 함수형 접근으로 옮긴 뒤에는 코드 중복이 드물어졌음  
  그냥 함수를 만들고 두 곳에서 호출하면 됨. 주된 추상화 이슈는 자료구조인데, TypeScript 인터페이스는 본질적으로 **덕 타이핑**이라서 여기서도 문제가 많지 않음  
  그래서 추상화 문제 때문에 생기는 코드 중복은 드묾. **개발자가 사일로화**되어 생기는 코드 중복이 훨씬 흔함
  - 취미로 함수형 언어를 쓰는데, 기억해야 할 핵심은 기법이라고 봄  
    현대 언어 대부분은 함수형 프로그래밍 이론 위에 쉽게 올라설 수 있고, Haskell을 꼭 알 필요는 없음. 사람마다 머리가 다르게 작동하겠지만, 작고 단순하며 가끔은 유연한 부품들이 전체를 만든다는 생각이 나에게는 잘 맞음  
    크고 복잡하며 모든 걸 다 하는 형태변환 기계와는 반대임
  - 코드 중복을 겪는 데 개발자가 꼭 사일로화되어 있을 필요는 없음  
    팀 규모가 일정 수준을 넘어서 각자가 다른 사람이 무엇을 하는지 모두 알 수 없게 되면 **코드 중복**은 꽤 필연적임. 모두가 함수형 스타일로 작성해도 마찬가지임  
    실제로 지난달 회사에서 이런 일이 있었음. 새 순수 헬퍼 함수를 작성해 파일 앞부분에 두었는데, 일주일 뒤 동료가 실질적으로 같은 기능을 하지만 시그니처가 다른 비슷한 헬퍼 함수가 같은 파일 끝부분에 이미 있다고 알려줌
  - “함수를 두 부분에서 호출한다”는 게 정확히 무슨 뜻인지 궁금함

- 본문과 같은 맥락에서, 둘 다 겪어본 사람이라면 동의할 것임. **과소설계된 코드베이스**가 과잉설계된 코드베이스보다 훨씬 다루기 쉬움

- 유지보수해야 했던 최악의 코드는 DRY를 따르려던 코드였음. 다만 그 원칙의 원래 의도를 이해하려 하지는 않았음  
  그 난장판에서 빠져나오는 유일한 방법은 넓은 범위의 **코드 중복**을 다시 도입하는 것이었음
  - 괜찮을 테니 걱정하지 말고, 새 사용 사례를 지원하려고 재사용 함수에 모호한 **불리언 매개변수** 몇 개만 더 추가해서 배포하면 됨
  - 핵심은 “시도했다”는 데 있음. 한동안 그렇게 하다가 추상화가 틀렸기 때문에 더는 충실히 따를 수 없는 지점에 도달한 것임

- 여기서는 두 발표가 떠오름: Mike Acton의 Data-Oriented Design and C++ [1]과 Brian Cantrill의 The Complexity of Simplicity [2]  
  Mike의 발표는 코드 해법이 현실 세계를 모델링할 필요는 없고, 서로 다른 데이터는 서로 다른 문제를 만들며, 따라서 서로 다른 해법이 필요하다고 말함. 발표를 충분히 잘 옮기긴 어렵지만 나에게 큰 영향을 줬음  
  Brian의 발표는 추상화 전반과 “올바른” 추상화를 찾는 일이 얼마나 어려운지를 다룸  
  1. [https://www.youtube.com/watch?v=rX0ItVEVjHc](<https://www.youtube.com/watch?v=rX0ItVEVjHc>)  
  2. [https://www.youtube.com/watch?v=Cum5uN2634o](<https://www.youtube.com/watch?v=Cum5uN2634o>)
  - 꽤 똑똑한 엔지니어도 코드베이스의 실제 필요보다 **현실 세계 은유**를 우선할 때가 있어서 늘 이상하게 느꼈음  
    예전에 학교를 나온 지 몇 년 안 됐을 때 Rust로 연결 풀을 구현하고 있었는데, 가장 합리적인 구현은 연결 객체가 풀에 약한 참조를 들고 있다가 drop될 때 자동으로 반환되게 하는 것이었음  
    매우 경험 많은 관리자였던 내 매니저는 “도서관이 책을 들고 있지, 책이 도서관을 들고 있지는 않다”는 이유로 이 아이디어를 싫어했음. 설계를 바꿀 만큼 설득력 있는 이유라고 느끼지 않았지만, 그는 그 은유의 렌즈를 통하지 않고는 이 문제를 다루려 하지 않았음  
    결국 다른 매니저가 “도서관 책이 도서관을 포함하진 않지만, 반납처를 가리키는 도서관 이름 도장은 뒤에 찍혀 있다”고 제안하면서 교착이 풀림. 그 매니저는 이 비유 확장을 합리적이라고 본 듯함  
    내가 더 경험이 많았다면 논점을 양보하지 않으면서도 그 비유 안에서 대화하는 법을 찾았을지도 모르지만, 지금도 코드의 추상화와 라이브러리 사용 경험의 결과를 따져보는 대신 그 은유를 표준 프레임으로 고집한 건 완전히 기이했다고 느낌

- 아무도 들으려 하지 않음. 정말 아무도 안 들음. 회사의 90%에는 새 추상화를 만들 때 황홀해하는 이른바 시니어 개발자가 있음  
  **과잉설계**, 추상화, 성급한 최적화는 엔지니어링의 3대 재앙임  
  동시에 그것들이 있어서 항상 일자리가 있을 테니 기쁘기도 함
  - Kubernetes, 엔지니어 수보다 많은 마이크로서비스, 몇 바이트 오버헤드를 줄이는 복잡한 프로토콜, 전부 클라우드, 그리고 단순 함수였어도 될 수많은 클래스가 딱 그런 예임

- 비슷하게, 어떤 개발자는 인라인 문자열이나 숫자 상수가 전부 악이라고 생각하는 듯함. 한 PR에서 이런 걸 봤음  
  `HTTPS_SCHEME = 'https'`  
  `DOMAIN = 'www.example.com'`  
  `url = HTTPS_SCHEME + '://' + DOMAIN`  
  “상수를 박아 넣지 말라”는 말을 **카고 컬트**처럼 따라 하는 것 말고 이게 뭘 얻는지 모르겠음. 게다가 상수 정의는 파일 맨 위에 있었고 URL을 만드는 코드는 수백 줄 떨어져 있었음
  - 코드에서는 **가까움**을 매우 좋아함. 쓰는 곳에 최대한 가깝게 정의하는 걸 선호함. 이건 정말 거슬리는 습관임  
    정규식도 파일 맨 위에 두지 말고 쓰는 곳에 두면 됨. 언어는 똑똑해서 아마 상수라는 걸 알아낼 수 있음  
    아주 작은 함수라면 그냥 람다를 쓰면 됨. 한두 번 쓰는 한 줄짜리 함수를 아주 멀리 떨어진 곳에 만들지 않았으면 함
  - 상수를 맨 위에 두면 더 쉽게 커스터마이즈할 수 있음. 특히 이 파일이 복제될 경우엔 더 그렇다  
    테스트나 스테이징에서 https 대신 http로 바꿔야 한다면 스킴과 도메인을 분리하고 상수를 위쪽이나 별도 파일에 두는 게 말이 됨. `url`이 여러 곳에서 구성되는지 한 곳에서만 구성되는지도 중요함  
    파일 맨 위에 이름 붙은 상수를 두는 건 매우 흔한 스타일이고, 때로는 팀 코딩 표준의 일부이기도 함  
    다른 이유도 있을 수 있으니 **Chesterton’s Fence**를 떠올리는 게 좋음. 어쨌든 카고 컬트라고 단정하는 건 좋은 생각이 아님. 누군가는 인라인 리터럴을 쓰는 것도 똑같이 카고 컬트라고 말할 수 있음. 이상해 보이면 물어보면 되고, 좋은 이유가 있을 수도 있고, 아무도 신경 쓰지 않았으니 리팩터링해서 상수를 인라인으로 넣어도 좋아할 수도 있음
  - 나도 이런 일을 겪었음. Event에 이름이 있으면 거대한 모놀리스나 마이크로서비스 저장소 묶음 전체에서 바로 grep해서 해당 이벤트와 관련된 모든 파일을 찾을 수 있음  
    그걸 상수로 빼면 다시 프로젝트를 하나씩 열어 **사용처 찾기**를 해야 함

- 마이크로서비스를 쓰면 둘 다 할 수 있음
  - 농담인 건 알지만, 이상적인 세계의 마이크로서비스에서는 서비스 간 **코드 중복**이라는 개념이 없음  
    한 서비스의 유지보수자라면 다른 서비스에 있는 코드는 신경 쓸 이유가 없음. 다른 팀 코드인데 왜 신경 써야 하나? 그 팀이 존재한다는 것조차 알 필요가 없음. 큰 시스템에서는 모든 애플리케이션의 존재를 현실적으로 알 수 없는 경우도 있음
  - 잠깐만요! 더 있습니다!  
    단돈 $19.95에 단일 장애 지점 하나를 여러 개의 단일 장애 지점으로 바꿔드립니다!
  - 10번 중 9번은 마이크로서비스가 서로 심하게 의존하게 되어 **분산 모놀리스**가 됨  
    서비스 지향 아키텍처를 쓰되 그냥 모놀리스를 배포하는 편이 나음. 테스트가 더 쉽고 직렬화/역직렬화라는 추가 계층도 피할 수 있음

- 대부분의 시니어는 DRY를 맹목적으로 따르지 말아야 한다는 걸 안다고 생각함. 그래도 우리 중 많은 사람은 **중복된 코드 소스** 여러 개를 유지해야 한다는 생각을 불편해함  
  이를 다루려면 두 호출자가 공통 코드에 의존하는 단순 모델을 면밀히 살펴봐야 함. 공통 코드가 한 호출자만의 필요 때문에 바뀌어야 한다면, 그 코드는 공통에 속하지 않음  
  DRY의 잘못된 목표는 캡슐화로 해결하려는 것임. 캡슐화는 리팩터링 작업을 호출자에서 공통 코드로 옮김. 하지만 공통 코드를 갱신하는 데 따르는 영향이 호출자보다 훨씬 크기 때문에 원하는 방향이 아님  
  캡슐화를 피하면서도 DRY를 지킬 수 있음. 호출자가 인지해야 하는 여러 개의 얇은 추상화를 두는 편이 더 낫다. OOP에서는 이를 위해 **SRP**와 **IoC**를 배우고, 절차적 프로그래밍에서는 일련의 헬퍼 함수를 호출하는 식으로 자연스럽게 나타남
