← posts/b.log()

blog92@web:~$ cat posts/anti-patterns-architecture-and-process.md

ARCHITECTURE37 min read

디자인 패턴 전에 알아야 할 안티패턴 — 2편: 아키텍처와 프로세스

Spaghetti Code, Big Ball of Mud, Lava Flow, Golden Hammer, Premature Optimization, Reinventing the Wheel — 시스템과 개발 문화 차원의 안티패턴

시작하며

1편에서 다룬 일곱 가지 — Magic Number, God Object, Long Method, Feature Envy, Shotgun Surgery, Primitive Obsession, Cargo Cult — 는 파일 단위 또는 함수 단위에서 관찰되는 안티패턴이었다. 개별 리팩토링으로 지역적으로 고칠 수 있는 것들이다.

이번 편은 다르다. 여기서 다루는 여섯은 시스템 전체의 구조 또는 개발 문화 자체에서 나타나는 안티패턴이다. 파일 하나를 잘 쓴다고 예방되지 않고, 지역적 리팩토링으로 해결되지도 않는다. 진단과 처방의 차원이 다르다.

아키텍처 레벨 — 시스템 구조가 붕괴한 상태

  1. Spaghetti Code — 제어 흐름이 얽혀서 추적이 불가능한 상태
  2. Big Ball of Mud — 아키텍처 자체가 없는 상태
  3. Lava Flow — 죽은 코드가 굳어 화석이 된 상태

프로세스 레벨 — 개발 문화의 판단 습관이 어긋난 상태

  1. Golden Hammer — 익숙한 도구를 모든 문제에 적용
  2. Premature Optimization — 측정 없는 성능 최적화
  3. Reinventing the Wheel — 검증된 도구 대신 자체 구현

1편이 "어떤 코드가 나쁜가"에 대한 답이었다면, 2편은 "어떤 시스템과 어떤 판단이 나쁜가"에 대한 답이다. 이 여섯을 이해하면 왜 특정 팀이 반복적으로 같은 종류의 문제를 만드는지, 왜 어떤 시스템은 손대는 것 자체가 두려워지는지가 보인다.


8. Spaghetti Code

정의

제어 흐름이 통제 없이 얽혀서, 어디서 어디로 흐르는지 추적이 불가능한 상태를 말한다. 스파게티 면발이 서로 얽혀 있는 모양을 본 딴 이름이다.

원래는 goto 문의 남용으로 발생한 패턴이었지만, 현대 코드에서는 다른 형태로 나타난다. 이벤트 → 상태 변경 → 이벤트 → 상태 변경의 연쇄, 서로를 트리거하는 useEffect 체인, 전역 상태에 대한 무제한 접근이 모두 스파게티의 현대적 얼굴이다.

냄새나는 코드

React에서 자주 보는 패턴이다.

tsx
function ProductPage() {
  const [product, setProduct] = useState<Product | null>(null);
  const [reviews, setReviews] = useState<Review[]>([]);
  const [relatedProducts, setRelatedProducts] = useState<Product[]>([]);
  const [averageRating, setAverageRating] = useState(0);
  const [isEligibleForDiscount, setIsEligibleForDiscount] = useState(false);
  const [displayPrice, setDisplayPrice] = useState(0);
 
  // useEffect 1
  useEffect(() => {
    fetchProduct(productId).then(setProduct);
  }, [productId]);
 
  // useEffect 2 — product가 바뀌면 리뷰를 다시 가져옴
  useEffect(() => {
    if (product) fetchReviews(product.id).then(setReviews);
  }, [product]);
 
  // useEffect 3 — 리뷰가 바뀌면 평균 평점 계산
  useEffect(() => {
    if (reviews.length > 0) {
      const avg = reviews.reduce((s, r) => s + r.rating, 0) / reviews.length;
      setAverageRating(avg);
    }
  }, [reviews]);
 
  // useEffect 4 — 평점이 특정 값 이상이면 할인 자격 부여
  useEffect(() => {
    if (averageRating >= 4.5) setIsEligibleForDiscount(true);
  }, [averageRating]);
 
  // useEffect 5 — 할인 자격이 바뀌면 표시 가격 재계산
  useEffect(() => {
    if (product) {
      setDisplayPrice(
        isEligibleForDiscount ? product.price * 0.9 : product.price
      );
    }
  }, [isEligibleForDiscount, product]);
 
  // useEffect 6 — product가 바뀌면 관련 상품
  useEffect(() => {
    if (product) fetchRelated(product.category).then(setRelatedProducts);
  }, [product]);
 
  // ...
}

이 코드에서 productId가 바뀌면 무슨 일이 일어나는가?

  1. useEffect 1 실행 → product 변경
  2. product 변경이 useEffect 2, 5, 6을 트리거
  3. useEffect 2 실행 → reviews 변경
  4. reviews 변경이 useEffect 3을 트리거
  5. useEffect 3 실행 → averageRating 변경
  6. averageRating 변경이 useEffect 4를 트리거
  7. useEffect 4 실행 → isEligibleForDiscount 변경
  8. isEligibleForDiscount 변경이 useEffect 5를 다시 트리거
  9. useEffect 5 실행 → displayPrice 변경

단 하나의 프롭 변경이 9번의 리렌더링과 5번의 상태 업데이트 체인을 유발한다. 그리고 각 useEffect가 서로를 트리거하기 때문에, 중간에 어떤 단계에서 조건이 다르게 평가되면 최종 상태가 예측하기 어려운 방식으로 달라진다.

여기서 새 기능을 추가한다고 하자. "리뷰 평점이 낮으면 관련 상품 표시 방식이 바뀌어야 한다." 이 조건을 어디에 넣어야 하는가? useEffect 3? 6? 새 useEffect? 추가한 코드가 기존 흐름의 어떤 부분을 깰지 예측이 안 된다.

왜 안티패턴인가

네 가지 문제가 겹친다.

1. 추적 불가. 어떤 상태 변경이 어디서 촉발되는지 파악이 어렵다. 디버깅 시 "이 값이 왜 여기서 바뀌지?"가 반복된다.

2. 테스트 불가. 단위 테스트가 실질적으로 불가능하다. 한 요소를 테스트하려고 해도 다른 요소의 상태를 다 설정해야 한다.

3. 확장 시 부작용 예측 불가. 새 로직을 추가할 때 기존 흐름의 어디를 깰지 알 수 없다. 리팩토링이 두려워진다.

4. 성능 저하. 위 예시처럼 한 번의 변경이 여러 번의 리렌더를 유발한다. 실제 사용자가 체감하는 느림으로 이어진다.

리팩토링: 흐름의 방향성 확보

핵심 원칙은 단방향 데이터 흐름이다. 상태 A가 상태 B를 트리거하고 B가 C를 트리거하는 연쇄를 끊는다.

위 코드는 이렇게 재구성된다.

tsx
function ProductPage() {
  // 1. 서버 데이터는 한 번에 병렬로
  const { data: product } = useQuery(["product", productId], () => fetchProduct(productId));
  const { data: reviews } = useQuery(
    ["reviews", productId],
    () => fetchReviews(productId),
    { enabled: !!product }
  );
  const { data: relatedProducts } = useQuery(
    ["related", product?.category],
    () => fetchRelated(product!.category),
    { enabled: !!product }
  );
 
  // 2. 파생 값은 useEffect가 아니라 계산으로
  const averageRating = useMemo(() => {
    if (!reviews?.length) return 0;
    return reviews.reduce((s, r) => s + r.rating, 0) / reviews.length;
  }, [reviews]);
 
  const isEligibleForDiscount = averageRating >= 4.5;
  const displayPrice = product
    ? (isEligibleForDiscount ? product.price * 0.9 : product.price)
    : 0;
 
  // 이제 useEffect가 하나도 없다
}

