레이어는 논리적으로, 패키지는 응집도 있게
모듈, 패키지와 아키텍처 레이어는 서로 다른 문제를 풉니다. 관련 동작은 가까이 두고 레이어는 기능을 흩뜨리지 않은 채 역할을 설명해야 합니다.
출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다.
gpt-5.6-sol모델을 사용해 생성·편집했습니다.
모듈 안에 아키텍처 레이어별 패키지를 하나씩 만들 필요는 없습니다. 프레젠테이션, 비즈니스, 구현과 데이터 접근 역할이 한 모듈에 함께 있을 수 있고, 디렉터리 트리에서 그 이름을 그대로 반복하지 않아도 됩니다.
제가 계속 구분해서 말하는 내용과 이어집니다. 모듈, 패키지와 레이어는 관계가 있지만 같은 구조가 아닙니다.
모듈은 물리적인 빌드 경계를 만들고 의존성을 제한할 수 있습니다. 패키지는 코드를 정리하고 가까움과 가시성을 다룹니다. 레이어는 애플리케이션 흐름에서 맡은 논리적 역할을 설명합니다. 셋을 같은 모양으로 강제하면 아키텍처 그림은 깔끔해 보여도 함께 바뀌는 코드는 찾기 어려워질 수 있습니다.
레이어 패키지가 없어도 레이어는 존재한다
코어 도메인 모듈 안의 Q&A 기능을 생각해 보겠습니다.
Q&A controller는 프레젠테이션 역할을 합니다. Q&A service는 비즈니스 유즈케이스를 조정합니다. finder, answer updater, remover, appender는 더 작은 구현 책임을 맡을 수 있습니다. repository 인터페이스와 구현체는 데이터 접근에 참여하고, 구현체는 별도 인프라 모듈에 있을 수도 있습니다.
클래스가 서로 다른 일을 하고 호출 흐름이 그 역할을 지나가기 때문에 레이어는 존재합니다. 아래와 같은 패키지가 있어야만 존재하는 것은 아닙니다.
presentation/
business/
implementation/
dataaccess/
대신 Q&A 클래스들을 자신이 지원하는 Q&A 동작 가까이에 둘 수 있습니다. 책임, 의존성 방향, 이름과 코드의 형태에서 레이어가 보이면 됩니다.
아키텍처가 디렉터리 이름보다 크기 때문에 이 차이가 중요합니다. business 아래에 클래스를 놓는다고 좋은 비즈니스 로직이 되지 않습니다. repository를 dataaccess로 옮긴다고 깨끗한 의존성 방향이 생기지도 않습니다. 설계는 구현이 품어야 합니다.
원하는 응집을 기준으로 패키징하기
QuestionService가 QuestionFinder, AnswerUpdater, QuestionRemover를 필요로 한다고 해보겠습니다. 역할마다 전역 레이어 패키지에 두면 service를 열었을 때 디렉터리 트리의 먼 가지에서 import가 들어옵니다. 하나의 기능에 속한 코드인데 위치상 무관해 보입니다.
저는 이를 강서구와 강남구에 사람을 나눈 뒤 둘 다 서울 주소니까 가깝다고 말하는 것에 비유합니다. 디렉터리 이름은 같은 아키텍처 개념을 공유할 수 있지만 매일 협력하는 클래스는 멀어집니다.
기능에만 쓰이는 협력 객체를 service 근처에 두면 관계가 한곳에서 보입니다. package-private 가시성을 이용해 모듈의 다른 코드가 호출할 대상을 줄일 수도 있습니다. 이동 횟수가 줄고, Q&A 동작을 바꾸는 사람이 모든 레이어 디렉터리를 돌아다니지 않아도 주변 책임을 볼 수 있습니다.
정해진 기능 패키지 템플릿 하나를 강요하려는 것은 아닙니다. 기준은 응집입니다. 같은 동작을 위해 존재하고 함께 바뀌는 코드는 대체로 가까이 있어야 합니다. 넓게 재사용하는 데이터 접근 구현이나 인프라 관심사는 다른 곳에 속할 수 있습니다. 패키지 경계는 그림을 만족시키는 대신 관계를 설명해야 합니다.
더 강한 경계가 필요할 때만 모듈 사용하기
같은 프로젝트에서 repository 인터페이스는 도메인 동작 가까이에 두고 기술 구현은 다른 모듈에 놓을 수 있습니다. 이는 패키지보다 강한 분리입니다. 빌드가 도메인 모듈의 인프라 구현 import를 막으면서 실행 애플리케이션은 둘을 조립하게 할 수 있습니다.
여기서도 레이어와 모듈은 일대일로 대응하지 않습니다. 한 모듈에 여러 논리 레이어가 있을 수 있습니다. 데이터 접근처럼 하나의 레이어가 한 모듈의 인터페이스와 다른 모듈의 구현으로 나뉠 수도 있습니다. 강제하려는 제약에 따라 구조가 달라집니다.
그림에 레이어가 네 개 있다는 이유만으로 네 모듈을 만들면 그 경계가 유용한지 확인하지 않은 채 빌드 경계만 추가합니다. 반대로 모듈 하나에 코드를 두어도 선명한 레이어 흐름을 만들 수 있습니다. 패키지 구성만으로 충분히 강하게 전달하기 어려운 규칙을 컴파일이나 런타임 조립이 강제해야 할 때 모듈을 쓰면 됩니다.
팀에 필요할 때만 역할을 더 드러내기
그래도 코드에서 레이어 역할이 바로 보이길 원하는 팀이 있습니다. 이름 규칙이 일정하지 않거나 온보딩 때 같은 혼란이 반복된다면 실제 필요일 수 있습니다.
먼저 책임과 패키지 응집을 분명하게 만들겠습니다. 그래도 신호가 더 필요하다고 팀이 합의한다면 팀에서 정한 annotation으로 역할을 표시할 수 있습니다. 모든 클래스를 전역 레이어 패키지로 옮기지 않고도 의도를 보여줍니다. 다만 이것이 제 기본 추천은 아닙니다. 리뷰나 도구에서 아무도 사용하지 않는 annotation은 장식이 됩니다.
사람들이 어느 클래스가 무엇을 호출해도 되는지 계속 혼동하거나, 같은 경계를 반복해서 넘거나, 특정 역할을 안정적으로 찾아야 하는 문제에서 선택해야 합니다. 그 문제를 푸는 가장 비용이 작은 신호를 고르면 됩니다.
하나의 폴더 트리에 모든 설계 차원을 표현시키지 마십시오. 모듈은 선택한 물리 경계를 강제하고, 패키지는 관련 코드를 가까이 두며, 레이어는 그 코드를 지나는 논리 책임을 설명하게 하십시오. 각 도구가 자기 일을 하면 아키텍처 그림을 파일시스템으로 복제하지 않아도 프로젝트를 이해하기 쉽고 응집도 있게 만들 수 있습니다.