DDD 프로젝트 구조
in Web-Programming on Spring
스프링 프로젝트 구조
스프링 프로젝트의 구성 요소를 나열하자면, Controller, Service, Repository, Dto, Config, Model 등이 있다. 그렇다면 이 구성요소들 관리하기 위해 스프링 프로젝트의 폴더 구조를 어떻게 구상해야 잘 관리할 수 있다고 할까?
계층별로 모아보기
최근에 한 팀 프로젝트를 설계하고 개발하면서 정해진 구조는 계층
별 구조였다.
Controller는 Controller끼리, Service는 Service끼리 계층별로 묶어서 구조를 짠 아주 단순한 구조가 되시겠다. 이 단순한 구조로 짠 구성은 빠르게 구조를 파악할 수 있게 만든다. 처음 스프링 프로젝트를 접해본 분이시라면, 가장 단순하게 짜는 방법이자 쉬운 방법이라 할 수 있겠다.
그러나 단순하다 해서 좋은 것만은 아니다. 한 계층 패키지에 수많은 클래스들이 모여있을 수도 있고, 이에 대한 거부감이 들 수도 있다. 또한 추가적인 요구사항이 오면 각 계층의 코드들이 점점 비대해질 것이다. 그리고 모놀리식 구조에서 마이크로서비스 구조로 변형되어 도메인 또는 서비스 별로 쪼개져야 할 때 어려움이 있을 것이다.
도메인별로 모아보기
흐음… 그러면 어떻게 하는 방법이 있는가? 지금까지 스프링 계층 구조에 집중을 했다면, 이제부터는 스프링 프로젝트의 대상, 도메인에 집중해본다. 스프링 프로젝트 이전에 DB를 설계하다보면 요구사항을 기반으로 도메인이 정해지고 그 도메인 기준으로 테이블이 짜여질 것이다. 도메인이 어떻게 등록하고 수정되고 읽어지고 삭제되는 지에 대한 로직들을 계층별로 보는 것이 아니라 순수 도메인 별로 모아보는 방식이 바로 DDD이다.
DDD, 풀으면 Domain Driven Designed이다. 말 그대로 도메인 기반 디자인이다. 여기서 말하는 도메인이란 유사한 업무의 집합체이다. 유사한 업무의 집합체들이 상호작용하며 나누어 설계하는 구조이다. 이 구조의 목표는 다음과 같다.
- 응집력은 높게, 결합도는 낮게
- 각각의 도메인은 서로 분리, 변경/확장 용이한 설계
이 구조에 대한 계층은 다음과 같다.
Presentation - Application - Domain - Infra 순으로만 접근이 가능하고, 한 계층의 관심사와 관련된 어떤 것도 다른 계층에 배치가 불가하다. 각계층의 설명은 다음과 같다.
- Presentaion layer
- 요청에 대한 해석과 응답하는 역할
- 사용자에게 UI 제공 및 응답을 다시 보내는 역할하는 모든 클래스들
- Controllers
- Application layer
- 비즈니스 로직 정의 및 수행, 도메인과 인프라스트럭쳐 계층 연결
- 실질적인 테이터 상태 변화 처리는 도메인에서 진행하도록 위임
- Services
- Domian layer
- 비즈니스 로직, 정보에 대한 실질적인 도메인을 가지고 있는 계층
- Entity, Model
- Infrastructure layer
- DB 및 외부 통신 그리고 내부 설정을 담당
위 구조를 기반으로 짠 예시는 다음과 같다.
src/
└── main/
├── java/
│ └── com/
│ └── yourcompany/
│ ├── application/ # 애플리케이션 레이어
│ │ ├── dto/ # 애플리케이션 계층 DTO
│ │ │ ├── UserDto.java
│ │ │ ├── RestaurantDto.java
│ │ │ └── OrderDto.java
│ │ ├── service/ # 애플리케이션 서비스
│ │ │ ├── UserService.java
│ │ │ ├── RestaurantService.java
│ │ │ └── OrderService.java
│ ├── domain/ # 도메인 레이어
│ │ ├── model/ # 도메인 모델
│ │ │ ├── User.java
│ │ │ ├── Restaurant.java
│ │ │ ├── Category.java
│ │ │ ├── Menu.java
│ │ │ ├── Order.java
│ │ │ ├── Review.java
│ │ │ ├── Payment.java
│ │ │ └── Address.java
│ │ ├── service/ # 도메인 서비스
│ │ │ ├── PaymentProcessingService.java
│ │ │ └── ReviewService.java
│ │ └── repository/ # 리포지토리 인터페이스
│ │ ├── UserRepository.java
│ │ ├── RestaurantRepository.java
│ │ └── OrderRepository.java
│ ├── infrastructure/ # 인프라스트럭처 레이어
│ │ ├── repository/ # JPA 리포지토리 구현체
│ │ │ ├── JpaUserRepository.java
│ │ │ ├── JpaRestaurantRepository.java
│ │ │ └── JpaOrderRepository.java
│ │ ├── config/ # 설정 파일
│ │ │ ├── SecurityConfig.java
│ │ │ └── DatabaseConfig.java
│ │ └── utility/ # 유틸리티 클래스
│ │ └── DataMapper.java
│ └── presentation/ # 프레젠테이션 레이어
│ ├── controller/ # 웹 컨트롤러
│ │ ├── UserController.java
│ │ ├── RestaurantController.java
│ │ └── OrderController.java
│ └── dto/ # 프레젠테이션 계층 DTO
│ ├── UserResponseDto.java
│ ├── RestaurantResponseDto.java
│ └── OrderResponseDto.java
└── resources/
├── application.properties # 스프링 설정 파일
└── ... # 기타 리소스 파일
주요 요소들의 설명은 다음과 같다.
- 도메인 모델 (domain/model): 엔티티와 밸류 오브젝트를 포함. 비즈니스 로직과 규칙을 캡슐화하며, 상태와 행동을 관리한다.
- 리포지토리 인터페이스 (domain/repository): 도메인 모델의 지속성을 관리하는 인터페이스를 정의. 구현은 infrastructure에서 수행한다.
- 도메인 서비스 (domain/service): 엔티티에 속하지 않는 도메인 로직을 처리. 복잡한 비즈니스 규칙이나 여러 엔티티 간의 상호작용을 처리한다.
- 인프라스트럭처 (infrastructure): 리포지토리의 구현체, 보안 설정, 데이터베이스 연결과 같은 기술적인 세부사항을 처리.
- 애플리케이션 서비스 (application): 애플리케이션의 작업 흐름을 조정하고, 트랜잭션 경계를 설정하며, DTO를 사용하여 도메인 로직을 프레젠테이션 레이어에 연결.
- 프레젠테이션 레이어 (presentation): 외부 요청을 받고 응답을 반환. 컨트롤러와 DTO(Data Transfer Objects)를 포함하여 클라이언트와의 인터페이스를 담당.
여기서 계층으로 보면 쫌 헷갈려 할 수도 있는 부분이 있다. 어플리케이션의 service와 도메인의 service 그리고 어플리케이션의 dto와 프레젠테이션의 dto 이다. 이에 대한 차이점은 다음과 같다.
어플리케이션의 service | 도메인의 service |
---|---|
사용자의 요청을 처리하고 시스템의 다른 부분과의 통합을 담당 | 도메인 모델 내에서 해결할 수 없는 도메인 간의 복잡한 연산을 수행 |
비즈니스 로직보다는 애플리케이션의 흐름과 트랜잭션을 관리 | 순수한 비즈니스 로직을 처리 |
어플리케이션의 dto | 프레젠테이션의 dto |
---|---|
다른 계층과의 데이터 통신 및 비즈니스 로직 처리를 위해 사용 | 사용자 인터페이스와의 데이터 교환에 초점 |
원시적인 데이터 | 보안이나 사용자 경험을 개선하기 위해 데이터를 필터링하거나 추가적으로 가공 |
범용적인 비즈니스 로직 처리를 위한 구조 | 특정 클라이언트 뷰에 맞춰진 데이터 구조 |
찐 도메인 별로 모아보기
DDD라… 근데 내용을 보니 이것도 계층이 있고, 그 계층별로 나누고 그 안에서 다시 나줘서 관리하네? 그럼 이게 도메인 별로 모아진게 맞냐란 의문이 들 수 있다. 그렇다면 진짜 찐 도메인별로 묶어서 생각하면 다음과 같은 구조이다.
src/
└── main/
├── java/
│ └── com/
│ └── yourcompany/
│ ├── user/ # 사용자 도메인
│ │ ├── dto/ # 사용자 도메인 DTO
│ │ │ ├── UserDto.java
│ │ │ └── UserResponseDto.java
│ │ ├── service/ # 사용자 도메인 서비스
│ │ │ ├── UserService.java
│ │ │ └── UserDomainService.java
│ │ ├── model/ # 사용자 도메인 모델
│ │ │ └── User.java
│ │ ├── repository/ # 사용자 도메인 리포지토리
│ │ │ ├── UserRepository.java
│ │ │ └── JpaUserRepository.java
│ │ └── controller/ # 사용자 도메인 컨트롤러
│ │ └── UserController.java
│ ├── restaurant/ # 음식점 도메인
│ │ ├── dto/
│ │ │ ├── RestaurantDto.java
│ │ │ └── RestaurantResponseDto.java
│ │ ├── service/
│ │ │ ├── RestaurantService.java
│ │ │ └── RestaurantDomainService.java
│ │ ├── model/
│ │ │ ├── Restaurant.java
│ │ │ └── Menu.java
│ │ ├── repository/
│ │ │ ├── RestaurantRepository.java
│ │ │ └── JpaRestaurantRepository.java
│ │ └── controller/
│ │ └── RestaurantController.java
│ ├── order/ # 주문 도메인
│ │ ├── dto/
│ │ │ ├── OrderDto.java
│ │ │ └── OrderResponseDto.java
│ │ ├── service/
│ │ │ ├── OrderService.java
│ │ │ └── OrderDomainService.java
│ │ ├── model/
│ │ │ ├── Order.java
│ │ │ └── Review.java
│ │ ├── repository/
│ │ │ ├── OrderRepository.java
│ │ │ └── JpaOrderRepository.java
│ │ └── controller/
│ │ └── OrderController.java
│ ├── payment/ # 결제 도메인
│ │ ├── dto/
│ │ │ └── PaymentDto.java
│ │ ├── service/
│ │ │ └── PaymentService.java
│ │ ├── model/
│ │ │ └── Payment.java
│ │ ├── repository/
│ │ │ └── PaymentRepository.java
│ │ └── controller/
│ │ └── PaymentController.java
│ ├── config/ # 전역 설정
│ │ └── DatabaseConfig.java
│ └── utility/ # 유틸리티 클래스
│ └── DataMapper.java
└── resources/
└── application.properties # 스프링 설정 파일
이 구조는 정말로 도메인 별로 나누었다. User/Restarant/Order/Payment 등 도메인 별로 나눈 다음, 도메인 내에서 dto, service, controller, repository, model 등 계층으로 분리하였다. 이렇게 되면 도메인이란 관점에 따라 관리하는 데에는 최적합일 것이다. 또한 나중에 MSA로 구조를 바꿀때 도메인별로 따로 쉽게 땔 수 있다는 장점도 가지고 있다. 다만, 기존에 비해 초입자가 파악하기에 난이도가 높은 편이다. 그리고 도메인 별로 관리가 되다보니 비슷한 로직의 구조가 반복될 가능성이 있다.
요약 및 결론
DDD의 개념을 가지고 만든 1안 구조와 순수 도메인 위주로 만든 2안 구조의 차이점은 다음과 같이 요약할 수 있다.
구분 | 1안: DDD 개념 | 2안: 도메인 우선 |
---|---|---|
구조 | - application/ : 애플리케이션 서비스와 DTO를 포함.- domain/ : 도메인 모델, 도메인 서비스, 리포지토리 인터페이스를 포함.- infrastructure/ : 인프라스트럭처 구현체 (JPA 리포지토리 등), 설정 파일.- presentation/ : 컨트롤러와 프레젠테이션 계층 DTO. | 각 도메인별 디렉토리 (user/, restaurant/, order/, payment/) 내에 모델, DTO, 서비스, 리포지토리, 컨트롤러가 포함 |
장점 | - 분리의 명확성: 각 계층의 역할이 명확하여 개발자가 시스템의 전체적인 흐름을 쉽게 이해. - 재사용성: 계층 간의 역할이 분리되어 있어, 예를 들어 데이터 접근 로직이나 비즈니스 로직을 다른 프로젝트에서 재사용하기 쉬움. - 교체 용이성: 하나의 계층의 구현을 변경해도 다른 계층에 영향을 주지 않기 때문에 시스템의 유지보수와 확장이 용이 | - 도메인 응집력: 모든 관련된 로직과 데이터가 같은 폴더 내에 위치하여 도메인 관련 작업 시 관련 파일을 쉽게 찾기 가능. - 유지보수성: 도메인별로 구분되어 있어 특정 기능이나 서비스의 수정이 다른 도메인에 영향을 미치지 않음. - 확장성: 각 도메인을 마이크로서비스로 분리하기 위한 기반 작업이 이미 구성되어 있어, 확장이 필요할 때 분리가 용이 |
단점 | - 도메인 간 결합: 동일 도메인의 로직과 데이터가 여러 계층에 걸쳐 분산되어 있어, 특정 도메인을 수정할 때 여러 파일과 계층을 건드릴 가능성 농후. - 개발 복잡성: 큰 프로젝트에서는 각 계층이 서로를 참조하면서 의존성이 복잡해질 수 있음. | - 중복 코드: 각 도메인이 비슷한 구조를 갖추고 있기 때문에, 예를 들어 공통 인프라 코드나 유틸리티 함수가 각 도메인에서 중복될 수 있음. - 학습 곡선: 새로운 개발자가 프로젝트 구조를 이해하고 각 도메인의 위치를 파악하는 데 어려움. - 팀 협업: 큰 팀에서 여러 도메인을 동시에 작업할 때 도메인 간의 상호작용을 관리하는 것이 복잡할 수 있음. |
스프링을 공부하고 사용하는 사람이라면 자기만의 관리 스타일이 있을 것이다. 또는 팀별로 관리를 해야하는 체계와 방침이 있을 것이다. 그리고 개발해야할 요구사항과 자원 및 제약사항들이 존재할 것이다. 이러한 것들을 모두 고려한 후에 관리 구조를 선택할 수 밖에 없다.
그러니까 이에 대한 정답은… 선택 이다. 충분히 고려한 후 관리 구조를 정하자!