핵심 변화 두 가지.

변화 1: 파생 상태(derived state)는 상태가 아니라 계산이다. averageRating, isEligibleForDiscount, displayPrice는 모두 다른 값에서 파생된다. 이건 별도 state가 아니라 매 렌더링마다 계산되는 값이어야 한다. useEffect + useState 조합으로 파생 값을 관리하는 것 자체가 안티패턴이다.

변화 2: 서버 상태는 서버 상태 라이브러리에. React Query, SWR 같은 도구가 캐싱, 로딩 상태, 의존성 관리를 해결한다. useEffect + fetch + useState 패턴을 손으로 짜지 않는다.

리렌더링 체인이 사라지고, 데이터 흐름이 위에서 아래로만 흐른다. 새 기능을 추가할 때 어디에 넣을지 명확하다.

더 넓은 리팩토링 도구

useEffect 지옥은 스파게티의 한 형태일 뿐이다. 다른 상황에는 다른 도구가 필요하다.

상태 머신(State Machine). 상태가 여러 개이고 전이 규칙이 복잡하면 명시적 상태 머신을 도입한다. XState 같은 도구는 "어떤 상태에서 어떤 이벤트가 올 때 어디로 가는가"를 명시화한다. 스파게티의 근본 원인인 "숨겨진 전이"가 사라진다.

이벤트 버스의 계층화. 서로 다른 계층의 이벤트를 섞지 않는다. UI 이벤트(클릭)와 도메인 이벤트(주문 완료)와 인프라 이벤트(WebSocket 재연결)를 같은 버스에 태우면 흐름이 얽힌다. 계층별로 분리하고, 하위 계층이 상위 계층에 이벤트를 보낼 때만 명시적 변환을 거친다.

단방향 아키텍처. Flux, Redux, Elm 계열의 아키텍처가 스파게티를 해결하기 위해 나왔다. 상태 변경은 항상 명시적 액션을 통해서만, 액션은 한 방향으로만 흐른다는 규칙이 강제된다.

함정 — 모든 상태를 명시화하려는 오버 엔지니어링

여기서도 반대 극단이 있다. 작은 UI 컴포넌트의 3개 상태에 상태 머신을 도입하는 것은 오버 엔지니어링이다. XState의 학습 비용, 보일러플레이트, 팀 공유 부담이 얻는 이득보다 크다.

판단 기준: "이 상태 전이의 규칙을 새 팀원에게 설명할 수 있는가?"

  • 3분 안에 설명 가능하면 그냥 useState로 충분하다.
  • 화이트보드에 그림이 필요하면 useReducer로 정리할 때가 됐다.
  • 그림이 복잡해서 여러 번 다시 그려야 하면 상태 머신 도입을 고려한다.

요약

항목내용
증상제어 흐름이 얽혀 있어 추적/디버깅 불가
현대적 형태useEffect 체인, 이벤트 간 트리거 연쇄, 전역 상태 남용
원인단방향 흐름 부재, 파생 상태를 상태로 취급
해법파생 값은 계산으로, 서버 상태는 라이브러리로, 필요하면 상태 머신
함정작은 UI에도 상태 머신을 우겨넣는 오버 엔지니어링
판단 기준상태 전이 규칙을 설명하는 데 필요한 도구의 무게

9. Big Ball of Mud

정의

아키텍처 자체가 부재한 상태. 시스템에 명확한 경계나 계층이 없고, 모든 것이 모든 것에 의존한다. 새 기능을 어디에 넣어야 할지 결정할 근거가 없고, 한 곳의 변경이 어디로 파급될지 예측할 수 없다.

이름은 Brian Foote와 Joseph Yoder의 1997년 논문에서 왔다. 그들은 이것을 "가장 흔한 소프트웨어 아키텍처"라고 지적했다. 실제 세계에서 대부분의 시스템은 정도 차이가 있을 뿐 이 상태에 가깝다.

Big Ball of Mud를 앞선 안티패턴들과의 관계로 보면 이해가 쉽다.

  • God Object: 하나의 클래스가 신
  • Big Ball of Mud: 시스템 전체가 신

시스템 전체가 하나의 거대한 God Object처럼 동작한다. 어떤 부분도 다른 부분으로부터 독립되어 있지 않다.

냄새나는 상황

코드 예시로 보여주기 어려운 안티패턴이다. 파일 하나가 아니라 전체의 상태이기 때문이다. 대신 이런 신호로 나타난다.

신호 1: 모듈 경계가 없다. frontend/, backend/, database/ 같은 최상위 폴더는 있지만, 그 안은 온갖 파일이 계층 없이 나열되어 있다. 도메인 개념별 폴더가 없거나, 있어도 그 폴더끼리 자유롭게 서로를 import한다.

신호 2: 순환 의존이 흔하다. A가 B를 import하고, B가 C를 import하고, C가 다시 A를 import하는 상태. 어느 것이 상위이고 어느 것이 하위인지 정의되지 않았다.

신호 3: 어디에 넣을지 매번 고민. 새 기능을 만들 때 "이 함수를 어디에 두지?"의 답이 매번 다르고, 결국 "그냥 여기가 편하겠다"로 정해진다.

신호 4: 조사 없이는 변경 불가. 한 함수를 수정하려면 그것을 호출하는 모든 곳을 grep하고, 각 호출자의 컨텍스트를 다 파악해야 한다. 안전한 수정 범위가 정의되어 있지 않다.

신호 5: "여기 만지면 저기 터진다". 특정 부분을 수정하면 관련 없어 보이는 다른 곳에서 버그가 발생한다. 팀에 "이 코드는 위험해서 절대 안 건드림"이라는 암묵적 규칙이 생긴다.

신호 6: 기술 스택의 혼재. 같은 문제를 푸는 서로 다른 방식이 시스템에 공존한다. 한 곳에서는 Redux, 다른 곳에서는 Context API, 또 다른 곳에서는 직접 만든 스토어. 시간대별 유행이 지층처럼 쌓여 있다.

왜 안티패턴인가

1. 변경 비용의 조합적 폭발. 모든 것이 모든 것에 연결되어 있으므로, 한 곳의 변경이 잠재적으로 시스템 전체에 영향을 미친다. 안전한 변경 범위가 없다.

2. 신규 팀원의 학습 불가. 시스템을 이해하는 유일한 방법은 "코드 전체를 읽고 감을 잡는 것"이다. 이는 코드가 몇 만 줄만 되어도 실질적으로 불가능해진다.

3. 팀 확장의 어려움. 사람 수가 늘어도 병렬로 작업할 수 있는 영역이 없다. 항상 서로의 변경이 충돌하고, 릴리즈마다 회의가 필요하다.

4. 자체 강화된다. 모든 새 코드가 기존 진흙에 더 얹혀서 진흙을 더 크게 만든다. 시간이 흐를수록 나빠진다.

왜 이런 시스템이 만들어지는가

Big Ball of Mud를 이해하려면 왜 생기는지를 알아야 한다. 무능이 아니라 합리적 압력의 누적 결과다.

압력 1: 시장 진출 속도. 초기 스타트업은 아키텍처를 세울 시간이 없다. "일단 돌아가는 것"이 우선이고, 이는 대개 옳은 판단이다.

