지금까지 여느 글과 마찬가지로, 이번 글도 내가 개발하면서 새롭게 깨달은 부분들을 정리해두려는 기록이다. 이번 주제는 패키지 구조이다.
많은 프로젝트가 그렇듯, 나 역시 자연스럽게 기능 기반 패키지 구조를 따라왔다. 기능에 따라 패키지를 나누는 구조는 익숙했고, 익숙한 만큼 별다른 의심 없이 따랐다. 어느새 ‘이게 정답이겠지’라는 관성에 젖어 있었다.
그러던 중 우아한테크코스 미션을 진행하면서 받았던 패키지 구조에 대한 코드리뷰는 생각보다 큰 충격이었다. 그동안 내가 얼마나 무의식적으로 패키지를 나눠왔는지, 그것이 정말 문제 해결에 적절한 구조였는지를 돌아보게 만든 계기였다.
그 이후로 진행 중인 프로젝트에서는 의식적으로 다른 방향을 선택했다. 기능이 아닌 계층 기반 아키텍처를 적용하자고 제안했고, 다행히도 그 제안안에 설득 당해준 팀원들과 함께 실험을 이어가고 있다.
현재 프로젝트의 패키지 구조는 다음과 같다.
com.ahmadda
├── application
│ ├── EventGuestService.java
│ ├── EventNotificationService.java
│ ├── LoginService.java
│ └── ...
│
├── domain
│ ├── Event.java
│ ├── EventEmailPayload.java
│ ├── EventNotification.java
│ ├── Member.java
│ ├── Organization.java
│ ├── Period.java
│ ├── ...
│ └── exception
│ ├── BusinessRuleViolatedException.java
│ ├── UnauthorizedOperationException.java
│ ├── BlankPropertyException.java
│ ├── NullPropertyException.java
│ └── ...
│
├── infra
│ ├── Event.java
│ └── exception
│ ├── BusinessRuleViolatedException.java
│ ├── UnauthorizedOperationException.java
│ ├── BlankPropertyException.java
│ ├── NullPropertyException.java
│ └── ...
│
├── presentation
│ ├── EventController.java
│ ├── OrganizationController.java
│ ├── LoginController.java
│ └── ...
해당 구조의 단점은 명확하다. 패키지 구조만 봐서는 기능을 유추할 수 있는 힌트가 거의 없다. 때문에, 한눈에 어떤 기능이 어디 있는지 파악하기 어렵고, 처음 진입한 사람은 꽤나 헤맬 수 있다.
하지만 이 단점이 오히려 장점이 되었다. 구조가 친절하지 않기 때문에, 우리는 서로의 코드를 더 많이 읽게 되었고, 자연스럽게 서로의 맥락을 이해하게 되었다. "이건 누구 코드지?"보다는 "이건 왜 이렇게 만들었을까?"라는 질문이 먼저 나왔다. 그런 질문들이 오가다 보니, 팀 내에 '버스 팩터'가 거의 존재하지 않게 되었다. 실제로 최근 트래픽이 몰리는 이벤트 대응 상황에서도, 누가 없어서 업무가 마비되는 일은 없었다.
또한, 패키지 구조가 기능에 고정되어 있지 않다 보니, 오히려 구조 개선이나 리팩토링을 더 과감하게 시도할 수 있었다. 기능 기반 구조였다면, 패키지 경계가 곧 책임의 경계로 굳어져 구조를 바꾸는 데 더 큰 심리적 부담이 따랐을 것이다.
하지만 지금의 구조는 초기 진입장벽만 넘어서면, 모두가 코드의 흐름과 맥락을 자연스럽게 이해하게 되고, 어색한 부분이 보이면 주저 없이 리팩토링하거나, 더 나은 구조를 제안하는 일이 일상이 되었다. 그 결과, 구조 개선은 특정 인물의 역할이 아닌 팀 전체의 자연스러운 행동으로 자리잡았다.
그리고 우리는 계층 구조를 따르면서도 공통 패키지는 의도적으로 배제했다. 흔히 사용하는 common, global 같은 디렉터리는 만들지 않았고, 대신 각 클래스의 책임과 사용 범위에 따라 위치를 명확히 정하는 방식을 택했다.
덕분에 exception, dto처럼 계층 간에 공유가 필요한 클래스들을 어디에 둘지에 대한 실제적인 고민이 생겼고, 그 고민 자체가 꽤 신선한 경험이었다. 예전에는 별다른 기준 없이 “공통이니까 common에 넣자”는 식의 습관적인 선택이 많았다면, 이번에는 클래스의 책임이 어느 계층에 더 가까운지, 어디까지 사용될지를 명확히 따져본 뒤 위치를 결정했다.
그 과정에서 exception, dto 같은 클래스를 common, global에 두는 선택이 어떤 트레이드오프를 전제로 한 결정인지 처음으로 체감할 수 있었다. 그리고 더 나아가, 단순히 “공유되니까”라는 이유만으로 아무 데나 모아두는 구조가 얼마나 쉽게 설계의 균형을 무너뜨릴 수 있는지도 자연스럽게 깨닫게 되었다.
지금의 선택이 언제까지 유효할지는 모르겠다. 하지만 적어도 지금 이 순간만큼은, 이 구조 덕분에 더 많이 고민하고, 더 깊이 이해하고, 더 잘 협업하고 있다고 느낀다.
물론 효율만 놓고 보면 지금의 구조는 분명 불편하다. 실제로 우리 팀만이 계층형 구조를 선택했고, 나머지 대부분의 팀들은 기능 기반 구조를 택했다. 그럼에도 불구하고 ‘학습’이라는 다소 낭만적인 이유 하나로 이 구조를 받아들여준 팀원들에게 진심으로 감사하다.
그리고 기능 기반 패키지 구조를 부정하려는 것은 아니다. 아마 언젠가는 전환하게 될 것이다. 다만 그 시점은, 팀 내에서 불편함이 명확해졌을 때거나 이 구조에서 얻을 수 있는 학습이 충분하다고 판단될 때로 남겨두고 싶다.
결국 좋은 아키텍처란, 특정 패턴이나 규칙을 따르는 것이 아니라 팀원 모두가 이해할 수 있고 함께 발전시킬 수 있는 구조라고 생각한다. 나중에 또 팀 내부에서 불편함을 느낄 때가 온다면, 그때는 그 불편함을 기준으로 다시 구조를 조정하면 된다고 감히 생각해본다.
계층 기반 아키텍처의 구조가 오히려 버스 팩터를 낮추는 결과로 이어졌다는 지점이 흥미롭네요!
전환 시점을 유연하게 가져가는 관점도 인상 깊었습니다~