프로필

데브고래밥

@devgoraebap

스택오버플로우의 단골손님이였던 Claude를 채찍질하는 개발자

Album Art

0:00 0:00
방문자 정보

요즘 관심있는

패키지 설계에 대한 토론 thumbnail image
95
0
#architecture

패키지 설계에 대한 토론

이전 글에서 패키지의 점진적인 개선에 대해 다뤘다. 이후 다른 개발자들과 이 주제로 많은 토론을 했는데, 좋은 내용들이 오가서 정리해보려고 한다.

필자가 중요시했던 핵심은 물리적인 패키지 간의 관계다. 최근 Spring Boot로 프로젝트를 진행하고 있으니, 패키지를 대충 표현하면 다음과 같다.

com.example.demo/
├── common     // 프레임워크 관련 전역 기능 (도메인 참조 가능 - 최상위 레이어)
├── app        // 도메인 기능의 상위 레이어 (Presentation + Application)
├── features   // 재사용되는 Application 로직
├── infra      // 도메인과 밀접하지만 외부 기술에 가까운 로직 (DB, 외부 API 등)
├── domain     // 도메인 핵심 기능 (Entity, Domain Model, Repository 인터페이스 등)
└── shared     // 도메인 외적 관심사 (Config, Utils, Constant, 서드파티 등)

Spring은 설정을 중앙화하는 경향이 있어서, 아직 프레임워크 관련 기능이 역으로 app 레이어를 참조하는 사례를 발견하지 못해 common이 최상위에 올라간 모습이다.

받은 비판적 피드백

다른 개발자들과 토론하면서 받은 비판을 정리하면 다음과 같다:

1. 도메인 기능이 여러 레이어에 나뉘어 있다

"도메인들이 레이어별로 먼저 분리되어 기능이 여러 곳에 흩어져 있는 게 마음에 들지 않는다."

이건 매우 타당한 반박이다. 필자의 프로젝트와는 반대로 도메인을 먼저 구성하고 내부적으로 계층 구조를 나타내는 방식을 사용하면 되지 않냐는 의견인 것 같다. 일리가 있다. 실제로 필자의 구조는 핵심 비즈니스 로직과 오케스트레이션 로직이 레이어별로 나뉘어 있기 때문에, 한 도메인의 전체 흐름을 파악하려면 여러 레이어를 오가며 봐야 한다는 점이 있다. 다만, 다음 내용까지 본 다음 한 번에 뭐가 문제가 되는지 나타내려고 한다.

2. DTO는 어디에 위치해야 하나?

"DTO가 app 레이어 말고도 featuresinfra에 개별적으로 위치하는데, API에 관련된 DTO는 Presentation 레이어 한 곳에 모여있는 게 맞지 않나? 이건 API의 책임 아닌가?"

필자는 이 의견에 매우 긍정한다.

DTO는 비즈니스 로직이 없는 순수한 데이터 계약이다. API 명세에 관련된 내용이니까 Presentation에서 이 DTO들을 일괄 관리하고, 다른 레이어들이 Presentation을 바라보는 게 맞지 않냐는 의도는 타당하다.

패키지 설계에서 무엇을 우선시할 것인가?

이제 이 의견들에 대한 답을 내놓자면, 패키지 설계 시 뭐를 우선적으로 판단하냐가 가장 중요할 것 같다.

Claude와 이야기해보니 이 현상을 표현하는 좋은 말을 찾았다:

필자의 접근: 물리적 의존성 우선

필자는 패키지의 물리적인 참조 규칙을 중요시한다. 이것을 기본이 되는 원칙으로 가져갔기 때문에, 패키지가 구성되는 방식에도 이 이유가 적용됨에 따라 어느 정도의 모범답안이 만들어지는 것이다.

현재 필자의 프로젝트에서는:

  • featuresapp보다 하위 레이어이므로 app을 참조할 수 없다
  • infraapp보다 하위 레이어이므로 app을 참조할 수 없다

따라서 app에서 DTO를 일괄 관리하게 되면 이 레이어 참조 규칙을 어기기 때문에, 자연스럽게 해당 레이어의 각 패키지들이 자신의 기능이 요구하는 명세(DTO)를 자체적으로 가지게 된다.

반대 의견: 개념적 의존성 우선

상반대는 의견도 존중하지만, 그 부분을 만족하려면 우선적인 판단 기준을 개념적 의존성으로 가져가야 한다.

