스스로를 반복하라
소프트웨어 경력 내내 가장 자주 들었던 조언 중 하나는 “반복하지 마라(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)가 개발한 개념으로, 전투기 조종사들은 이를 활용해 책임질 수 있는 마지막 순간까지 행동 방침 결정을 미루고, 이를 통해 현재 상황과 가용 정보를 바탕으로 최선의 결정을 내립니다. ↩
글을 무작위로 읽기