압력 2: 요구사항의 급변. 초기에 세운 아키텍처가 3개월 뒤 요구사항과 맞지 않는 것은 흔한 일이다. 다시 세울 시간은 없다.

압력 3: 팀 구성원의 순환. 초기 개발자가 떠나면 그의 설계 의도가 함께 떠난다. 남은 사람은 그 구조를 이해하지 못한 채 새 기능을 얹는다.

압력 4: "돈 되는 것 우선". 리팩토링은 매출을 만들지 않고, 새 기능은 매출을 만든다. 리팩토링은 항상 나중으로 밀린다.

압력 5: 성공의 저주. 시스템이 잘 돌아가서 사용자가 늘고 요구사항이 늘어난다. 원래 감당하도록 설계되지 않은 규모까지 도달한 뒤에 아키텍처 부담이 인식된다.

이 압력들을 이해하지 못하면 Big Ball of Mud를 만난 팀에게 그저 "왜 이렇게 짰냐"고 비난하게 된다. 그 비난은 부당하다. 지금 그 시스템이 존재하고 사용자에게 가치를 주고 있다는 사실 자체가 그 시스템의 정당성을 입증한다. 문제는 다음 단계로 가는 방법이다.

리팩토링: 전면 재작성이 아니라 점진적 침식

Big Ball of Mud를 만난 팀이 가장 흔히 저지르는 실수는 전면 재작성(rewrite) 결정이다. 결과는 거의 항상 실패다. 이유는 세 가지다.

  1. 재작성 중에도 기존 시스템은 계속 변한다. 재작성이 끝날 무렵 원본은 이미 다른 시스템이 되어 있다.
  2. 기존 시스템에는 명시되지 않은 비즈니스 규칙이 수백 개 숨어 있다. "이 조건에서는 이렇게 처리한다"가 코드에는 있지만 문서에는 없다.
  3. 재작성 기간 동안 새 기능 개발이 멈추거나 둘 다 진행하며 팀이 과로한다.

대안은 Strangler Fig 패턴이다. 이름은 무화과나무의 기생 종에서 왔다. 기존 나무를 감싸며 자라 점차 원래 나무를 대체하는 방식이다.

기본 흐름:

  1. 경계를 정의한다. 시스템에서 논리적으로 분리 가능한 첫 번째 조각을 찾는다. 예: "사용자 인증"이라는 도메인.
  2. 인터페이스를 추출한다. 그 영역에 대한 접근을 하나의 API로 통일한다. 다른 모든 코드가 이 API를 통해서만 인증 관련 기능을 호출한다.
  3. 뒤에서 재구현한다. API 뒤의 구현을 새 구조로 다시 만든다. 겉으로 보이는 API는 그대로다.
  4. 스위칭한다. 준비되면 API 구현을 새것으로 바꾼다. 문제가 있으면 되돌린다.
  5. 다음 조각으로. 다음 도메인에 대해 반복한다.

몇 년이 걸릴 수 있는 작업이지만, 매 순간 시스템은 동작한다. 새 기능 개발도 병렬로 진행된다.

동시에 시행하는 원칙 몇 가지가 있다.

  • 새 코드는 이미 정의된 경계 안에서만 짠다. 진흙에 진흙을 얹지 않는다.
  • 경계를 강제할 도구를 도입한다. 순환 의존 감지, 모듈 간 import 제한 규칙 같은 정적 분석.
  • 테스트 커버리지를 먼저 확보한다. 리팩토링을 시작하려면 안전망이 필요하다. 특히 통합 테스트.

함정 — 완벽한 아키텍처의 추구

Big Ball of Mud를 배운 팀이 다음으로 빠지는 함정은 완벽한 아키텍처를 처음부터 세우려는 시도다. 도메인 주도 설계, 헥사고날 아키텍처, 클린 아키텍처를 3인 스타트업에 처음부터 적용하려 든다.

결과는 두 가지다. 첫째, 오버 엔지니어링으로 초기 속도가 죽는다. 둘째, 팀이 그 구조를 유지할 지식이 없으면 반년 뒤 다른 종류의 Big Ball of Mud가 된다 — 이번에는 "구조는 있지만 아무도 지키지 않는" 형태로.

시스템의 아키텍처 성숙도는 시스템의 크기, 팀의 규모, 도메인 복잡도에 맞춰서 자란다. 3인 팀은 3인 팀에 맞는 아키텍처를 갖고, 30인 팀은 30인 팀에 맞는 아키텍처를 갖는다. 필요보다 앞선 아키텍처는 그 자체가 짐이다.

요약

항목내용
증상아키텍처 부재, 모든 것이 모든 것에 의존
신호순환 의존 / 어디에 넣을지 매번 고민 / 여기 만지면 저기 터짐
원인시장 압력, 요구사항 변화, 팀 순환의 누적 (무능이 아님)
해법Strangler Fig 패턴으로 점진적 대체
안 되는 것전면 재작성
함정완벽한 아키텍처의 성급한 도입
판단 기준시스템/팀/도메인 규모에 맞는 성숙도

10. Lava Flow

정의

한 번 프로덕션에 들어간 코드가 아무도 지우지 못한 채 화석처럼 남는 상태를 말한다. 이름은 화산에서 흘러나온 용암(lava)이 굳어 그 자리에 지형이 되는 이미지에서 왔다.

Big Ball of Mud와의 차이: Big Ball of Mud는 활성 코드의 얽힘이고, Lava Flow는 죽은 코드의 축적이다. 이 둘은 함께 나타나는 경우가 많지만 별개의 병이다.

냄새나는 상황

패턴 1: "옛날 API"의 잔존. 새 API v2가 만들어졌지만 v1도 여전히 살아있다. v1을 쓰는 클라이언트가 있을지도 모르니 지울 수 없다. 2년 뒤에도 여전히 살아있다.

패턴 2: 실험 코드의 프로덕션 진입. A/B 테스트를 위한 코드, 프로토타입, "일단 이렇게 해보자" 코드가 프로덕션에 들어갔다가 검증 후에도 정리되지 않는다.

패턴 3: 주석 처리된 큰 블록.

typescript
function processOrder(order: Order) {
  // TODO: 예전 방식. 새 로직으로 대체 예정이지만 혹시 몰라 남겨둠 (2023-03)
  // if (order.legacyType === "type_A") {
  //   return handleLegacyTypeA(order);
  // }
  // if (order.legacyType === "type_B") {
  //   return handleLegacyTypeB(order);
  // }
  // ... 200줄 더 ...
 
  return newOrderPipeline(order);
}

3년이 지나도록 그대로 남아있다. 새 팀원은 "이거 왜 있죠?"를 물어보고, 아무도 대답하지 못한다.

패턴 4: "레거시니까 건드리지 마세요" 표시.

typescript
/**
 * @deprecated 사용하지 마세요. 대신 newProcessor 사용.
 */
export class LegacyProcessor {
  // 그런데 20곳에서 아직 이걸 쓰고 있음
}

"deprecated"라고 표시되어 있지만 실제로는 여전히 여러 곳에서 사용된다. 완전 제거를 하려면 20곳의 호출을 다 정리해야 하고, 그 20곳은 각자 이유가 있어서 아직 안 옮겨졌다.

패턴 5: 미완성 리팩토링의 흔적. 누군가 중간까지 리팩토링을 시도하다가 시간이 없어 멈췄다. 새 구조와 옛 구조가 동시에 존재하고, 어느 쪽이 정답인지 팀 아무도 모른다.

왜 안티패턴인가

