ComplexityTaming
주니어 개발자들을 위한 패턴 언어 - 코드의 덩어리를 쪼개고 이름을 붙여 복잡성의 야수를 길들이는 법
Contents
The Story: The Gordian Knot vs. The Organized Toolbox
두 명의 개발자, 민수와 하나가 '복잡한 배송료 계산 엔진'을 다루고 있다.
민수의 코드 (The Gordian Knot): 민수의 계산 함수는 300줄이 넘는다. 국가별 세율, 무게당 단가, 부피당 단가, 할인 프로모션, 주말 할증 로직이 거대한 if-else와 for 루프 속에 뒤섞여 있다. "로직이 워낙 복잡해서 어쩔 수 없어요." 민수 자신도 코드를 수정할 때마다 어디가 어떻게 영향을 받을지 몰라 전전긍긍한다. 작은 기능 하나를 추가하려고 해도 실타래처럼 엉킨 코드 전체를 읽어야 한다.
하나의 코드 (The Organized Toolbox): 하나는 복잡함을 인정하고 그것을 다루는 도구들을 꺼냈다.
분해(Decomposition): 거대한 함수를 calculateTax(), calculateBaseRate(), applyPromotions()로 쪼갰다.
이름 붙이기(Naming): 각 조각에 명확한 이름을 붙여 무엇을 하는지 드러냈다.
추상화(Abstraction): 배송 정책을 ShippingPolicy 인터페이스로 정의하여 새로운 정책을 추가하기 쉽게 만들었다.
하나는 복잡한 문제를 여러 개의 '단순한 문제'로 바꾸어 놓았다. 그녀의 코드는 부품들이 잘 정리된 도구함 같아서, 누구나 필요한 부분만 골라 이해하고 수정할 수 있었다.
Context
시스템이 OrganicGrowth를 통해 성장하면서, 특정 부분의 로직이 감당하기 힘들 정도로 복잡해졌다. 함수가 길어지고, 조건문이 중첩되며, 변수가 너무 많아진다.
일상적인 상황:
- 코드를 한 번에 머릿속에 다 담을 수 없다.
- "이 부분은 건드리기 무섭다"는 말이 나오기 시작한다.
- 작은 수정 하나가 예상치 못한 곳에서 버그를 일으킨다.
- 코드를 설명하기 위해 수많은 주석이 필요하다.
당신은 지금 복잡성이라는 야수에게 먹히기 직전이다.
Problem
관리되지 않는 복잡성은 개발자의 인지 한계를 넘어서고, 결국 시스템의 유지보수를 불가능하게 만든다.
인지 부하(Cognitive Load) 폭발: 코드를 읽는 것만으로도 뇌의 에너지가 고갈된다.
수정 비용 급증: 어디를 고쳐야 할지 찾는 데 시간이 더 많이 걸린다.
기술 부채의 근원: 복잡한 코드는 리팩토링하기 힘들기 때문에 계속 방치되고, 시스템은 점점 더 썩어간다.
Solution
추상화, 분해, 네이밍, 관심사 분리라는 "복잡성 길들이기 도구"를 사용하여 문제를 다룰 수 있는 크기로 쪼개라.
복잡성을 없앨 수는 없지만, 그것을 **정리**할 수는 있다.
Principle 1: Divide and Conquer (분할 정복)
큰 문제는 작은 문제들로 나누어라.
- 100줄짜리 함수보다는 10줄짜리 함수 10개가 낫다.
각 함수는 단 한 가지 일만 해야 한다(SingleResponsibility).
작은 조각들은 그 자체로 이해하기 쉽고 테스트하기도 쉽다(CleanIsolation).
Principle 2: Names as Abstractions (이름을 통한 추상화)
복잡한 로직 뒤에 이름을 붙여 숨겨라.
if (age > 19 && hasTicket && !isBlacklisted) 라고 쓰는 대신, if (canEnter(user)) 라고 써라.
이름을 붙이는 순간, 독자는 '어떻게(How)'가 아닌 '무엇을(What)' 하는지에 집중할 수 있다. 이것이 추상화의 본질이다(NamesAsDesign).
Principle 3: Separation of Concerns (관심사 분리)
서로 다른 종류의 지식은 섞지 마라.
- 비즈니스 로직과 데이터 저장 로직을 분리하라.
- UI 표현 로직과 데이터 처리 로직을 분리하라.
- 관심사가 분리되면 한 곳을 고칠 때 다른 곳을 걱정할 필요가 없다.
Principle 4: Encapsulate Volatility (변하는 것을 캡슐화하라)
자주 바뀔 것 같은 부분은 따로 떼어내어 감싸라.
- 할인 정책이 매달 바뀐다면, 그 로직을 별도의 클래스로 분리하라.
- 변화의 물결이 시스템 전체로 퍼지지 않도록 방파제를 세우는 것이다.
Real Examples
Example 1: Nested If-Else vs. Guard Clauses
민수:
if (user != null) {
if (user.isActive()) {
if (order.isValid()) {
process(order);
}
}
}하나 (Guard Clauses):
if (user == null) return; if (!user.isActive()) return; if (!order.isValid()) return; process(order);
복잡한 중첩을 제거하여 사고의 흐름을 직선으로 만들었다.
Example 2: Extracting a Policy Object
배송비 계산 로직이 상품 종류, 거리, 무게에 따라 복잡하게 얽혀 있다면, 이를 FeeCalculator라는 객체로 추출하여 복잡성을 그 안에 가둔다.
Common Pitfalls
"Premature Abstraction" (성급한 추상화)
문제가 충분히 복잡해지기도 전에 미리 레이어를 나누는 것.
DirectPath를 기억하라. 복잡성이 고통을 줄 때 비로소 길들이기를 시작하라.
"Leaky Abstraction" (새는 추상화)
추상화를 만들었지만, 내부 세부 사항이 밖으로 새어 나오는 것.
- 추상화된 이름을 보고도 내부 코드를 들여다봐야만 한다면, 그것은 실패한 추상화다.
Over-splitting (과도한 쪼개기)
너무 잘게 쪼개서 오히려 전체 흐름을 보기 힘든 경우.
- 관련 있는 것들은 다시 뭉쳐서 적절한 응집도를 유지하라.
Connection to Other Patterns
NamesAsDesign - 좋은 이름은 복잡성을 이기는 가장 강력한 무기다. 핵심 도구
DataAsFoundation - 데이터 구조를 먼저 잘 잡으면 로직의 복잡성이 획기적으로 줄어든다. 기반
DirectPath - 복잡성을 줄이기 위해 만든 장치가 또 다른 복잡성을 만들지 않도록 경계하라. 균형
OrganicGrowth - 복잡성은 한 번에 해결되지 않는다. 자라나는 속도에 맞춰 지속적으로 길들여야 한다. 과정
Signs of Success
- 거대한 로직을 수정했는데, 수정 범위가 특정 클래스나 함수 내로 한정된다.
- 새로운 팀원이 코드를 보고 "생각보다 읽기 쉽네요"라고 말한다.
- 각 함수나 클래스의 책임이 명확하여 테스트 코드를 짜기가 매우 쉽다.
- 코드를 읽을 때 머릿속에서 시뮬레이션할 내용이 현저히 줄어든다.
The Ultimate Insight
소프트웨어 공학의 역사는 복잡성을 다루는 도구를 개발해온 역사다.
우리는 복잡한 세상을 다루기 위해 프로그래밍을 한다. 하지만 코드가 세상보다 더 복잡해져서는 안 된다. 문제를 쪼개고, 이름을 붙이고, 경계를 세워라. 복잡성을 통제할 수 있을 때, 비로소 당신은 시스템의 주인이 된다.
CategoryPatternLanguage CategoryProgramming CategoryDesign CategoryRefactoring
