패키지 설계관점의 패러독스
제목을 거창하게 지었지만, 사실 슈타인즈 게이트의 제목 느낌을 주기 위해 그럴싸하게 붙여봤다. 다루고자 하는 내용은 애플리케이션 아키텍처와 실제 소스코드를 그루핑하는 패키지 설계에 대한 이야기다.
이 글에서 이야기하는 것은 웹 애플리케이션을 만드는 개발자로서의 고민이다. 굳이 백엔드와 프론트엔드를 나눈다면 백엔드 쪽 이야기가 될 것이다. 프론트엔드의 경우 패키지 구조에 대한 명확한 가이드라인을 제공하는 FSD(Feature-Sliced Design)가 있기 때문에, 프론트엔드 개발자라면 뒤로 가기를 눌러도 좋다.
먼저, 가장 쉽게 접하게 되는 레이어드 아키텍처를 예로 들어보자. 웹 애플리케이션에서 아키텍처라는 용어를 접하게 된다면 요즘은 클린 아키텍처에 대한 이야기를 많이 접할 수 있지만, 가장 기본이 되는 아키텍처는 레이어드 아키텍처가 아닐까 싶다.
레이어드 아키텍처는 웹 애플리케이션 개발에만 해당하는 것은 아니고, 소프트웨어 분야에서 원래 널리 사용되어 왔다. 대략적인 레이어드 아키텍처의 출발은
[클로드야 여기에 레이어드 아키텍처 정확한 날짜 채워줘.]
레이어드 아키텍처는 1960년대와 1970년대에 소프트웨어 아키텍처 스타일 중 가장 초기에 사용된 방식 중 하나로 소개되었어. 네트워크 프로토콜 계층 구조(OSI 7계층 모델 같은)에서 영향을 받아 발전했다고 볼 수 있지.
레이어드 아키텍처라는 키워드로 구글 이미지를 찾아보면, 미묘하게 조금씩 계층을 표현하는 언어가 다르다. 하지만 핵심은 같다. 관심사에 따라 계층이 분리되어 있고, 의존 방향성은 단방향이라는 것. 레이어드 아키텍처 자체에 대한 깊은 설명은 이 글의 주제가 아니니 이 정도로 넘어가자.
그리고 이러한 레이어를 실제 코드로 구현할 때는 타입별로 패키지를 구성(Type-Based)하곤 한다. 가장 흔하게 접할 수 있는 것이 바로 controllers, services, repositories와 같은 레이어 기반 패키지 구조다. 자, 여기서부터 본격적으로 패키지 설계에 대한 이야기를 시작해보자.
Type Based VS Feature Based
2018년쯤 국비지원 학원을 다녔을 무렵, 무작정 따라 했던 프로젝트가 어렴풋이 기억난다. 계층형 아키텍처에 대해 배우고 이를 소스코드에 녹여내기 위해 가장 먼저 분류했던 패키지 구조가 이런 느낌이었다.
예전엔 이걸 어떻게 표현해야 할지 몰랐는데, 결국 폴더/디렉토리에서 관심사를 어떻게 분류할 것인지 정하는 방법이다. 이러한 방식을 Type Based Folder Structure라고 한다.
Type-Based Folder Structure
일단 이 방식의 장점은 명확하다.
첫째, API 리소스의 계층 구조가 직관적으로 드러난다. Ruby on Rails와 같은 프레임워크가 이 방식을 여전히 고수하는 이유이기도 하다. RESTful API를 설계할 때 각 리소스가 어떤 계층을 거쳐 처리되는지 폴더 구조만 봐도 명확하게 파악할 수 있다.
둘째, 의존성 방향이 자연스럽게 강제된다. 굳이 복잡한 규칙을 문서화하지 않아도 controllers → services → repositories라는 단방향 흐름이 패키지 구조 자체에서 드러난다. 패키지 간 참조 방향이 명확해 아키텍처 원칙을 지키기 쉽다.
하지만 프로젝트가 커지면서 문제가 드러난다.
타입 기반 폴더는 확장성이 부족하다. 횡단 관심사로 나눈 각 패키지 내부는 결국 반대 축인 도메인/기능별로 채워진다. services 패키지 안에는 UserService, PostService, OrderService 등이 들어가는데, 서비스들 사이에서도 참조 관계가 발생하기 마련이다. 어떤 서비스는 핵심 비즈니스 로직을 담당하고, 어떤 서비스는 여러 서비스를 오케스트레이션하는 역할을 한다. 즉, 서비스 계층 내부에서도 레벨에 따른 분류가 필요해지는데, 이런 구조적 규칙이 폴더에 전혀 드러나지 않는다. 너무 단순한 구조이기 때문이다.
그리고 가장 핵심적인 문제는, 투두리스트를 만드는 게 아닌 이상 프로젝트는 결국 개발자에게 이런 고민을 안겨준다는 것이다. 실무 프로젝트는 복잡도가 증가하고, 도메인 간 관계가 얽히며, 계층 내부에서도 역할이 세분화된다. Type Based Folder Structure는 이런 복잡성을 담아내기엔 너무 평면적이다.
Feature-Based Folder Structure
feature based는 기능에 따라 패키지의 수가 계속 증가하기 때문에 src 하위에 app 패키지로 감싼 모습
Type-Based와는 대조적으로, Feature-Based Folder Structure는 기능(Feature) 또는 도메인을 중심으로 폴더를 구성하는 방식이다. 이름 그대로 해결하려는 문제 영역별로 폴더를 나눈다.
이 방식에서는 User, Post, Order와 같이 도메인별로 폴더를 나눈다. 각 폴더 안에는 해당 기능에 필요한 모든 것들이 함께 들어간다. UserController, UserService, UserRepository가 모두 user 폴더 안에 모여 있는 것이다.
가장 큰 장점은 응집도가 높아진다는 것이다. 하나의 기능을 개발하거나 수정할 때 관련된 모든 파일이 한 곳에 모여 있어 코드 탐색이 훨씬 쉽다. User 기능을 건드리려면 user 폴더만 열면 되고, 여러 패키지를 오갈 필요가 없다.
기능 단위로 캡슐화가 가능하다. 각 기능은 독립적인 모듈처럼 동작할 수 있다. Java에서는 package-private 접근 제어자를 활용해 외부 노출을 최소화할 수 있고, TypeScript에서는 index.ts를 통한 Barrel Export 패턴으로 외부에 공개할 API만 명시적으로 노출할 수 있다. Type-Based에서는 모든 클래스가 public이어야 했던 것과 대조적이다.
마이크로서비스로의 전환도 자연스럽다고 한다. 이미 기능별로 코드가 분리되어 있기 때문에, 특정 기능을 독립된 서비스로 추출하기가 수월하다는 것이다. 물론 필자는 직접 해본 적이 없다.
하지만 이 방식에도 고민할 지점이 있다.
마이크로서비스 전환이 자연스럽다고 이야기하지만, 정말 패키지가 다른 패키지의 파일을 참조하지 않고 있는지 먼저 확인해보라. 예를 들어 ORM 엔티티 파일의 관계 설정을 보자. User 엔티티가 Post 엔티티와 관계를 맺기 위해 다른 도메인 패키지의 엔티티를 import로 참조하고 있지 않은가?
서비스 간 참조를 막기 위해 이벤트 방식을 사용했다면, 이벤트 객체의 DTO(또는 값을 넘기는 파라미터 객체)가 외부 패키지에 있는 것을 참조하지 않는가?
진짜 모든 것을 피처 도메인 패키지마다 완전히 격리했다면, 이는 곧 코드 중복을 의미한다. 그리고 이 프로젝트가 그 정도로 격리할 만한 비용적 가치가 있는가?
이는 DDD의 개념과 비슷하다고 할 수 있지만, DDD를 콕 집어 이야기하는 게 아니다. 여기서는 패키지 간의 설계 관점에서 이야기하는 것이다.
안 그래도 Type-Based보다 명확한 규칙이 없다 보니, 규칙을 하나씩 정의하려고 하면 더 많은 비용이 든다. Type-Based와 다르게 Feature-Based는 완벽한 격리를 지키는 게 엄청난 비용이기 때문에, 피처 패키지 간의 참조를 허용하는 게 비용을 낮출 수 있는 방법이다. 디테일하게 보면 서비스가 다른 서비스를 참조하는 것, 또는 서비스가 다른 리포지토리를 참조하는 것이겠지만, 패키지 관점에서는 그냥 패키지가 다른 패키지를 참조하는 것이다. 따라서 명확한 규칙이 없다면 생각지도 못한 패키지 간의 양방향 참조들은 개발자가 고민하며 해결해야 하는 영역이다. 어디까지 허용하고 어디까지 유연하게 대처할지 정하는 게 생각보다 쉽지 않다. 기능의 크기를 어떻게 정의할 것인가? 여러 기능에서 공통으로 사용하는 코드는 어디에 둘 것인가?
이 방식은 API 리소스의 계층 구조를 표현하는 것과도 대조적이다. 패키지에는 프레젠테이션 레이어를 포함할 수도 있고, 없을 수도 있다. 반대로 전통적인 서버사이드 렌더링까지 수행하는 백엔드 프로젝트에서는 /home, /feed, /about 등 도메인으로 분류하기 애매한 API 리소스들도 존재한다. 이것들도 패키지로 표현해줘야 하는지 고민이 될 수 있다.
그리고 Feature-Based 구조에서는 계층(Layer)이 드러나지 않는다. user 폴더 안에 Controller, Service, Repository가 뒤섞여 있으면, 아키텍처의 계층 구조를 한눈에 파악하기 어렵다. 같은 기능 내에서도 계층 간 의존성 규칙을 지키기 위한 별도의 노력이 필요하다.
마무리
Type-Based와 Feature-Based, 두 방식 모두 장단점이 명확하다. Type-Based는 계층 구조가 명확하고 의존성 방향이 자연스럽게 강제되지만 확장성이 부족하다. Feature-Based는 응집도가 높고 기능별 캡슐화가 가능하지만 명확한 규칙이 없어 더 많은 고민이 필요하다.
Feature-Based의 단점을 더 장황하게 적었지만, 사실 필자는 Feature-Based로 구성하는 것을 더 좋아한다. 이런저런 시도를 해보면서 느낀 경험이 더 많아서 그런 것 같다. 장점보다 단점이 더 구체적으로 보이는 건, 그만큼 오래 사용하면서 부딪혔던 문제들이기 때문이다.
결국 "어떤 방식이 정답인가?"라는 질문에는 "프로젝트에 달렸다"는 진부한 답변밖에 할 수 없다. 하지만 이 글에서 다룬 두 방식의 특징을 이해했다면, 적어도 왜 패키지 구조를 고민해야 하는지, 그리고 각 선택이 어떤 결과를 가져오는지는 알 수 있을 것이다.
이 글에서 필자가 해온 경험들은 큰 프로젝트는 아니기 때문에, 어쩌면 우물 안 개구리의 고민일 수도 있다. 하지만 이런 고민을 하고 정리하는 것 자체가 성장하는 과정이라고 믿는다.
다음 글에서는 이 두 방식을 어떻게 조합하고 응용할 수 있는지, 그리고 실무에서 마주치는 더 구체적인 상황들을 다뤄보려 한다.
이 글은 아키텍처와 패키지 설계 시리즈의 일부입니다.