1. 인지 부담 증가. 코드를 이해하는 사람이 매번 "이거 살아있는 코드인가?"를 판단해야 한다. 죽은 코드는 살아있는 코드의 이해를 방해한다.

2. 불필요한 유지보수. 죽은 코드가 컴파일 에러를 내면 고쳐야 한다. 의존성 업데이트할 때 죽은 코드도 함께 업데이트해야 한다. 실제로 실행되지 않는 코드에 시간이 든다.

3. 검색의 오염. grep으로 뭔가를 찾을 때 죽은 코드가 결과에 섞여 나온다. 참조 횟수 계산이 왜곡된다.

4. 새 팀원의 혼란. 신입 개발자가 코드베이스를 이해하려 할 때 죽은 코드를 따라간다. "이 함수 왜 있어요?"의 답을 받지 못한다.

5. 보안 위험. 죽은 코드가 오래된 취약한 라이브러리를 의존하고 있으면, 보안 스캐너가 경고를 낸다. 실제로는 실행되지 않는데도 대응해야 한다.

왜 생기는가

Lava Flow는 두 가지 심리에서 자란다.

심리 1: 상실 회피. "혹시 나중에 쓸 수도 있으니 남겨두자." 실제로 나중에 쓰이는 경우는 거의 없다. 필요하면 git 히스토리에서 복원할 수 있다는 사실은 자주 잊힌다.

심리 2: 책임 회피. "내가 지웠다가 뭐 잘못될까 무섭다." 누구도 자기 손으로 지우고 싶어하지 않는다. 지워서 문제가 생기면 자기 책임이지만, 남겨둬서 코드베이스가 무거워지는 것은 아무의 책임도 아니다.

이 심리들이 누적되면 시스템은 점점 무거워진다. 5년 된 시스템이 실제 로직의 두 배 크기의 죽은 코드를 갖고 있는 것은 흔한 일이다.

리팩토링: 죽은 코드 감별과 삭제

먼저 발견해야 한다. 죽은 코드 발견에는 여러 도구가 있다.

정적 분석. TypeScript의 noUnusedLocals, ESLint의 no-unused-vars, ts-prune, knip 같은 도구가 사용되지 않는 export를 찾아준다. 완벽하지 않지만 시작점이 된다.

동적 관찰. 프로덕션에서 실제로 실행되는 함수를 로깅으로 추적한다. 코드 커버리지 도구를 프로덕션에 적용하는 방식도 있다. 6개월 동안 한 번도 호출되지 않은 함수는 삭제 후보다.

의존성 그래프. import 관계를 추적해서 아무도 참조하지 않는 파일을 찾는다. 단, 동적 import나 문자열 기반 참조는 놓칠 수 있다.

발견 후에는 삭제다. 백업이 필요하다는 걱정은 git이 해결해준다. 진짜로 필요할 때는 git log에서 복원하면 된다.

주석 처리된 코드 블록에 대한 규칙은 단순하다.

주석 처리된 코드는 지운다. 예외는 없다.

만약 그 코드가 나중에 필요할 수도 있다면, git 히스토리에 남아있으므로 안전하다. 주석으로 남겨둔 코드는 시간이 지나면 왜 남겨졌는지도 아무도 모르게 된다.

조직적 방어선

Lava Flow는 개인의 노력만으로 해결되지 않는다. 조직적 규칙이 필요하다.

규칙 1: Deprecated에는 삭제 데드라인이 붙는다. @deprecated 표시만으로 끝나지 않고, "2026-06-01까지 제거"라는 명확한 날짜와 담당자가 붙는다. 그날이 오면 실제로 삭제한다.

규칙 2: 실험 코드의 만료 정책. A/B 테스트나 기능 플래그에는 만료일이 있다. 6개월 넘게 활성/비활성 상태가 유지되면 자동으로 알림이 가고, 결정을 강제한다.

규칙 3: 죽은 코드 정리 시간의 확보. 매 스프린트에 몇 %의 시간을 코드베이스 정리에 배정한다. "여유 있을 때 하자"는 영원히 하지 않겠다는 뜻이다.

규칙 4: 삭제를 축하한다. 삭제 PR이 추가 PR만큼 존중받는 문화. 지운 라인 수를 성과 지표에 포함하는 팀도 있다.

함정 — 살아있는 코드를 죽은 것으로 오인

죽은 코드 삭제에도 함정이 있다. 정적 분석이 발견하지 못하는 참조가 있다.

  • 문자열 기반 동적 import: import(`./handlers/${type}`)
  • 리플렉션: 이름으로 함수를 조회하는 코드
  • 외부 시스템 통합: 다른 서비스가 API로 호출하는 함수
  • 테스트에서만 사용: 프로덕션 코드는 없지만 테스트 픽스처가 참조
  • 설정 파일 기반: JSON/YAML 설정이 클래스 이름을 참조

삭제 전에는 다음을 확인한다.

  1. 코드베이스 전체 grep (문자열 매칭까지)
  2. CI 통과
  3. 스테이징 환경 배포 후 관찰 기간
  4. 프로덕션 배포 후 로그 모니터링

특히 프로덕션에 영향이 클 가능성이 있는 삭제는 먼저 로깅부터 추가하고, 일정 기간 관찰한 뒤 삭제하는 접근이 안전하다. "이 함수가 호출되었습니다" 로그를 3개월 붙여서 아무도 부르지 않는 것을 확인한 뒤 지운다.

요약

항목내용
증상죽은 코드가 지워지지 않고 축적
형태옛 API 잔존 / 주석 처리 블록 / deprecated 다수 참조 / 실험 코드 잔존
원인상실 회피 심리 + 책임 회피 심리
해법정적/동적 분석으로 발견 → 삭제 (git이 백업)
조직적 방어Deprecated 데드라인, 실험 코드 만료 정책, 정리 시간 확보
함정동적 참조를 놓치고 살아있는 코드 삭제
안전 절차로깅 추가 → 관찰 기간 → 삭제

11. Golden Hammer

정의