예를 들어 DTO를 Presentation에 두는 설계는 DTO를 'API 계약'이라는 안정적인 추상으로 보고, Infrastructure가 이에 의존하는 클린 아키텍처의 사상을 반영한 것이라고 생각한다.

  • DTO는 API 계약이므로 Presentation에 위치한다
  • Infrastructure는 Presentation의 DTO를 참조한다
  • 개념적으로 Infrastructure가 상위 레이어(Presentation)에 의존하게 된다

어느 쪽이 맞는가?

둘 다 맞다. 다만 무엇을 우선시하느냐에 따라 설계가 달라질 뿐이다.

필자는 물리적 참조 규칙을 우선시하여 패키지 간 의존성을 명확하게 관리하는 것을 선택했다. 이 방식은 각 레이어가 독립적으로 유지되지만, DTO가 여러 곳에 분산되는 단점이 있다.

반대 의견은 개념적 의존성을 우선시하여 각 요소가 논리적으로 적절한 위치에 있는 것을 선택한 것이다. 이 방식은 DTO가 한 곳에 모이는 장점이 있지만, Infrastructure가 상위 레이어를 참조하는 구조가 된다.

결국 프로젝트의 특성과 팀의 우선순위에 따라 선택하면 된다. 정답은 없다. 다만 선택한 원칙을 일관되게 적용하는 것이 중요하다.

하지만 어떤 방식을 선택하든, 궁극적으로 계층을 먼저 나타내고 내부적으로 도메인을 표현하는 구조를 사용하던, 도메인을 먼저 나타내고 내부적으로 계층을 표현하는 구조를 사용하던, 양보할 수 없는 한 가지가 있다.

프로젝트의 기본적인 구조는 무조건 프레임워크/애플리케이션 관심사와 도메인 관심사를 나누는 것으로 시작해야 한다는 것이다.

예를 들어:

com.example.demo/
├── app      // 도메인 관심사 - 비즈니스 로직과 도메인 기능
└── shared   // 도메인 외적 관심사 - 프레임워크 설정, 유틸, 공통 기능

프로젝트의 루트 패키지에는 도메인과 도메인 외의 것이 명확히 나뉘어야 한다. 이것이 지켜지지 않으면, 비즈니스 로직과 기술적 관심사가 뒤섞여 프로젝트가 복잡해지고 유지보수가 어려워진다.

계층 우선이든 도메인 우선이든, 이 기본 원칙만큼은 반드시 지켜져야 한다고 생각한다.

물리적 참조 규칙을 우선하는 방식

그렇다면, '물리적 참조 규칙'을 최우선 원칙으로 삼는 방식에 대해 더 깊이 파고들어 보자.

이 원칙을 따르다 보면, 필연적으로 하나의 질문에 도달하게 된다. "서로 다른 도메인 패키지들은 어떻게 협력해야 하는가?"

단순히 "도메인 A는 도메인 B를 참조할 수 있다"와 같은 규칙을 만드는 것만으로는 부족하다. 현실의 요구사항은 우리가 그어놓은 깔끔한 경계를 쉽게 넘어오기 때문이다.

이 불일치는 특히 Command와 Query의 차이에서 극명하게 드러난다. 

Command(생성, 수정, 삭제)는 대부분 우리가 설계한 단일 도메인의 경계 안에서 깔끔하게 처리된다. 하지만 Query(조회)는 다르다. UI는 도메인 모델과 1:1로 매칭되지 않으며, 화면 하나를 구성하기 위해 여러 도메인의 데이터를 조합해야 하는 경우가 비일비재하다.

예를 들어, "주문 목록과 각 주문의 결제 상태, 배송 진행 상황"을 한 화면에 보여줘야 한다면, 이 조회 로직은 Order, Payment, Delivery 중 어느 도메인에 속해야 할까? 어느 곳에 두어도 어색하고, 다른 도메인에 대한 과도한 의존성을 만들게 된다.

이런 문제가 발생하는 근본적인 이유는, 대부분의 아키텍처 예제가 데이터를 변경하는 Use Case(Command)는 잘 다루지만, 실제 프로젝트에서 훨씬 더 빈번하고 복잡하게 나타나는 조회(Query)에 대해서는 충분한 가이드를 제공하지 않기 때문이다.

