이 단어는 왜 이렇게 자주, 이렇게 헐겁게 쓰이는가
코드 리뷰에 "이건 안티패턴이에요"라는 코멘트가 달리면 대화는 대개 두 갈래로 간다. 작성자가 수긍하고 고치거나, "왜요?"라고 되묻고 난 뒤 아무도 근거를 대지 못하거나.
두 번째가 드물지 않은 이유는 단어 자체에 판정 기준이 들어 있지 않기 때문이다. 실무에서 '안티패턴'은 상당 부분 "내 취향에 맞지 않는다"의 권위 있는 번역어로 쓰인다. 그런데 이 말에는 원래 꽤 엄격한 정의가 있고, 그 정의를 알고 나면 쓸 수 있는 자리가 크게 줄어든다.
이 시리즈의 1편은 그 정의를 세우는 데 쓴다. 앞으로 15편에 걸쳐 "이것은 안티패턴이다"라고 말할 텐데, 그 말이 매번 무엇을 주장하는 것인지 먼저 합의해두지 않으면 나머지 전부가 취향 목록이 된다.
정의: 해법이 두 개 있어야 안티패턴이다
용어 자체는 1995년 Andrew Koenig가 Journal of Object-Oriented Programming에 쓴 "Patterns and Antipatterns"에서 나왔다. 그의 규정은 짧다. 안티패턴은 패턴과 똑같은 구조를 갖되, 해법의 자리에 "해법처럼 보이지만 해법이 아닌 것"이 들어 있는 것이다.
이걸 실무에서 쓸 수 있는 형태로 다듬은 건 1998년 Brown, Malveau, McCormick, Mowbray의 책 AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis였다. 여기서 결정적인 규정이 하나 나온다.
안티패턴은 두 개의 해법을 갖는다. 하나는 문제를 일으키는 해법(the problematic solution)이고, 다른 하나는 리팩터링된 해법(the refactored solution)이다.
이 문장이 이 시리즈 전체의 기준선이다. 탈출 경로를 제시하지 못하면 안티패턴이라고 부를 자격이 없다. 나쁘다고만 말하고 어디로 가야 하는지 말하지 못한다면, 그건 안티패턴 지적이 아니라 불평이다.
이 기준은 생각보다 많은 걸 걸러낸다. "우리 레거시 시스템은 안티패턴 덩어리예요"는 대부분 이 조건을 통과하지 못한다. 탈출 경로가 "처음부터 다시 만들기"라면 그건 리팩터링된 해법이 아니라 포기다.
패턴과의 관계: 안티패턴은 패턴의 시체가 아니다
디자인 패턴의 계보는 건축가 Christopher Alexander에서 시작한다. 그가 A Pattern Language(1977)에서 쓴 패턴의 형식은 맥락(context) — 그 맥락에서 충돌하는 힘들(forces) — 해법(solution)이다. 핵심은 해법이 아니라 해법 앞에 놓인 맥락에 있다. 어떤 힘들이 서로 당기고 있을 때 이 해법이 균형을 만드는가.
GoF의 『디자인 패턴』(1994)이 이 형식을 소프트웨어로 가져오면서 결과(consequences) 항목을 명시적으로 덧붙였다. 이 패턴을 쓰면 무엇을 얻고 무엇을 내주는가. 맥락이 앞을 지키고 결과가 뒤를 지키는 구조다.
여기서 흔히 잘려 나가는 부분이 정확히 그 맨 앞과 맨 뒤다. 사람들은 패턴을 "해법"으로 기억한다. 싱글턴은 인스턴스를 하나만 만드는 방법, 옵저버는 변경을 통지하는 방법. 맥락과 결과는 책 페이지에 남고 머리에는 해법만 남는다.
안티패턴은 정확히 그 지점에서 자란다.
안티패턴은 대부분 맥락이 사라진 뒤에도 남아 있는 해법이다.
싱글턴 자체는 틀린 코드가 아니다. 프로세스가 하나이고, 테스트가 통합 테스트 중심이고, 인스턴스 수명이 프로세스 수명과 같던 맥락에서 싱글턴은 합리적인 해법이었다. 그 맥락이 무너진 뒤에도 — 여러 워커 프로세스, 격리가 필요한 단위 테스트, 요청 단위 수명 — 같은 해법이 남아 있을 때 비로소 안티패턴이 된다.
그래서 안티패턴의 역사는 실패의 역사가 아니라 성공이 유통기한을 넘긴 역사다. 이 시리즈를 시대 순으로 구성한 이유가 여기 있다.
코드 스멜·안티패턴·실패 사례의 구분
세 단어가 섞여 쓰이는데, 층위가 다르다.
| 코드 스멜 | 안티패턴 | 실패 사례 | |
|---|---|---|---|
| 층위 | 국소적 (함수·클래스) | 구조적 (모듈·서비스·조직) | 사건 (한 번의 장애·프로젝트) |
| 성격 | 증상 | 구조 | 결과 |
| 반복성 | 도구가 기계적으로 탐지 | 여러 팀이 독립적으로 도달 | 그 팀에서 한 번 |
| 대응 | 리팩터링 카탈로그 | 구조 변경 + 조직 변경 | 포스트모템 |
| 예 | Long Method, Magic Number | Distributed Monolith | "지난 분기 결제 장애" |
Kent Beck과 Martin Fowler가 『리팩터링』(1999)에서 정리한 코드 스멜은 증상이다. 열이 나는 것과 같다. 열은 그 자체로 병명이 아니고, 열이 있다고 반드시 치료가 필요한 것도 아니다.
안티패턴은 구조다. 그래서 함수 하나를 고쳐서는 사라지지 않고, 대개 조직의 인센티브까지 건드려야 한다. Distributed Monolith(9편)를 코드 리뷰로 고칠 수 없는 이유가 이것이다.
실패 사례는 한 번 일어난 일이다. 안티패턴으로 승격되려면 여러 조직에서 독립적으로 반복되어야 한다. 이 시리즈에서 다루는 것들은 모두 그 검증을 통과한 것만 고른다.
안티패턴 판별 체크리스트
앞으로 각 편을 읽을 때, 그리고 실무에서 누군가 "안티패턴"이라는 단어를 쓸 때 적용할 네 가지 조건이다. 넷을 다 만족해야 한다.
- 반복성 — 여러 팀이 서로 모른 채 같은 구조에 도달했는가? 한 사람의 특이한 선택은 안티패턴이 아니라 그냥 나쁜 코드다.
- 초기 매력 — 처음에는 합리적으로 보였는가? 아무도 매력을 못 느끼는 선택은 애초에 퍼지지 않는다. 매력이 없으면 안티패턴이 아니다.
- 순손실 — 시간축을 길게 놓았을 때 이득보다 손해가 큰가? 시간축이 중요하다. 대부분의 안티패턴은 첫 3개월 동안은 순이득이다.
- 검증된 대안 — 리팩터링된 해법이 존재하고, 그 해법의 대가도 알려져 있는가?
4번에 "대가도 알려져 있는가"를 붙인 건 의도적이다. 탈출 경로에 비용이 없다고 주장하는 순간, 그 주장 자체가 다음 시대의 안티패턴이 된다. 마이크로서비스가 정확히 그렇게 팔렸다.
같은 코드, 다른 판정
판정이 코드가 아니라 맥락에서 나온다는 걸 보여주는 가장 짧은 예를 하나 보자.
// cache.ts
const store = new Map<string, unknown>()
export function remember<T>(key: string, compute: () => T): T {
if (store.has(key)) return store.get(key) as T
const value = compute()
store.set(key, value)
return value
}여덟 줄짜리 모듈 수준 캐시다. 이 코드에 대한 판정은 세 가지로 갈린다.
맥락 A — 일회성 CLI 스크립트. 프로세스가 몇 초 살고 끝난다. 무효화가 필요 없고, 메모리 상한이 문제 될 일도 없고, 동시성도 없다. 이건 좋은 코드다. 여기에 TTL과 LRU와 의존성 주입을 얹으면 그게 과잉 설계다.
맥락 B — 장기 실행 Node 서버. 프로세스가 몇 주씩 산다. 이제 이 코드는 세 가지를 동시에 갖는다. 상한이 없으니 메모리 누수, 무효화가 없으니 스테일 데이터, 모듈 스코프이니 테스트 간 상태 오염. 게다가 세 문제 모두 점진적으로 나타나서, 배포 직후에는 아무 증상이 없다.
맥락 C — 서버리스 함수. 인스턴스마다 캐시가 따로 생기고 콜드 스타트마다 비워진다. 히트율을 예측할 수 없고, "대부분 맞는데 가끔 오래된 값이 나온다"는 상태가 된다. 디버깅 조건으로는 최악이다. 없는 캐시보다 나쁘다.
같은 여덟 줄이 맥락에 따라 정답, 시한폭탄, 유령이 된다. 그래서 안티패턴을 카탈로그로 외워서 코드에 적용하는 방식은 작동하지 않는다. 각 편에 "이 안티패턴이 오히려 정답인 경우" 절을 고정으로 넣는 이유다.
여기에 한 가지 역설이 붙는다. 안티패턴 목록을 외워서 기계적으로 적용하는 것 자체가 Golden Hammer라는 안티패턴이다(16편). 도구를 먼저 정하고 문제를 거기에 맞추는 구조는, 그 도구가 "안티패턴 체크리스트"일 때도 똑같이 성립한다.
이 시리즈의 지도
시대 순으로 가는 이유를 한 표로 요약하면 이렇다. 각 시대는 이전 시대의 문제를 실제로 풀었고, 그 해법이 다음 시대의 안티패턴이 됐다.
| 시대 | 실제로 해결한 문제 | 그 해법이 만든 다음 문제 |
|---|---|---|
| 모놀리식 | 배포·트랜잭션·호출의 단순함 | 경계 부재 → Big Ball of Mud |
| SOA / ESB | 이기종 시스템 통합 | 중앙 집중 → 병목과 단일 장애점 |
| MSA | 독립 배포와 팀 자율성 | 경계 오설정 → Distributed Monolith |
| 서버리스 | 운영 부담 제거 | 상태·연결·관측의 붕괴 |
- 1막(2~5편) 모놀리식 시대 — 경계가 공짜였기 때문에 경계를 긋지 않았던 시대
- 2막(6~8편) SOA·ESB 시대 — 쪼갠 조각을 다시 중앙 장치로 묶었던 시대
- 3막(9~12편) MSA 시대 — 경계를 만들었지만 어디에 그을지는 아무도 알려주지 않았던 시대
- 4막(13~15편) 서버리스 시대 — 서버를 지우면서 서버가 주던 것들도 같이 지운 시대
- 종장(16편) 왜 같은 실수가 반복되는가
각 편을 읽는 법
모든 편은 같은 여섯 단계로 간다.
- 증상 — 코드나 장애 현상에서 출발
- 왜 이렇게 됐나 — 당시의 합리성 복원
- 메커니즘 — 무너지는 인과. 가능하면 수치로
- 탈출 경로 — 리팩터링된 해법과 그 대가
- 판단 기준 — 이게 오히려 정답인 경우
- 요약과 다음 편
3번을 수치로 쓰는 걸 원칙으로 삼는다. "결합도가 높아진다"는 말은 반박할 수도 검증할 수도 없다. "호출 깊이 5단계에서 각 서비스 가용성이 99.9%면 전체 가용성은 99.5%, 월 3.6시간 다운"은 검증 가능하다. 안티패턴 논의가 취향 싸움으로 끝나지 않으려면 이 차이가 필요하다.
요약
| 항목 | 내용 |
|---|---|
| 정의 | 흔히 쓰이고, 처음엔 합리적으로 보이며, 시간축에서 순손실이고, 검증된 탈출 경로가 있는 해법 |
| 핵심 조건 | 해법이 두 개(문제적 해법 + 리팩터링된 해법). 탈출 경로 없으면 안티패턴 아님 |
| 발생 기전 | 맥락이 바뀌었는데 해법만 남음 |
| 스멜과의 차이 | 스멜은 국소적 증상, 안티패턴은 구조. 조직까지 건드려야 사라짐 |
| 판정 주체 | 코드가 아니라 코드가 놓인 맥락 |
| 주의 | 카탈로그를 기계적으로 적용하는 것 자체가 Golden Hammer |
다음 편 — 2편. God Object와 Big Ball of Mud — 경계 없는 서버
UserService가 1,800줄이 되는 과정을 커밋 단위로 재구성한다. 왜 "그냥 여기에 붙이는 게 제일 싸다"가 매번 국소 최적해이면서 전역 최악해가 되는지, 변경 비용 곡선으로 따져본다. 그리고 이 덩어리에서 이음매(seam)를 찾아내는 구체적인 방법을 다룬다.