자기가 잘 아는 도구/기술/패턴을 모든 문제에 적용하려는 안티패턴. "망치를 쥔 사람에게는 모든 것이 못으로 보인다"는 격언(Maslow's Hammer)에서 이름을 딴 것이다.

Cargo Cult와의 결정적 차이:

  • Cargo Cult: 이유를 모르고 남을 따라함
  • Golden Hammer: 이유를 알지만 이 상황에 안 맞는 것도 계속 씀

Cargo Cult가 무지의 문제라면, Golden Hammer는 편향의 문제다. Golden Hammer는 자기가 검증된 지식을 가지고 있다는 확신에서 나오기 때문에 방어하기가 더 어렵다.

냄새나는 상황

패턴 1: 모든 것을 자기 도구로.

  • React를 잘 아는 개발자가 Landing Page에도 React SSR을 도입 (정적 HTML로 충분한 경우에도)
  • 함수형 프로그래밍 광이 모든 데이터 흐름을 monad 체인으로 (팀원이 못 읽음)
  • 마이크로서비스 경험자가 3인 스타트업에도 마이크로서비스 (배포 지옥이 되어 돌아옴)
  • SQL 잘 아는 개발자가 관리 화면 상태까지 DB에 저장 (localStorage로 충분한데)

패턴 2: 문제를 도구에 맞춤.

정상적인 흐름은 "문제 → 도구 선택"이다. Golden Hammer는 "도구 → 문제 재해석"이다. 실제 문제가 도구에 맞지 않으면 문제 정의를 왜곡한다.

예: 팀이 GraphQL을 도입하기로 결정한 뒤, REST가 더 적합한 배치 export 엔드포인트도 억지로 GraphQL로 만든다. 결과는 GraphQL이 잘 못 하는 것을 GraphQL로 하는 어색한 구조.

패턴 3: "이걸로 다 됩니다"의 반복.

기술 선택 회의에서 항상 같은 사람이 항상 같은 도구를 제안한다. 문제의 성격이 매번 달라도 답은 같다. 그 도구 자체의 한계에 대한 언급은 없다.

패턴 4: 팀 확산을 통한 전염.

리드 개발자의 Golden Hammer는 팀 전체의 표준이 된다. 신입은 이 도구가 "우리 팀 방식"이라고 학습하고, 자기도 모든 곳에 그것을 쓰게 된다. Cargo Cult가 여기에 결합되어 재생산된다.

왜 안티패턴인가

1. 문제와 도구의 어긋남. 모든 도구는 특정 문제에 최적화되어 있다. 어긋난 문제에 사용하면 원래 도구를 쓸 때 얻을 이득을 잃고 원래 문제도 잘 풀지 못한다.

2. 팀의 도구 다양성 상실. 한 도구만 계속 쓰면 팀은 다른 도구를 배우지 못한다. 5년 뒤 그 도구가 시대에 뒤처져도 대안을 평가할 능력이 없다.

3. 신입 학습의 왜곡. 신입은 "이 도구가 이 문제에 왜 적합한가"를 배우지 못하고 "이 도구를 어떻게 쓰는가"만 배운다. 도구 선택 능력이 자라지 않는다.

4. 방어가 어렵다. 그 도구가 실제로 좋은 도구이고 쓰는 사람이 그것을 잘 안다는 사실이 반론을 어렵게 만든다. "그건 저 상황에는 안 맞아요"라고 말해도 "제가 이걸로 다 해봤어요"라는 응답을 받는다.

심리적 뿌리

Golden Hammer는 세 가지 심리에서 자란다.

심리 1: 학습 투자의 회수. 어떤 도구를 배우는 데 큰 시간을 투자했다면, 그 투자를 정당화하고 싶다. 다른 도구를 배우면 원래 투자가 낮아 보인다.

심리 2: 능력의 확인. 자기가 잘하는 것을 쓸 때 유능감을 느낀다. 새 도구를 쓰면 초보로 돌아간다. 무의식적으로 이를 회피한다.

심리 3: 예측 가능성. 익숙한 도구는 결과가 예측 가능하다. 새 도구는 예측하지 못한 문제가 생길 수 있다. 리스크 회피 성향이 Golden Hammer로 이어진다.

이 심리들은 다 정상적이다. 그래서 Golden Hammer가 흔하다. 문제는 이 심리를 인식하지 못한 채 매번 "이번에도 이게 답이야"라는 결론을 내리는 것이다.

리팩토링: 결정 프레임의 도입

Golden Hammer는 사고 습관이므로, 치료도 사고 습관을 바꾸는 것이다.

방어선 1: 대안을 강제로 나열.

새 기술을 선택할 때, 항상 최소 세 가지 대안을 나열하고 각각의 장단점을 문서화한다. 후보가 자기 손에 잡히는 도구 하나뿐이면 그 자체가 Golden Hammer의 신호다.

방어선 2: 각 도구의 안 맞는 경우를 명시.

어떤 도구든 안 맞는 경우가 있다. "이 도구가 이 문제에 안 맞는 경우는 어떤 경우인가?"에 답하지 못하면 그 도구를 이해한 것이 아니다.

예: React를 안다면 "React가 부적합한 경우는? → SEO가 중요하고 상호작용이 거의 없는 페이지, 극도로 단순한 UI, 번들 크기가 결정적으로 중요한 상황" 이런 답이 나와야 한다.

방어선 3: 다른 사람에게 결정 위임.

특정 사람이 특정 도구의 지지자라면, 그 도구가 관련된 결정은 다른 사람이 초안을 잡게 한다. 이해충돌 회피의 원칙을 코드 결정에도 적용한다.

방어선 4: 팀 도구의 순환.

프로젝트마다 다른 도구를 의도적으로 시도한다. 학습 비용이 있지만 팀의 도구 다양성을 유지하는 투자다. 단, 이것이 다음에 나올 "Reinventing the Wheel"과 겹치지 않도록 균형이 필요하다.

Cargo Cult와의 결합 위험

Golden Hammer와 Cargo Cult는 종종 결합해서 나타난다.

  • 시니어의 Golden Hammer(익숙해서 항상 쓰는 도구)가
  • 팀의 Cargo Cult(이유를 모르고 표준으로 학습)로 번지고
  • 새 프로젝트에도 무비판적으로 적용되며
  • 대안을 검토할 능력이 사라진 채 자기 강화된다

이 결합을 인식하는 것 자체가 방어의 시작이다. "우리 팀은 왜 항상 X를 쓰지?"라는 질문이 정상화되어야 한다.

함정 — 새로움 자체의 추구

Golden Hammer를 피하려다 반대 극단에 빠질 수 있다. 새로운 도구 자체를 목적으로 삼는 것이다. 지금 잘 돌아가는 도구 대신 최신 트렌드를 도입한다. 이건 Cargo Cult의 한 형태다.

균형점은 이렇다: 도구 선택은 도구가 아니라 문제에서 시작한다. 문제가 명확할 때 그 문제에 가장 적합한 도구를 검토하고 선택한다. "익숙해서" 선택하지도 않고 "최신이라서" 선택하지도 않는다.

요약

항목내용
증상익숙한 도구를 모든 문제에 적용
Cargo Cult와의 차이이유를 알지만 편향으로 선택
원인학습 투자 회수 심리, 유능감 유지, 예측 가능성 선호
해법대안 강제 나열, 안 맞는 경우 명시, 결정권 분산
Cargo Cult와의 결합시니어의 Hammer가 팀의 Cult로 번짐
함정반대로 새로움 자체를 추구
판단 기준도구가 아니라 문제에서 시작

12. Premature Optimization

정의

실제 성능 문제가 확인되지 않은 상태에서 성능 최적화를 시도하는 것. Donald Knuth의 1974년 논문에서 나온 유명한 인용이 이 안티패턴의 이름을 만들었다.

"Premature optimization is the root of all evil."

주의할 점은 이 인용이 자주 잘못 인용된다는 것이다. Knuth의 원문은 다음과 같다.

"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%."

즉 "측정 없이 미리 최적화하지 마라. 단, 진짜 병목인 3%는 놓치지 마라"가 원래 의미다. "성능을 신경 쓰지 마라"가 아니다.

냄새나는 코드

패턴 1: 명확성을 희생한 미시 최적화.

typescript
// 가독성 있는 원본
function findActiveUsers(users: User[]): User[] {
  return users.filter(u => u.status === "active" && u.lastLogin > lastMonth);
}
 
// "최적화된" 버전
function findActiveUsers(users: User[]): User[] {
  const result: User[] = [];
  const ts = lastMonth.getTime();
  for (let i = 0, len = users.length; i < len; i++) {
    const u = users[i];
    if (u.status === "active" && u.lastLogin.getTime() > ts) {
      result.push(u);
    }
  }
  return result;
}

이 최적화가 실제로 의미 있으려면 users가 매우 크거나 이 함수가 매우 자주 호출되어야 한다. 그런 정보 없이 미리 이렇게 짜면 가독성만 잃는다.

패턴 2: 프로파일링 없는 캐싱.

typescript
const cache = new Map<string, User>();
 
async function getUser(id: string): Promise<User> {
  if (cache.has(id)) return cache.get(id)!;
  const user = await db.query.users.findFirst({ where: eq(users.id, id) });
  cache.set(id, user!);
  return user!;
}

이 캐시가 정당화되려면 다음을 알아야 한다.

  • 같은 사용자가 얼마나 자주 조회되는가?
  • DB 쿼리의 실제 지연은? 캐시 관리 오버헤드보다 큰가?
  • 캐시 무효화 전략은? 사용자 정보가 바뀌면 어떻게 되는가?
  • 메모리 사용량 한계는? LRU가 필요한가?

이런 것 없이 "캐시가 있으면 좋겠지"로 붙이면 종종 오래된 데이터를 보여주는 버그의 원인이 된다. 성능 이득은 없거나 미미하고, 정확성 위험은 실제로 증가한다.

패턴 3: 아직 쓰이지 않는 최적화 인프라.

Redis를 도입하지만 실제로는 캐시 히트가 5%도 안 되는 상황. CDN을 세팅하지만 실제로 캐시되는 자산이 거의 없는 상황. 로드 밸런서를 도입하지만 부하가 그 임계점 근처도 아닌 상황.

패턴 4: 인덱스 남발.

DB의 모든 컬럼에 인덱스를 붙인다. 쿼리 성능이 오를 것 같지만, 실제로는:

  • 쓰기 성능이 크게 저하됨
  • 디스크 사용량 폭증
  • 옵티마이저가 잘못된 인덱스를 선택할 위험 증가
  • 사용되지 않는 인덱스가 유지보수 부담이 됨

프로파일링 없이 인덱스를 붙이는 것은 최적화가 아니다.

왜 안티패턴인가

1. 실제 병목이 아닌 곳에 시간을 씀. 프로그램의 성능은 파레토 법칙을 따른다. 시간의 대부분이 코드의 소수 영역에서 소비된다. 그 3%가 아닌 97%를 최적화하면 노력 대비 이득이 거의 없다.

2. 가독성 희생. 최적화된 코드는 대개 알아보기 어렵다. 명확성을 잃은 대신 얻는 게 없으면 순손실이다.

3. 최적화 자체가 버그의 원인. 캐시 일관성, 인덱스 튜닝, 병렬성은 새로운 버그를 만드는 대표적 원천이다. 필요 없는 최적화는 필요 없는 버그를 초래한다.

4. 변경 유연성 상실. 특정 성능 특성에 맞춰 최적화한 코드는 요구사항이 바뀔 때 전면 재작성이 필요할 수 있다. 미리 하지 않았다면 자유도가 남아있었을 것이다.

리팩토링: 측정 우선 원칙

Premature Optimization의 반대는 성능 무시가 아니라 측정 기반 최적화다.

순서 1: 명확성 우선 작성.

첫 버전은 명확하게 짠다. 성능은 나중 문제로 미룬다. 대부분의 경우, 명확하게 짠 코드가 충분히 빠르다.

순서 2: 성능 요구사항 명시.

성능 목표를 구체적으로 정의한다. "빠르게"가 아니라 "p95 응답 시간 200ms 미만", "동시 접속 1000명 처리". 이 기준이 있어야 최적화의 성공 여부를 판단할 수 있다.

순서 3: 프로파일링.

실제 병목이 어디인지 측정한다. 언어별로 도구가 다르다.

  • Node.js: --prof, Chrome DevTools, clinic.js
  • 브라우저: Chrome DevTools Performance 패널
  • DB: EXPLAIN ANALYZE, slow query log
  • 시스템 레벨: perf, flame graph

측정 없이 최적화하는 것은 안 하는 것보다 나쁘다. 대개 잘못된 곳을 고친다.

순서 4: 병목만 최적화.

측정으로 확인된 병목에만 손을 댄다. 그리고 최적화 후에 다시 측정한다. 실제로 개선되었는지 확인하지 않으면 그 최적화가 유지될 이유가 없다.

순서 5: 문서화.

왜 이 코드가 이렇게 최적화되어 있는지 주석으로 남긴다. 다음 사람이 "이거 왜 이렇게 짜놨지?"라고 원래대로 돌리지 않도록.

typescript
// PERFORMANCE: 이 함수는 매 요청마다 호출되고 users.length가 10만 이상.
// 프로파일링 결과 전체 요청 시간의 40%가 여기서 소비됨.
// filter 대신 for문으로 바꿔서 30% 개선 확인 (2026-06 벤치마크).

함정 — "성능은 나중에" 극단

Premature Optimization을 방어선으로 삼아 성능을 아예 무시하는 것도 함정이다. Knuth의 말은 "성능을 무시하라"가 아니다.

특히 다음 세 종류의 성능 문제는 나중에 고치기가 매우 어렵다.

1. 아키텍처 수준의 성능. N+1 쿼리를 유발하는 API 구조, 잘못된 데이터 모델, 잘못된 캐싱 계층은 나중에 고치려면 시스템 전체를 뜯어야 한다. 이건 처음부터 고려한다.

2. 알고리즘 복잡도. O(n²)로 짤 것을 O(n)으로 짤 수 있으면 처음부터 그렇게 짠다. 알고리즘 선택은 리팩토링이 매우 비싸다.

3. 데이터 규모 가정. "이 데이터가 얼마나 커질 수 있는가"를 처음에 생각해두지 않으면 나중에 스케일링 위기가 온다. 상한을 미리 정하지 않으면 모든 데이터가 무제한이라고 가정하게 된다.

규칙: 알고리즘과 아키텍처는 처음부터, 미시 최적화는 측정 후에. 이 구분이 Premature Optimization 방어와 성능 무시 사이의 균형이다.

요약

항목내용
증상측정 없이, 병목 확인 없이 성능 최적화 시도
원어Knuth의 인용은 "97%는 최적화하지 마라, 단 3%는 놓치지 마라"
원인성능에 대한 막연한 불안, 프로파일링 습관 부재
해법명확성 우선 → 요구사항 명시 → 프로파일링 → 병목만 최적화
함정성능을 아예 무시하는 반대 극단
처음부터 고려아키텍처, 알고리즘 복잡도, 데이터 규모 가정
나중에 고려미시 최적화, 캐싱, 인덱스

13. Reinventing the Wheel

정의

이미 잘 검증된 라이브러리/도구가 있는데 처음부터 다시 만드는 안티패턴. "바퀴를 재발명한다"는 표현에서 왔다. 관련 개념으로 NIH(Not Invented Here) 신드롬이 있는데, 조직 차원에서 외부에서 만든 것에 대한 거부감을 뜻한다.

Cargo Cult의 정반대편에 있다.

  • Cargo Cult: 이유 없이 남의 것을 도입
  • Reinventing the Wheel: 이유 없이 남의 것을 거부

이 둘은 함께 어떤 도구를 도입할지에 대한 판단의 양극단이다. 건강한 판단은 그 사이에 있다.

냄새나는 상황

패턴 1: 자기만의 날짜 유틸리티.

typescript
// utils/date.ts — 자체 구현
export function formatDate(date: Date, format: string): string {
  // 시간대, DST, 로케일, 여러 포맷 처리...
  // 300줄
}
 
export function parseDate(str: string): Date { /* 200줄 */ }
export function addDays(date: Date, days: number): Date { /* ... */ }
export function diffDays(a: Date, b: Date): number { /* ... */ }
// ...

date-fns, dayjs, luxon 같은 성숙한 라이브러리가 이미 존재한다. 날짜/시간 처리는 시간대, 서머타임, 윤년, 로케일 등의 지뢰밭이다. 자체 구현은 반드시 그 지뢰들을 다시 밟는다.

패턴 2: 자기만의 상태 관리.

React 팀에 "Redux는 무거워요, Context는 리렌더링 문제가 있어요, Zustand는 잘 몰라요"의 결론으로 자체 상태 관리 라이브러리를 만든다. 6개월 뒤 300줄이 되고, 팀에서 그것을 이해하는 사람은 만든 사람뿐이다.

패턴 3: 자기만의 폼 라이브러리.

react-hook-form, formik 같은 도구가 있지만 "우리 요구사항이 특수해서" 자체 구현. 검증, 에러 표시, 조건부 필드, 폼 초기화, 폼 리셋, 접근성, 성능 최적화를 다 다시 해결한다.

패턴 4: 자기만의 HTTP 클라이언트.

axios, ky, ofetch가 있지만 fetch를 감싼 자체 클라이언트를 만든다. 초기에는 20줄이지만 재시도, 타임아웃, 인터셉터, 취소, 진행률, 파일 업로드가 추가되면서 500줄이 된다.

패턴 5: 자기만의 로거.

winston, pino가 있지만 자체 로거 구현. 초기에는 console.log 래핑 수준이지만, 로그 레벨, 포맷팅, 로테이션, 원격 전송, 구조화 로깅이 요구되면서 프로덕션 이슈의 원천이 된다.

왜 안티패턴인가

1. 시간 낭비. 이미 해결된 문제를 다시 해결하는 데 팀 시간을 쓴다. 그 시간에 실제 도메인 문제를 풀 수 있었다.

2. 이미 알려진 버그의 재발견. 성숙한 라이브러리는 수년간 수많은 사용자가 발견한 엣지 케이스를 다뤘다. 자체 구현은 그 엣지 케이스들을 다시 발견해야 한다. 사용자에게 버그를 뿌리면서.

3. 유지보수 부담. 자체 구현은 만든 팀이 영원히 유지해야 한다. 라이브러리는 외부 커뮤니티가 유지한다.

4. 팀 확산의 장벽. 신입 개발자는 자체 도구를 새로 배워야 한다. 일반 라이브러리를 썼다면 기존 지식을 쓸 수 있었다.

5. 채용의 어려움. 회사 특화 도구를 아는 사람만 채용해야 하거나, 채용 후 학습에 시간을 쓴다.

왜 생기는가

심리 1: 통제 욕구. "우리가 만들면 우리가 원하는 대로 바꿀 수 있다." 실제로는 라이브러리도 fork하거나 PR을 보낼 수 있다. 통제감은 자주 환상이다.

심리 2: "간단해 보인다". "20줄이면 되는데 라이브러리 뭐 하러." 6개월 뒤 그 20줄은 300줄이 되어 있다. 라이브러리의 크기는 그것이 다루는 엣지 케이스의 크기다.

심리 3: 재미. 도구를 만드는 것이 즐겁다. 사업 로직을 짜는 것보다 재미있다. 무의식적으로 자체 구현을 선호한다.

심리 4: 학습 투자 정당화. 새 라이브러리를 배우는 것보다 자기가 짜는 것이 편하다. 편한 길을 선택한다.

심리 5: 의존성 회피. "라이브러리에 종속되면 위험하다." 이 걱정 자체는 정당하지만, 종속을 피하려고 자체 구현을 하면 자기 자신에게 종속된다.

리팩토링: 도입 판단 프레임

Reinventing the Wheel과 Cargo Cult 사이의 균형을 잡는 판단 프레임이 필요하다. 새 라이브러리 도입 앞에 던질 질문들.

질문 1: 자체 구현의 진짜 비용은?

초기 개발 시간만 계산하는 것은 절반의 계산이다. 실제 비용은:

  • 초기 개발 시간
  • 향후 유지보수 시간
  • 엣지 케이스 대응 (라이브러리는 이미 처리)
  • 문서화
  • 팀 확산 (신입 학습)
  • 버그 수정
  • 성능 최적화

이 총합이 라이브러리 학습 비용 + 도입 비용 + 종속 관리 비용보다 작을 때만 자체 구현이 합리적이다.

질문 2: 라이브러리의 신뢰성은?

라이브러리 선택도 판단이다. 점검할 것들.

  • GitHub star 수, 다운로드 수 (인기의 대리 지표)
  • 마지막 커밋 시점, 릴리즈 주기 (유지되고 있는가)
  • 열린 이슈 수, 해결 속도
  • 라이선스 (MIT/Apache가 안전, GPL은 주의)
  • 크기 (번들에 영향)
  • 대안 존재 여부 (다른 라이브러리로 옮길 수 있는가)

질문 3: 도메인 특화가 실제로 필요한가?

"우리는 특수하다"는 자주 나오는 이유지만, 대개 특수한 정도는 라이브러리를 확장하는 것으로 충분하다. 진짜로 도메인 특화가 필요한 경우는 드물다. 기존 라이브러리를 감싸는 얇은 어댑터로 대부분 해결된다.

질문 4: 반대 방향 위험은?

라이브러리에 종속되면 다음이 문제다.

  • 라이브러리가 방향을 바꾸면 (breaking change)
  • 라이브러리가 방치되면
  • 라이브러리에 보안 취약점이 생기면

이 위험을 완화하는 방법: 얇은 어댑터로 감싸서 라이브러리 교체가 가능한 구조 유지. 라이브러리 전체 API를 앱 코드가 직접 쓰지 않는다.

Cargo Cult와의 균형

두 안티패턴 사이의 판단은 다음처럼 정리된다.

상황Cargo CultReinventing the Wheel
"다들 쓰니까 쓰자"발생(반대편 오류)
"특수하니까 우리가 만들자"(반대편 오류)발생
판단 기준문제 정의가 없다진짜 비용 계산이 없다

건강한 판단은 문제 정의 → 대안 검토 (자체 구현 포함) → 총비용 비교 → 얇은 어댑터로 감싸기의 흐름이다. 이 흐름이 있으면 어느 극단에도 빠지지 않는다.

함정 — 모든 것을 라이브러리로

Reinventing the Wheel을 방어선으로 삼아 반대 극단에 빠질 수 있다. 모든 것을 라이브러리로 해결하려는 것이다.

  • 5줄이면 되는 유틸리티를 위해 200KB 라이브러리를 도입
  • 자기 앱에만 특화된 로직인데도 범용 라이브러리를 찾아 헤맴
  • 라이브러리를 조합하고 조합해서 실제 로직보다 라이브러리 관리에 시간을 더 씀

또한 라이브러리 도입 자체가 새로운 안티패턴을 만들 수 있다.

  • 의존성 지옥: 서로 충돌하는 라이브러리들
  • 번들 크기 폭증: 모바일에서 로딩 느려짐
  • 보안 취약점: 간접 의존성까지 관리해야 함
  • 업그레이드 부담: 주기적 breaking change 대응

기준은 반복된다: 문제 정의가 먼저다. 라이브러리가 그 문제에 대한 답인가.

요약

항목내용
증상검증된 도구 대신 자체 구현
Cargo Cult와의 관계정반대 극단
원인통제 욕구, "간단해 보임", 재미, 학습 투자 회피
해법총비용 계산, 라이브러리 신뢰성 평가, 얇은 어댑터
함정반대로 모든 것을 라이브러리로 (번들 폭증, 의존성 지옥)
판단 기준문제 정의 → 대안 검토 → 총비용 비교 → 어댑터로 감싸기

여섯 가지를 관통하는 원리

1편의 안티패턴들이 "코드가 어떻게 나쁜가"에 대한 답이었다면, 2편은 "왜 시스템과 팀이 나빠지는가"에 대한 답이다. 여섯 가지를 관통하는 원리 세 가지를 뽑으면 다음과 같다.

원리 1. 시스템은 짧게 뿐 아니라 오래도 살아있어야 한다

Spaghetti Code, Big Ball of Mud, Lava Flow는 모두 시간에 대한 안티패턴이다. 단기에는 문제가 없다. 만든 사람은 자기 코드를 이해한다. 문제는 6개월, 1년, 3년이 지났을 때다. 시스템이 커지고 사람이 바뀌면서 초기의 편의가 부담이 된다.

이걸 방어하려면 미래의 자기 자신 또는 미래의 팀원을 상상하며 코드를 짜야 한다. "지금 나에게 편한 것"이 아니라 "1년 뒤 이걸 처음 보는 사람에게 편한 것"이 기준이다.

원리 2. 판단은 편향의 총합이다

Golden Hammer, Premature Optimization, Reinventing the Wheel은 모두 판단 습관에 대한 안티패턴이다. 그리고 각 안티패턴의 뿌리에는 인간의 자연스러운 심리가 있다.

  • Golden Hammer: 학습 투자 회수, 유능감 유지
  • Premature Optimization: 성능에 대한 막연한 불안
  • Reinventing the Wheel: 통제 욕구, 재미

이 심리들은 다 정상적이다. 문제는 그것을 인식하지 못한 채 매번 같은 판단을 반복하는 것이다. 자신의 편향을 의식하는 것이 방어의 시작이다. "내가 이걸 선택하는 진짜 이유는 무엇인가?"를 자신에게 물을 수 있어야 한다.

원리 3. 모든 방어선에는 반대 극단이 있다

이 패턴은 1편에서도 반복적으로 등장했지만, 2편에서 더 명확하다. 각 안티패턴에 대한 방어가 지나치면 새로운 안티패턴이 된다.

  • Spaghetti 방어 → 오버 엔지니어링 (모든 상태에 상태 머신)
  • Big Ball of Mud 방어 → 성급한 완벽한 아키텍처
  • Lava Flow 방어 → 살아있는 코드까지 실수로 삭제
  • Golden Hammer 방어 → 새로움 자체의 추구
  • Premature Optimization 방어 → 성능 무시
  • Reinventing the Wheel 방어 → 모든 것을 라이브러리로

"이 안티패턴만 피하면 된다"는 사고 자체가 새로운 Cargo Cult가 된다. 한 방향으로 도망친다고 안전한 곳에 도착하지는 않는다. 양방향의 위험을 모두 인식한 상태에서 매 결정을 상황에 맞게 내리는 것이 실제 실력이다.


실무에 적용할 때

이 여섯은 1편의 일곱과 달리 개인의 리팩토링으로 해결되지 않는다. 조직 차원의 방어선이 필요하다.

1. 코드 리뷰의 초점을 확장.

코드 리뷰는 대개 "이 함수가 잘 짜여있는가"에 집중된다. 하지만 아키텍처와 프로세스 안티패턴은 개별 함수 리뷰로 잡히지 않는다. 다음도 리뷰 대상이다.

  • 이 파일이 어디에 위치하는가 (모듈 경계 준수)
  • 새 import가 순환을 만들지 않는가
  • 새 라이브러리 도입에 이유 문서가 있는가
  • deprecated 마킹이 누적되고 있지 않은가

2. 정기적 부채 점검.

스프린트마다 또는 분기마다 코드베이스의 상태를 점검하는 시간. deprecated 목록, 죽은 코드, 순환 의존, 오래된 실험 코드를 확인한다. 항목마다 담당자를 정하고, 방치되지 않도록 관리한다.

3. Architectural Decision Record.

1편에서 언급한 ADR이 여기서도 결정적이다. 특히 Golden Hammer와 Reinventing the Wheel은 ADR 없이 방어하기 어렵다. "왜 이 도구를 선택했는가"의 기록이 있으면 후임자가 그 결정을 재평가할 수 있다.

4. 삭제와 정리를 성과로 인정.

코드 삭제, 라이브러리 제거, 아키텍처 단순화 같은 활동을 팀이 인정하는 문화. 라인 수 증가만 성과가 되면 Lava Flow와 Big Ball of Mud가 자란다.

5. 자기 자신에 대한 방어선.

이 시리즈를 다 읽었다면 "우리 회사가 Big Ball of Mud 같다"는 진단부터 하고 싶어질 것이다. 그 진단은 아마 맞을 것이다. 하지만 1편의 마지막 경고를 다시 상기해야 한다.

배운 렌즈로 남을 판단하는 무기로 쓰지 마라. 특히 회사의 오래된 시스템이나 시니어의 결정에 대해서. 그 시스템은 지금까지 사업을 굴렸다는 실증이 있다. 지식이 새로 생겼다고 그 실증을 무시하지 않는다.

건강한 사용법:

  • 자기가 짜는 새 코드에 렌즈로 활용.
  • 팀과 공통 언어로 사용. "이건 Lava Flow가 되어가고 있어"라는 대화가 가능해진다.
  • 조직 프로세스를 개선할 근거로 사용. ADR 도입, 정기 점검 도입, 리뷰 기준 확장.
  • 남의 코드를 비난하는 도구로는 사용하지 않기.

두 편을 마치며

디자인 패턴 학습을 위한 전제로 시작한 안티패턴 여정이 여기까지 왔다.

1편 — 코드와 설계 차원의 일곱 안티패턴 (Magic Number, God Object, Long Method, Feature Envy, Shotgun Surgery, Primitive Obsession, Cargo Cult)

2편 — 아키텍처와 프로세스 차원의 여섯 안티패턴 (Spaghetti Code, Big Ball of Mud, Lava Flow, Golden Hammer, Premature Optimization, Reinventing the Wheel)

이 열세 가지는 서로 독립적이지 않다. 지역적인 God Object가 여러 개 모이면 Big Ball of Mud가 되고, Long Method가 useEffect 체인과 결합되면 Spaghetti Code가 된다. Golden Hammer는 Cargo Cult로 팀에 확산되고, 확산된 결과가 축적되면 Lava Flow가 된다. 하나의 안티패턴이 다른 안티패턴의 씨앗이 된다.

이제 다음 단계는 디자인 패턴이다. 하지만 그 학습에 들어가기 전에 한 걸음이 더 필요하다. 지금까지 배운 열세 개의 렌즈로 자기가 짜고 있는 코드를 며칠 동안 관찰하는 시간이다. 실제로 어떤 안티패턴에 해당하는지, 왜 그렇게 되었는지, 지금 고칠 가치가 있는지를 판단해본다. 이 관찰이 없으면 디자인 패턴 역시 Cargo Cult로 배우게 된다.

디자인 패턴은 이 안티패턴들에 대한 반복 검증된 답이다. Strategy는 Feature Envy의 정책 분리, Facade는 God Object 없이 복잡성 다루기, Observer는 Shotgun Surgery의 알림 통합, Adapter는 Reinventing the Wheel과 종속 관리의 균형점이다.

문제를 체감한 이후에 답을 보는 것이 답을 먼저 보고 문제를 찾는 것보다 훨씬 튼튼한 학습이 된다. 여기까지가 그 준비였다.

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..