이런 애매한 상황이 늘어날수록, 'A는 B를 참조할 수 있다' 같은 수평적 규칙은 점점 더 많은 예외 조항을 필요로 하게 된다. 결국 규칙을 해석하고 적용하는 데 드는 인지적 비용이 커지고, 설계의
일관성을 유지하기 어려워진다.

그래서 우리는 자연스럽게 다른 해결책을 모색하게 된다. 도메인 간의 복잡한 수평적 참조 규칙을 관리하는 대신, '상위 레이어는 하위 레이어를 참조할 수 있다'는 단순하고 명료한 수직적 규칙으로
전환하는 것이다. 복잡한 의존성 문제를 고민하는 대신, 그 복잡성을 다룰 수 있는 하위 레이어를 만들어 책임을 위임하고 경계를 명확히 긋는 방식이다.

결론적으로, '물리적 참조 규칙'을 우선하는 접근법은 필연적으로 레이어(Layer)라는 명확한 경계를 통해 도메인 간의 복잡성을 단순화하는 방향으로 진화한다. 이는 팀 전체가 쉽게 이해하고
일관되게 적용할 수 있는 강력한 구조적 해법이 된다.

개념적 의존성을 우선하는 방식

반면, 아키텍처의 개념적 규칙을 우선하게 되면 패키지 구조에 대한 접근이 좀 더 유연해진다. 물리적인 import 방향보다는, 우리가 채택한 아키텍처의 사상을 패키지 구조에 얼마나 잘 녹여냈는지를
더 중요하게 여기는 관점이다. 예를 들어 인터페이스를 통해 도메인 간 의존성을 역전시켰다면, 패키지 레벨에서 발생하는 일부 순환 참조는 큰 문제로 보지 않을 수도 있다.

이 방식이 가진 이상적인 장점은 개념적 명확성이다. 각 레이어와 도메인의 역할이 아키텍처 이론과 일치하기 때문에, 명확한 규칙이 존재하지 않아도 팀원들이 공통된 이해를 바탕으로 코드를 작성할 수 있다는 것이다.

하지만 이 접근법의 단점 또한 명확하며, 바로 그 이상이 현실에서 얼마나 어려운지를 보여준다. 아키텍처의 추상적인 개념에 의존하기 때문에 개발자마다 해석이 달라질 여지가 크다. "클린 아키텍처"라는 동일한 목표를 가졌음에도 회사마다, 팀마다 그 구현 방식이 제각각인 이유가 바로 여기에 있다.

결국 이 방식이 성공하려면, 아키텍처의 개념을 각자의 경험이나 신념에 따라 다르게 해석하는 일을 최소화하고, 우리 팀만의 명확한 컨벤션을 세우는 과정이 반드시 필요하다.

Reddit 개발 커뮤니티에서는 오늘도 클린 아키텍처가 완벽한지, 실용적이지 않은지에 대해 열띤 토론을 이어가고 있다.

솔직히 말해, 이 영역은 필자 역시 아직 탐구하고 배워나가는 과정에 있다. 그렇기에 이 방식의 장점을 부각하며 "이런 상황에서는 괜찮다"라고 섣불리 결론 내리기가 어렵다. 아마도 더 많은 경험과 깊은 고민이 쌓인 후에야, 이 유연함 속에서 질서를 찾아내는 나만의 관점을 이야기할 수 있을 것 같다.

마무리

결국 모든 설계는 트레이드오프의 산물이다. 어떤 아키텍처도 모든 상황을 완벽하게 해결해주는 은 탄환(Silver Bullet)은 될 수 없다. 

필자 역시 헥사고날 아키텍처나 클린 아키텍처의 이론적 우아함에 매료되어 데모 프로젝트를 몇 번이고 만들어보곤 했다. 하지만 이론의 완벽함을 실제 프로젝트에 그대로 적용하려 할 때마다, 그 복잡성과 실용성 사이의 괴리감에 부딪히곤 했다.

만약 내가 열렬한 클린 아키텍처의 신봉자였다면, 이전 글에서처럼 '점진적인 패키지 구조 개선' 같은 실용적인 타협안을 제시하지 않았을 것이다. 처음부터 완벽한 구조를 설계하고 그 원칙을 고수하라고 주장했을지도 모른다.

아직 정리되지 않은 생각들이 많아 모든 것을 이 글에 담지는 못했다. 더 많은 프로젝트를 경험하고, 더 깊이 고민하다 보면 언젠가 또 다른 길이 보이리라 믿는다.