❗

성급한 추상화를 피해보자

태그
소프트웨어 공학
최종 수정일
Last updated October 1, 2026
Slug
aha-programming
Date
October 1, 2026
Description
aha-programming 을 통해서 성급한 추상화를 피하는 방법에 대해서 배워봅니다. DRY, WET 과 같은 소프트웨어 공학 법칙과 같은 원칙을 명목적으로 따르다보면 생기는 문제점들을 다룹니다.
Published
Published
notion image
소프트웨어 공학 책을 읽다 보면 YAGNI(You ain’t gonna need it) / KISS(Keep It Simple Stupid!) / DRY (Dont’ Repeat Yourself)이런저런 원칙을 만나게 됩니다. 한때는 "이 원칙들을 지키며 개발해야지"라고 다짐했습니다. 물론 먼 미래의 내가 이 코드를 다시 보고서 왜 그랬나 싶을 때가 많습니다.
저는 그 중 DRY(Don't Repeat Yourself) 원칙을 좋아했습니다. 따라서, 이를 생각하다보니 코드를 너무 잘게 쪼갰습니다. 뭐든 util 함수로 빼고, 어딘가에서 재사용할 거라는 생각에 함수를 계속 나눴습니다. 그러다가, 언젠가 다시 합쳐야 하는 순간이 오면 골머리를 썩곤 했습니다. 물론 다 합치고 나면 뿌듯했습니다. 👍🏻

DRY와 WET 원칙은 각각 무슨 약어일까?
먼저 두 원칙의 이름부터 짚어 보겠습니다.
DRY는 “Don't Repeat Yourself”입니다. 같은 것을 반복하지 말라는 뜻입니다.
WET은 “Write Everything Twice”입니다. 모든 것을 두 번씩 써도 된다는 쪽입니다. DRY(마른)와 WET(젖은)은 단어부터 서로 짝을 이루며 같이 사용합니다.
WET을 설명할 때 이런 문장이 따라옵니다.
You can ask yourself "Haven't I written this before?" two times, but never three.
"이거 전에 써 본 적 있는데?"라는 생각은 두 번까지는 해도 되지만, 세 번째는 안 된다는 뜻입니다.
두 원칙은 서로 반대를 말하는 것처럼 보입니다. DRY는 반복하지 말라고 하고, WET은 한 번은 반복해도 된다고 하니까요. 그러면 정답은 무엇일까요? 아니면 정답이란 게 존재할까요? 답하기 전에, DRY를 좋아하던 시절의 제 코드부터 돌아보겠습니다.
DRY를 따르면 코드가 어떻게 될까?
금액 뒤에 "원"을 붙여 보여 주는 단순한 기능을 예로 들겠습니다. DRY를 좋아하던 때의 저는 이런 식으로 쪼갰습니다.
// utils/string.ts // 문자열 뒤에 접미사를 붙이는 함수입니다. 어디서나 쓸 수 있을 것 같아서 만들게 됩니다. export const addSuffix = (value: string, suffix: string) => `${value}${suffix}`; // utils/number.ts // 숫자에 천 단위 구분 기호를 넣는 함수입니다. export const withComma = (value: number) => value.toLocaleString('ko-KR'); // utils/price.ts // 위 두 함수를 조합해서 원화 표기를 만드는 함수입니다. export const formatKRW = (value: number) => addSuffix(withComma(value), '원');
addSuffix는 재사용될 것 같아서 만든 함수입니다. 하지만 실제로 쓰는 곳은 formatKRW 하나뿐이었고, 먼 미래에 addSuffix 와 애매하게 역할을 달리하는 함수를 발견하곤 합니다.
이 한 줄을 이해하려면 파일 세 개를 열어야 합니다. 이 때 얻은 것은 코드 몇 글자 줄이기, 자기 만족, 지적 허영심에 가깝습니다. 반면에 잃은 것은 인지 속도, 읽는 속도, 코드 리뷰어의 시간입니다.
간단한 코드들이라면 상관 없겠지만, 여러 훅과 유틸 함수가 섞여있는 상황에 하나씩 타고 들어가다보면 어느새 지쳐있는 스스로를 발견할 수 있습니다. 중복은 줄었지만 코드를 이해하는 비용은 오히려 늘어났습니다. 이 점을 보며 아래와 같은 글을 쓰기도 했습니다.

DRY는 정말 '같은 코드를 쓰지 말라'는 뜻이었을까?
DRY의 원래 정의에는 '코드'라는 단어가 없습니다.
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system
시스템 안의 모든 지식은 단일의, 명확한 표현으로만 존재해야 한다는 뜻입니다. 여기서 지식이란 "닉네임은 12자까지 허용한다" 같은 규칙이나 결정을 말합니다. 이런 규칙은 한 곳에 두는 것이 맞습니다. 닉네임 길이 제한이 input, validation, 안내 문구에 따로 적혀 있으면 규칙이 바뀔 때 한 곳을 놓치게 됩니다.
// 닉네임 길이 제한이라는 규칙을 한 곳에만 둡니다. export const MAX_NICKNAME_LENGTH = 12; // input, validation, 안내 문구가 모두 같은 값을 씁니다. <input maxLength={MAX_NICKNAME_LENGTH} />; const isValid = nickname.length <= MAX_NICKNAME_LENGTH; const message = `닉네임은 ${MAX_NICKNAME_LENGTH}자까지 입력할 수 있습니다.`;
반대로 코드가 비슷해 보이기만 하는 경우가 있습니다. 회원가입 form의 닉네임 input과 프로필 수정 form의 닉네임 input은 코드가 거의 같습니다. 하지만 회원가입 쪽에는 중복 확인이 붙고, 프로필 수정 쪽에서는 현재 닉네임과 같으면 통과시켜야 합니다.
코드가 같은 것과 같은 이유로 바뀌는 것은 다릅니다.
지금은 같아 보여도 바뀌는 이유가 다르다면, 그 코드는 다른 지식입니다.
그러면 AHA는 무엇이 다를까?
최근 AHA 라는 원칙을 접하게 되었고 마음에 들었습니다. Avoid Hasty Abstractions입니다. 서두른 추상화를 피하라는 원칙입니다. 이는 Kent C. Dodds란 JavaScript 개발자가 소개하는 원칙입니다.
YouTubeYouTubeAHA Programming – Kent C. Dodds
AHA에는 몇 번 반복되면 추상화하라는 글이 없습니다. 대신 내가 지금 알고 있는 이 지식만으로 이 추상화를 만들어도 되는지를 묻습니다. 이는 추상화 해도 되는 시점이 오기 전엔(성급하게는) 추상화 하지 않게끔 해주며, 딱딱한 규칙을 주지 않으면서 추상화를 신중하게 하도록 돕는 원칙이라고 설명합니다.
결국 중도를 지키라는 이야기이고, 중도의 기준을 횟수에 두지 않습니다. 두 번 반복된 코드가 있다고 잘못된 것도 아니고, 두 번 쓰는 것이 정답인 것도 아닙니다. 즉, AHA는 추상화를 하지 말라는 원칙이 아니라, 서두르지 말라는 원칙입니다.

성급한 추상화를 하면 어떤 모양이 될까?
상품 목록의 ProductCard와 장바구니의 CartItem을 예로 들겠습니다. 둘 다 이미지, 이름, 가격을 보여 주고, 서로 다른 버튼이 붙습니다.
// 상품 목록에서 쓰는 card입니다. function ProductCard({ product }: { product: Product }) { return ( <li className="card"> <img src={product.imageUrl} alt={product.name} /> <h3>{product.name}</h3> <p>{product.price.toLocaleString()}원</p> <WishButton id={product.id} /> </li> ); } // 장바구니에서 쓰는 card입니다. function CartItem({ item }: { item: CartItem }) { return ( <li className="card"> <img src={item.imageUrl} alt={item.name} /> <h3>{item.name}</h3> <p>{item.price.toLocaleString()}원</p> <QuantityStepper itemId={item.id} /> <button onClick={() => removeFromCart(item.id)}>삭제</button> </li> ); }
위쪽 네 줄이 똑같아 보입니다. DRY를 좋아하던 시절이라면 바로 ItemCard 하나로 합쳤을 것입니다.
합친 뒤 요구사항이 하나씩 들어왔다고 가정해 보겠습니다.
요구사항 1. 검색 결과 화면에서 작은 card가 필요해졌습니다. 요구사항 2. 이벤트 상품은 가격을 숨겨야 했습니다. 요구사항 3. 주문서에서는 수량 조절 UI를 숨겨야 했습니다.
// 처음에는 variant 하나로 충분했습니다. interface ItemCardProps { item: Item; variant: 'product' | 'cart'; compact?: boolean; // 요구사항 1 : 검색 결과 화면용 작은 card hidePrice?: boolean; // 요구사항 2 : 이벤트 상품의 가격 숨김 showQuantity?: boolean; // 요구사항 3 : 주문서의 수량 조절 UI 숨김 onRemove?: () => void; } function ItemCard({ item, variant, compact, hidePrice, showQuantity, onRemove }: ItemCardProps) { return ( // 요구사항 1 : compact면 작은 card 스타일을 씁니다. <li className={compact ? 'card card--compact' : 'card'}> <img src={item.imageUrl} alt={item.name} /> <h3>{item.name}</h3> {/* 요구사항 2 : hidePrice면 가격을 그리지 않습니다. */} {!hidePrice && <p>{item.price.toLocaleString()}원</p>} {variant === 'product' && <WishButton id={item.id} />} {/* 요구사항 3 : showQuantity일 때만 수량 조절 UI를 그립니다. */} {variant === 'cart' && showQuantity && <QuantityStepper itemId={item.id} />} {variant === 'cart' && onRemove && <button onClick={onRemove}>삭제</button>} {item.soldOut && variant === 'product' && <SoldOutBadge />} {item.soldOut && variant === 'cart' && <p>품절된 상품입니다. 삭제해 주세요.</p>} </li> ); }
variant로 간단히 두가지 컴포넌트를 합쳐서 쓰는데, 요구사항들을 처리하다보니 compact, hidePrice, showQuantity등의 prop들이 늘어났습니다.
이제 ItemCard를 쓰는 쪽은 <ItemCard variant="cart" compact hidePrice /> 같은 조합을 매번 머릿속으로 따져 봐야 합니다. variant === 'cart'가 여기 저기서 보이는 것은, 한 component 안에 사실상 여러 책임의 component들이 들어 있다는 신호입니다.
에어컨, TV, 오디오 리모컨을 만능 리모컨 하나로 합쳤더니 버튼이 수십 개가 되고, TV를 끄려다 에어컨을 끄게 되는 상황과 비슷합니다.
notion image
prefer duplication over the wrong abstraction
잘못된 추상화를 갖느니 차라리 중복을 택하기를 권하기도 합니다.
AHA로 다시 쓰면 어떻게 달라질까?
같은 상황을 AHA로 다시 써 보겠습니다. 처음 두 component는 그대로 둡니다.
// 요구사항 1 : 가격 표기는 상품 목록과 장바구니가 같은 규칙을 씁니다. 같은 목적을 가지니 따로 유틸 함수로 뺍니다. export const formatPrice = (price: number) => `${price.toLocaleString('ko-KR')}원`; function ProductCard({ product }: { product: Product }) { return ( <li className="card"> <img src={product.imageUrl} alt={product.name} /> <h3>{product.name}</h3> <p>{formatPrice(product.price)}</p> {/* 요구사항 2 : 상품 목록에서는 품절이면 뱃지만 보여 줍니다. */} {product.soldOut && <SoldOutBadge />} {/* 요구사항 3 : 상품 목록에서는 찜하기 버튼이 필요합니다. */} <WishButton id={product.id} /> </li> ); } function CartItem({ item, onRemove }: { item: CartItemData; onRemove: () => void }) { return ( <li className="card"> <img src={item.imageUrl} alt={item.name} /> <h3>{item.name}</h3> <p>{formatPrice(item.price)}</p> {/* 요구사항 4 : 장바구니에서는 품절이면 삭제를 안내하는 문구를 보여 줍니다. */} {item.soldOut && <p>품절된 상품입니다. 삭제해 주세요.</p>} {/* 요구사항 5 : 장바구니에서는 수량 조절과 삭제가 필요합니다. */} <QuantityStepper itemId={item.id} /> <button onClick={onRemove}>삭제</button> </li> ); }
비슷해 보이는 JSX는 그대로 남아 있습니다. 대신 위의 각 요구사항이 바뀔 때 고칠 곳이 분명합니다. 장바구니의 품절 문구가 바뀌면 CartItem만 보면 됩니다.
이미 합쳐 버렸다면 되돌리는 방법도 있습니다. 추상화한 코드를 모든 호출처에 다시 풀어 넣고, 각 호출처에서 필요 없는 부분을 지운 뒤 남는 공통점을 다시 보면 됩니다. (제 경험담입니다 ㅎㅎ)

마무리
DRY는 반복하지 말라고 하고, WET은 두 번까지는 괜찮다고 합니다. AHA는 횟수 대신 지금 알고 있는 지식 수준으로 이 추상화가 맞는지를 묻습니다. 즉, 추상화는 코드가 같아 보일 때가 아니라 같은 이유로 바뀔 때 만들면 됩니다. 비슷한 코드를 발견했다면 바로 합치지 말고, 한동안 그대로 두고 지켜보는 것은 어떨까요?
참고 자료