DirectPath
주니어 개발자들을 위한 패턴 언어 - 복잡한 미로를 걷어내고 문제와 해결책 사이의 최단 거리를 찾는 법
Contents
The Story: The Maze of Indirection vs. The Straight Line
두 명의 개발자, 민수와 하나가 '사용자 이름 조회' 기능을 구현하고 있다.
민수의 설계 (The Maze): 민수는 "확장성"을 중요하게 생각했다.
UserDTO를 정의한다.
IUserRepository 인터페이스를 만들고 UserRepositoryImpl을 구현한다.
IUserService 인터페이스를 만들고 UserServiceImpl을 구현한다.
UserController가 서비스를 호출하고, 서비스는 저장소를 호출한다.
단순히 DB에서 이름 하나 가져오는데 5개의 파일과 10개의 메서드 호출이 얽혀 있었다. 나중에 버그가 생겼을 때, 민수는 데이터가 어디서 어떻게 변하는지 찾기 위해 수많은 파일을 넘나들며 길을 잃었다.
하나의 설계 (The Straight Line): 하나는 "명확함"을 선택했다. "지금은 복잡한 추상화가 필요 없어. 그냥 컨트롤러에서 필요한 데이터를 바로 가져오자." 하나는 불필요한 인터페이스와 추상 레이어를 걷어냈다. 입력(Request)이 들어오면 바로 데이터(DB)에 접근하여 결과(Response)를 내보내는 직선 경로를 만들었다. 코드는 단 몇 줄뿐이었고, 누가 읽어도 데이터의 흐름이 한눈에 보였다. 하나는 나중에 정말로 확장이 필요할 때, OrganicGrowth의 원칙에 따라 그때 레이어를 나누기로 했다.
Context
StrongCenter를 중심으로 기능을 구현하고 있다. CleanIsolation을 위해 의존성을 관리하려고 노력 중이다. 하지만 어느 순간부터 코드가 너무 많은 레이어와 "위임(Delegation)"으로 가득 차기 시작한다.
일상적인 상황:
- 코드 한 줄을 고치기 위해 5개 이상의 파일을 열어야 한다.
- "이 인터페이스의 실제 구현체가 어디 있지?"를 찾느라 시간을 다 쓴다.
- 단순히 값을 전달하기만 하는 '깡통' 메서드들이 너무 많다.
- "나중에 필요할 것 같아서" 미리 레이어를 나누어 두었다.
당신은 지금 미래의 유령과 싸우느라, 현재의 명확함을 희생하고 있다.
Problem
과도한 간접성(Indirection)과 레이어는 시스템의 흐름을 가리고 인지 부하를 높인다.
흐름의 파편화: 데이터가 어떻게 변하는지 추적하기가 매우 힘들어진다.
수정의 고통: 필드 하나를 추가하려면 DTO, Mapper, Service, Repository를 모두 고쳐야 한다 (Shotgun Surgery).
가짜 추상화: 구현체가 하나뿐인 인터페이스는 추상화가 아니라 장애물일 뿐이다.
Solution
입력에서 출력에 이르는 경로를 가리는 불필요한 레이어를 제거하고, 가능한 한 직접적으로 표현하라.
추상화는 공짜가 아니다. 그 비용을 지불할 가치가 있을 때만 도입하라.
Principle 1: Eliminate Empty Layers (깡통 레이어 제거)
단순히 다음 단계로 넘겨주기만 하는 클래스나 서비스는 과감히 삭제하라.
- 비즈니스 로직이 없는 Service 레이어는 필요 없다.
- Controller에서 Repository를 직접 호출하는 것이 죄는 아니다. 단순한 CRUD라면 그것이 더 명확하다.
Principle 2: Value Clarity over "Best Practices" (관행보다 명확성)
"레이어드 아키텍처는 4계층이어야 한다"는 교과서적 원칙에 얽매이지 마라.
- 당신의 프로젝트 규모와 복잡성에 맞는 경로를 찾아라.
복잡함(Complexity)은 필요할 때 도입해도 늦지 않다(OrganicGrowth).
Principle 3: Short Feedback Path (짧은 피드백 경로)
코드의 경로가 짧을수록 TightLoop가 쉬워진다.
- 확인해야 할 파일이 적을수록, 내 실수를 더 빨리 발견할 수 있다.
Principle 4: Flatten the Abstraction (추상화 납작하게 만들기)
너무 깊은 상속 구조나 중첩된 함수 호출을 경계하라.
- "A는 B를 부르고, B는 C를 부르고..." 하는 식의 호출이 3단계 이상 깊어진다면, 그것을 하나로 합칠 수 없는지 검토하라.
Real Examples
Example 1: Implementation-only Interface
민수:
interface UserService { User find(Long id); }
class UserServiceImpl implements UserService { ... }(구현체가 하나뿐인데 인터페이스를 만듦)
하나:
class UserService { User find(Long id); }(직접 클래스를 사용. 나중에 Mock이 필요하면 그때 인터페이스를 뽑아내거나 프레임워크의 기능을 활용함)
Example 2: Data Transformation Hell
민수: UserEntity -> UserVO -> UserDTO -> UserResponse. 데이터가 각 레이어를 지날 때마다 똑같은 필드들을 복사해서 옮긴다.
하나: UserEntity를 필요한 곳까지 직접 전달하거나, 꼭 필요한 경우에만 최소한의 변환을 수행한다. 경로를 단순화하여 필드 추가/삭제의 고통을 줄인다.
Common Pitfalls
"But I need to hide implementation!" (구현을 숨겨야 하는데요!)
구현을 숨기는 목적은 **변경의 영향을 줄이기 위해서**다. 그런데 레이어를 너무 많이 만들어서 오히려 변경할 때마다 모든 레이어를 다 고쳐야 한다면, 당신은 구현을 숨긴 게 아니라 복잡성을 노출한 것이다.
Fear of "Dirty" Controller
"컨트롤러에 로직이 있으면 안 돼!"라는 강박. 하지만 5줄짜리 로직을 위해 새로운 서비스 클래스와 인터페이스를 만드는 것이 더 "지저분한" 결과일 수 있다. 로직이 작다면 그 자리에 두어라. 커지면 그때 옮겨라.
Connection to Other Patterns
OrganicGrowth - 처음엔 DirectPath로 시작하고, 복잡성이 자라나면 레이어를 분화시켜라. 성장 순서
TightLoop - 경로가 짧아야 피드백이 빨라진다. 상호 작용
CleanIsolation - 격리를 위해 레이어를 만들지만, 그 레이어가 흐름을 방해해서는 안 된다. 균형
DataAsFoundation - 데이터가 흐르는 길을 깨끗하게 닦는 것이 DirectPath다. 목표
Signs of Success
- 전체 코드의 파일 개수가 줄어든다.
- 기능을 수정할 때 수정해야 하는 파일의 범위가 좁아진다.
- 코드를 처음 보는 사람이 "아, 여기서 데이터가 와서 저기로 가는구나"를 1분 안에 파악한다.
- 불필요한 보일러플레이트(Boilerplate) 코드가 사라진다.
The Ultimate Insight
가장 완벽한 설계는 더 보태 것이 없을 때가 아니라, 더 뺄 것이 없을 때 완성된다. (생텍쥐페리)
추상화라는 미로를 만드느라 에너지를 낭비하지 마라. 문제와 해결책을 잇는 가장 짧고 투명한 길을 만들어라. 그 길 위에서 코드는 가장 강력한 힘을 발휘한다.
CategoryPatternLanguage CategoryProgramming CategoryDesign CategorySimplicity
