개발표준정의서, 특히 패키지 구조에 대한 가이드를 정립하면서 어떤 패키지 구조가 가장 Best Practice일까에 대한 고민이 많았다. 오늘은 패키지 구조에 대해 잘 정리된 글이 있어 이 글을 기반으로 정리해보는 시간을 가져보려 한다.
패키지 구성은 크게 레이어 계층형과 도메인형 이렇게 2가지 유형으로 나눠 생각해볼 수 있다. 각 유형별로 간단하게 알아보고, Best Practice를 고민해보자.
계층형
계층형 구조는 각 계층을 대표하는 디렉터리를 기준으로 코드들이 구성된다.
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── DemoApplication.java
│ │ ├── const
│ │ ├── controller
│ │ ├── domain
│ │ ├── dto
│ │ ├── exception
│ │ ├── mapper
│ │ └── service
│ └── resources
│ └── application.properties
계층형 구조의 장점은 해당 프로젝트에 이해가 상대적으로 낮아도 전체적인 구조를 빠르게 파악할 수 있다는 점이다. 단점으로는 디렉터리에 클래스들이 너무 많이 모이게 된다는 점이 있다.
도메인형
도메인형 구조는 도메인 디렉터리를 기준으로 코드를 구성한다.
─ src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── DemoApplication.java
│ │ ├── coupon
│ │ │ ├── controller
│ │ │ ├── const
│ │ │ ├── domain
│ │ │ ├── dto
│ │ │ ├── exception
│ │ │ ├── mapper
│ │ │ └── service
│ │ ├── member
│ │ │ ├── controller
│ │ │ ├── const
│ │ │ ├── domain
│ │ │ ├── dto
│ │ │ ├── exception
│ │ │ ├── mapper
│ │ │ └── service
│ │ └── order
│ │ ├── controller
│ │ ├── domainconst
│ │ ├── domain
│ │ ├── dto
│ │ ├── exception
│ │ ├── mapper
│ │ └── service
│ └── resources
│ └── application.properties
도메인형의 장점은, 관련된 코드들이 응집해 있다는 점이다. 단점으로는 프로젝트에 대한 이해도가 낮을 경우 전체적인 구조를 파악하기 어렵다는 점이 있다.
개인적인 Best Practices
참조한 포스팅의 필자와 마찬가지로, 나 역시 도메인 중심의 디렉터리 구조가 더 나은 선택이라고 생각한다. 이 구조를 선택하는 데에는 관습적인 요소도 있지만, 최근에는 개발 표준을 정립하면서도 도메인형 디렉터리 구조가 더 효과적이라는 생각을 하고 있다.
복잡성의 증가 : 계층형 구조의 한계
계층형 구조에서는 Controller나 Service 계층에 너무 많은 클래스가 몰리면서, 복잡도가 급격히 상승한다. 프로젝트 초기에는 상위 디렉토리 몇 개만으로 전체적인 구조를 쉽게 파악할 수 있을지 모르지만, 클래스가 30~40개 이상씩 늘어남에 따라 이러한 단순함은 금새 사라진다. 예를 들어 xxxController , xxxService 와 같은 이름으로 길게 나열된 클래스들은 개별적으로는 이해하기 쉬울 수 있으나, 전체 시스템의 맥락에서 이들을 통합적으로 이해하는 것은 매우 어려워진다.
코드 응집의 중요성 : 도메인형 구조의 강점
반면, 도메인형 구조는 관련된 코드들을 하나의 도메인 안에 모아두기 때문에, 이러한 복잡성을 효과적으로 완화할 수 있다. 관련된 코드가 한 곳에 모여있으면, 자연스럽게 연관된 코드 스타일, 변수 이름, 클래스 구조 등을 공유하게 되고, 이는 일관된 코딩 스타일과 패턴을 유지하는 데 큰 도움이 된다. 결과적으로 개발자들이 서로의 코드를 더 쉽게 이해하고 유지 보수할 수 있는 환경이 조성될 수 있다고 생각한다.
.
도메인형 디렉터리 구조
SM처럼 이미 운영 중인 프로젝트에서는 상황에 따라 유연하게 접근하는 것이 필요할 수 있다. 그러나 SI를 진행할 때는 설계 단계에서 패키지 구조를 체계적으로 정립할 시간이 주어지기 때문에, 최적의 방안을 고민할 여지가 더 많아지는 것 같다. 이 과정에서 무엇이 Best Practice인지 깊이 생각해보게 된다.
도메인 중심으로 디렉터리를 구성할 경우, 새로운 고민거리가 생길 수 있다. 바로 config, util, error, common, infra 와 같이, 도메인에 속하지 않은 항목들을 어디에 배치할 지에 대한 문제이다. 도메인 중심 구조라는 큰 틀 안에서는 이러한 요소들을 조금 더 유연하게 구성해도 무방하다고 생각한다.
참조한 포스팅에서는 다음과 같이, 도메인에 속하지 않는 코드를 global, infra 로 나누어 관리하는 방식을 제시하였다.
전체적인 구조
다음은 참조한 포스팅에서 설명하고 있는 구조이다. 직접 해당 포스팅에서 확인해보는 것도 추천한다.
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── spring
│ │ └── guide
│ │ ├── ApiApp.java
│ │ ├── SampleApi.java
│ │ ├── domain
│ │ │ ├── coupon
│ │ │ │ ├── api
│ │ │ │ ├── application
│ │ │ │ ├── dao
│ │ │ │ ├── domain
│ │ │ │ ├── dto
│ │ │ │ └── exception
│ │ │ ├── member
│ │ │ │ ├── api
│ │ │ │ ├── application
│ │ │ │ ├── dao
│ │ │ │ ├── domain
│ │ │ │ ├── dto
│ │ │ │ └── exception
│ │ │ └── model
│ │ │ ├── Address.java
│ │ │ ├── Email.java
│ │ │ └── Name.java
│ │ ├── global
│ │ │ ├── common
│ │ │ │ ├── request
│ │ │ │ └── response
│ │ │ ├── config
│ │ │ │ ├── SwaggerConfig.java
│ │ │ │ ├── properties
│ │ │ │ ├── resttemplate
│ │ │ │ └── security
│ │ │ ├── error
│ │ │ │ ├── ErrorResponse.java
│ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ │ └── exception
│ │ │ └── util
│ │ └── infra
│ │ ├── email
│ │ └── sms
│ │ ├── AmazonSmsClient.java
│ │ ├── SmsClient.java
│ │ └── dto
│ └── resources
│ ├── application-dev.yml
│ ├── application-local.yml
│ ├── application-prod.yml
│ └── application.yml
전체적인 구조는 도메인을 담당하는 디렉터리 domain, 전체적인 설정을 관리하는 global , 외부 인프라를 관리하는 infra를 기준으로 생각해볼 수 있다.
Domain
├── domain
│ ├── member
│ │ ├── api
│ │ │ └── MemberApi.java
│ │ ├── application
│ │ │ ├── MemberProfileService.java
│ │ │ ├── MemberSearchService.java
│ │ │ ├── MemberSignUpRestService.java
│ │ │ └── MemberSignUpService.java
│ │ ├── dao
│ │ │ ├── MemberFindDao.java
│ │ │ ├── MemberPredicateExecutor.java
│ │ │ ├── MemberRepository.java
│ │ │ ├── MemberSupportRepository.java
│ │ │ └── MemberSupportRepositoryImpl.java
│ │ ├── domain
│ │ │ ├── Member.java
│ │ │ └── ReferralCode.java
│ │ ├── dto
│ │ │ ├── MemberExistenceType.java
│ │ │ ├── MemberProfileUpdate.java
│ │ │ ├── MemberResponse.java
│ │ │ └── SignUpRequest.java
│ │ └── exception
│ │ ├── EmailDuplicateException.java
│ │ ├── EmailNotFoundException.java
│ │ └── MemberNotFoundException.java
│ └── model
│ ├── Address.java
│ ├── Email.java
│ └── Name.java
model 디렉터리는 Domain Entity 객체들이 공통적으로 사용할 객체들로 구성된다. 대표적으로 Embeddable 객체, Enum 객체 등이 있다.
infra
└── infra
├── email
└── sms
├── AmazonSmsClient.java
├── KtSmsClient.java
├── SmsClient.java
└── dto
└── SmsRequest.java
infra 디렉터리는 infrastructure와 관련된 코드들로 구성된다. infrastructure는 대표적으로 이메일 알림, SMS 알림 등 외부 서비스에 대한 코드들이 존재한다.
Reference