스스로를 반복하라
원문은 Matthias Endler님이 에 게재했습니다. 이 블로그 구독하기
소프트웨어 업계에서 일하며 가장 자주 들었던 조언 중 하나는 “스스로를 반복하지 마라(Don’t Repeat Yourself)”, 이른바 DRY 원칙이었다. 오랫동안 나는 그 말을 액면 그대로 받아들였고, 그 타당성을 의심해 본 적도 없었다.
하지만 진짜 고수들이 코드를 작성하는 모습을 보고 생각이 바뀌었다. 그들은 수시로 코드를 복사해 붙여 넣는다1. 스스로를 반복하는 것에도 분명한 장점이 있다는 걸 깨달았다.
왜 사람들은 DRY를 사랑하는가
흔히 하는 이야기는 이렇다. 코드를 반복하면 같은 버그를 여러 곳에서 고쳐야 하지만, 공통된 추상화가 있으면 한 번만 고치면 된다는 것이다.
우리가 반복을 피하는 또 다른 이유는 그것이 우리를 영리하게 느끼게 만들기 때문이다. “봐, 나는 반복을 피하는 똑똑한 방법을 다 알아! 인터페이스, 제네릭, 고차 함수, 상속을 활용할 줄 안다고!”
두 이유 모두 잘못 짚은 것이다. 스스로를 반복하는 것에는 장기적으로 목표에 더 가까워지게 해주는 여러 이점이 있다.
흐름을 유지하기
코드를 작성할 때는 흐름을 유지해 몰입 상태에 들어가고 싶다. 완벽한 추상화를 설계하려고 계속 멈춰 서면 흐름을 잃기 쉽다.
대신 스스로에게 복사-붙여넣기를 허용하면 사고의 흐름을 유지한 채 눈앞의 문제에 집중할 수 있다. 올바른 추상화를 찾으려는 또 다른 고민을 동시에 떠안을 필요가 없다.
종종 기존 코드를 복사해 수정하면서 나아가다가, 부담이 될 정도로 중복이 쌓였을 때 리팩터링하는 편이 훨씬 쉽다.
“작성 모드”와 “리팩터링 모드”는 서로 다른 두 가지 프로그래밍 모드라고 생각한다. 작성 모드에서는 아이디어를 코드로 옮기는 데 집중하고, 코드가 엉망이라고 끊임없이 지적하는 내면의 비평가를 잠재워야 한다. 리팩터링 모드에서는 정반대의 역할, 즉 비평가가 된다. 올바른 추상화를 찾고, 중복을 제거하며, 가독성을 높여 코드를 개선할 방법을 찾는다.
이 두 모드를 분리하라. 동시에 둘 다 하려고 하지 마라.2
올바른 추상화를 찾는 일은 어렵다
코드를 작성하기 시작할 때는 아직 올바른 추상화가 무엇인지 알 수 없다. 하지만 코드를 복사하다 보면 올바른 추상화가 스스로 드러난다. 같은 코드를 계속 복사하는 일이 너무 지루해지면, 그때서야 추상화할 방법을 찾기 시작하기 때문이다. 내 경우 보통 같은 코드를 한 번 복사한 뒤에 그런 생각이 들지만, 두세 번째 복사까지는 그 충동을 참으려 노력한다.
너무 일찍 시작하면 문제에 맞지 않는 잘못된 추상화를 만들게 될 수 있다. 어딘가 어색하게 느껴진다면 잘못됐다는 신호다. 전형적인 증상은 다음과 같다.
- 의도를 드러내지 못하는 포괄적인 이름, 예:
generate_invoice가 되어야 할 곳에render_pdf_file을 쓰는 경우 - 추가적인 맥락 없이는 이해하기 어려움
- 추상화가 한두 곳에서만 사용됨
- 구현 세부 사항에 강하게 결합됨
잘못된 추상화를 없애기는 어렵다
우리는 가장 먼저 떠오른 추상화에 쉽게 안주하지만, 대개 그것은 올바른 추상화가 아니다. 그리고 잘못된 추상화를 제거하는 일은 힘든 작업이다. 이제 데이터 흐름이 그 추상화에 의존하게 되었기 때문이다.
우리는 또한 추상화를 만드는 데 시간과 노력을 들였기 때문에 자신의 추상화에 애착을 갖게 되는 경향이 있다. 그래서 더 이상 문제에 맞지 않더라도 버리기를 꺼리게 되는데, 이는 매몰 비용 오류다.
다른 프로그래머들까지 그 추상화에 의존하기 시작하면 상황은 더 나빠진다. 코드베이스의 다른 부분을 망가뜨릴 수 있으므로 변경할 때 조심해야 한다. 한번 추상화를 도입하면 오랫동안, 때로는 영원히 그것을 안고 가야 한다.
대신 코드 복사본이 있었다면 다른 곳을 망가뜨릴 걱정 없이 한 곳만 수정하면 된다.
중복은 잘못된 추상화보다 훨씬 저렴하다
—Sandi Metz, The Wrong Abstraction
문제 영역에 대해 확실히 이해하게 된 마지막 순간까지 추상화를 확정하는 것을 미루는 편이 낫다.3
추상화의 정신적 부담
추상화는 코드 중복을 줄여주지만 대가가 따른다.
추상화는 코드가 실제로 무엇을 하는지 이해하기 위해 여러 단계의 간접 참조를 넘나들어야 하므로 코드를 읽고 이해하고 유지하기 더 어렵게 만들 수 있다. 추상화는 다른 파일, 모듈, 라이브러리에 존재할 수도 있다.
이러한 계층을 오가는 비용은 크다. 숙련된 프로그래머라면 몇 단계의 추상화를 머릿속에 유지할 수 있겠지만, 우리 모두에게는 제한된 컨텍스트 윈도우가 있다(코드베이스에 대한 친숙도에 따라 달라진다).
코드를 복사하면 모든 로직을 한곳에 모아둘 수 있다. 전체를 그냥 읽고 무엇을 하는지 바로 이해할 수 있다.
성급한 추상화의 유혹을 참아라
때로 코드는 비슷해 보여도 서로 다른 목적을 수행한다.
예를 들어, 컬렉션을 순회하며 합계를 계산하는 두 코드 조각을 생각해 보자.
total = 0
for item in shopping_cart:
total += item.price * item.quantity그리고 코드의 다른 곳에는 이런 코드가 있다
total = 0
for item in package_items:
total += item.weight * item.rate두 경우 모두 컬렉션을 순회하며 합계를 계산한다. 헬퍼 함수를 도입하고 싶은 유혹이 들 수 있지만, 두 계산은 매우 다르다.
몇 번의 반복을 거치면 이 두 코드 조각은 서로 다른 방향으로 발전할 수 있다:
def calculate_total_price(shopping_cart):
if not shopping_cart:
raise ValueError("Shopping cart cannot be empty")
total = 0.0
for item in shopping_cart:
# Round for financial precision
total += round(item.price * item.quantity, 2)
return total반면 배송비 계산은 이렇게 보일 수 있다:
def calculate_shipping_cost(package_items, destination_zone):
# Use higher of actual weight vs dimensional weight
total_weight = sum(item.weight for item in package_items)
total_volume = sum(item.length * item.width * item.height for item in package_items)
dimensional_weight = total_volume / 5000 # FedEx formula
billable_weight = max(total_weight, dimensional_weight)
return billable_weight * shipping_rates[destination_zone]만약 “스스로를 반복하지 마라”를 너무 일찍 적용했다면 각 계산의 맥락과 세부 요구사항을 잃어버렸을 것이다.
DRY는 복잡성을 초래할 수 있다
DRY 원칙은 어떤 중복이든 반드시 피해야 하는 절대적인 규칙으로 잘못 해석되곤 하며, 이는 복잡성으로 이어질 수 있다.
추상화를 도입해 반복을 피하려고 하면 실제 비즈니스 로직에서 멀리 떨어진 곳에서 모든 엣지 케이스를 처리해야 한다. 결국 모든 경우에 동작하도록 추상화에 불필요한 검사와 조건을 추가하게 된다. 나중에는 그 검사를 넣은 이유를 잊어버리면서도 호출자를 망가뜨리고 싶지 않아 “혹시 모르니” 그대로 둔다. 그 결과 코드베이스에 복잡성만 더하는 데드 코드가 남는다. 모두 스스로를 반복하지 않으려 했기 때문이다.
흔히 하는 말은 스스로를 반복하면 같은 버그를 여러 곳에서 고쳐야 한다는 것이다. 하지만 이는 버그가 모든 복사본에 존재한다는 가정에 기반한다. 실제로는 각 복사본이 서로 다르게 발전했을 수 있고, 버그는 그중 하나에만 존재할 수도 있다.
공유 추상화를 만들면 그 추상화의 버그가 모든 호출자를 망가뜨려 여러 기능이 한꺼번에 고장 난다. 코드가 중복된 경우에는 버그가 특정한 한 사용 사례에만 격리된다.
나중에 정리하라
공유 추상화에서 아무것도 망가뜨리지 않았는지 확인하는 일은 코드 복사본 하나를 검사하는 것보다 훨씬 어렵다. 물론 복사본이 많으면 모두 수정하는 것을 잊어버릴 위험도 있다.
이 접근법을 성공시키는 핵심은 나중에 정리하는 것이다. 이는 코드를 커밋하기 전이나 코드 리뷰 중에 이루어질 수 있다.
이 단계에서 복사한 코드를 살펴보며 그대로 두는 것이 합리적인지, 혹은 올바른 추상화가 보이는지 판단할 수 있다. 나는 문제에 대해 더 잘 이해하게 된 후에 리팩터링을 시도하되, 그보다 일찍 하지는 않으려 한다.
잘못된 추상화를 되돌리는 한 가지 요령은 코드를 사용되던 곳으로 다시 인라인하는 것이다. 한동안 코드베이스에서 다시 “스스로를 반복”하게 되지만 괜찮다. 새로 얻은 정보를 바탕으로 문제를 다시 생각해 보라. 종종 문제에 더 잘 맞는 더 나은 추상화를 찾게 될 것이다.
추상화가 잘못되었을 때, 가장 빠른 전진은 뒤로 가는 것이다.
—Sandi Metz, The Wrong Abstraction
tl;dr
올바른 추상화를 찾는 것은 좋지만 집착하지는 마라. 흐름을 유지하고 올바른 추상화를 찾는 데 도움이 된다면 코드를 복사하는 것을 두려워하지 마라.
다시 한번 강조한다: “스스로를 반복하라.”
예시는 Rustendo64를 작업하는 Ferris나 C++ 게임 엔진을 작업하는 tokiospliff 영상을 참고하라. ↩
이것은 내가 글을 쓰는 방식이기도 하다. 먼저 초안을 쓰면서 내면의 비평가를 차단하고, 그다음 편집자/비평가 역할을 맡아 글을 “리팩터링”한다. 이렇게 하면 창의성을 막지 않는 빠른 피드백 루프와 더 다듬어지고 구조가 잘 잡힌 최종 결과물이라는 두 마리 토끼를 모두 잡을 수 있다. 물론 내가 이 방법을 고안한 것은 아니다. 이 기법에 대해 더 알고 싶다면 앤 라모트(Anne Lamott)의 책 Bird by Bird: Instructions on Writing and Life에 실린 “형편없는 초고(Shitty first drafts)”를 읽어보길 권한다. ↩
이는 “관찰(Observe), 판단(Orient), 결정(Decide), 행동(Act)”을 뜻하는 OODA 루프 개념과 유사하다. 군사 전략가 존 보이드(John Boyd)가 개발한 것으로, 전투기 조종사들은 이를 활용해 책임질 수 있는 마지막 순간까지 행동 방침 결정을 미루고, 이를 통해 현재 상황과 가용한 정보에 기반해 최선의 결정을 내린다. ↩
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기