[Spring] 스프링 디렉터리 패키지 구조
계층형 디렉터리 구조
com
ㄴ example
ㄴ nuribank
ㄴ config
ㄴ controller
ㄴ domain
ㄴ repository
ㄴ service
ㄴ security
ㄴ exception
스프링 각 웹 계층들을 대표하는 클래스, 디렉터리들을 기반으로 패키징한다.
스프링 웹 계층
- Web Layer: 사용자의 요청과 이에 대한 응답 반환의 전반적인 처리가 일어나는 영역
- Service Layer: Web Layer와 Repository Layer 사이에서 실질적인 애플리케이션 비즈니스 로직이 일어나는 영역
- Repository Layer: DB에 접근 및 통신하는 영역
장점
- 전체적인 구조를 빠르게 파악할 수 있다.
단점
- 각 패키지에 클래스들이 너무 많이 모이게 된다.
- 하나의 패키지 안에 서로 의존하는 수많은 클래스들이 모여 있어 유지보수가 힘들다.
도메인형 디렉터리 구조
com
ㄴ example
ㄴ nuribank
ㄴ domain
| ㄴ account
| | ㄴ controller
| | ㄴ dto
| | ㄴ entity
| | ㄴ exception
| | ㄴ repository
| | ㄴ service
| ㄴ user
| | ㄴ controller
| | ㄴ dto
| | ㄴ entity
| | ㄴ exception
| | ㄴ repository
| | ㄴ service
| ...
ㄴ global
ㄴ auth
ㄴ common
ㄴ config
ㄴ error
ㄴ infra
ㄴ util
스프링 웹 계층이 아닌 도메인 별로 패키지 분리가 가능하도록 한다.
장점
- 관련된 코드들이 응집해 있다.
- 각각의 도메인들이 서로 의존하는 코드가 적도록 설계하기 적합해서 코드의 재활용성이 향상된다.
단점
- 프로젝트에 대한 이해도가 낮을 경우 전체적인 구조를 파악하기 어렵다.
적용 방법
다음은 도메인형 패키징의 한 예시이다.
- 최상위 레벨에서는 domain과 global로 패키징한다.
- domain 패키지에서는 도메인을 기준으로 하위 패키지를 구성한다.
- global 패키지에서는 프로젝트 전방위적으로 사용할 수 있는 클래스들로 구성한다.
실무
우선 패키지 방식에 있어서 정답은 없다.
현재 방식에 불편을 느끼면 언제든지 패키지 구조를 변경하면 된다.
가장 올바른 패키지 방식은 개발자가 관리하기 쉽고, 능률을 향상시키는 방식이다.
나의 생각
클래스 개수가 작은 매우 단순한 프로젝트에서는 계층형, 도메인형 상관없다.
그러나 프로젝트 규모가 커질 수록 도메인 구조로 설계하는 것이 좋다. (특히, 서비스 개발의 경우)
<도메인 구조의 장점>
✅ 유지보수
만약 UserController의 어떤 api를 수정해야 한다면?
UserController, UserService, UserRepository, User 관련 클래스, ... 등 User 도메인에 관련된 여러 코드를 수정해야 할 수도 있다
만약 계층형을 사용한다면 각 도메인에 관련된 클래스들이 분산되어 있지만,
도메인형을 사용한다면 각 도메인에 관련된 클래스들의 응집을 높일 수 있기 때문에 유지보수 하기에 용이하다.
✅ SRP
비즈니스 로직을 작성하다 보면 SRP 원칙을 준수하기 위해 특정 역할을 하는 전용 클래스들을 많이 생성하게 된다.
(ex. UserInfo, UserValidator, AccountGenerator...)
보통 이런 클래스들은 계층형을 사용하면 service 디렉터리에 생성하게 되는데, 이렇게 되면 비즈니스 로직을 위한 여러 도메인의 클래스들이 한 패키지에 뒤섞이게 된다.
도메인형을 사용하면 각 도메인에 속한 비즈니스 로직을 위한 클래스들은 하나의 패키지에서 쉽게 확인할 수 있다.
✅ MSA
만약 프로젝트 요구사항이 추가되어 UserController를 MSA의 한 파트로 분리해야 한다면 어떨까.
위 예시에서 계층형은 프로젝트 전체 패키지를 건드려야 하지만,
도메인형은 user 패키지만 건드리면 된다.
실제로 최근에는 MSA의 영향으로 인해 도메인형 패키지를 많이 사용한다고 한다.
+) MSA에 대한 추가적인 성찰
만약 user 도메인에 관련된 패키지를 MSA 도입으로 인해 분리해야 한다면 생기는 이슈
Q: UserController, 혹은 UserService가 다른 패키지(도메인)의 AccountService를 의존하고 있다면?
A: 객체 지향 설계를 잘 했다면 UserService는 AccountService를 인터페이스로 의존하고 있을 것.
만일 user를 분리해도 AccountService 인터페이스에 대한 구현체만 잘 만들어주면 된다.
+ 추가로 컨트롤러에서 여러 서비스를 의존하는 것을 조심하자. 여러 서비스 의존으로 인해 컨트롤러에 비즈니스 로직이 포함되는 상황을 주의하자.
Q: 엔티티 간에 연관관계는 어떻게 처리 할 것인가? (user - account 엔티티 간에 연관관계가 있을 때)
즉, 물리적으로 분산된 환경에서 엔티티 간에 매핑은 어떻게 변경해야 할까?
A: 고유 식별자(id)를 통해 참조하는 방법이 있다.
추론 객체 참조라고도 하는 이 방법은 user에서 account가 아닌 account_id를 참조하는 것이다.
그럼 user를 분리했을 때 다른 애플리케이션에 있는 account 엔티티를 필요하지 않다.
+ '추론 객체 참조' 추가 학습 - 애그리게잇
도메인 주도 설계 시 업무상 관련 있는 도메인들을 묶어 객체간 관계가 복잡해지지 않도록 생명주기와 무결성을 유지하게 해주는 애그리게잇 (aggregate)을 정의해야 한다.
애그리게잇을 통해 관련 있는 객체들을 묶어 경계를 명확히 정의할 수 있다.
이에 도메인 주도 설계 구현의 저자 반 버논은 "ID로 다른 애그리게잇을 참조하라" 라고 말했다.
만약 user와 account라고 하는 애그리게잇이 있고, 이 둘은 서로 객체로 연관을 맺고 있다면,
user 애그리게잇은 언제 참조하게 될지 모르는, 상대적으로 연관성이 덜한 account 애그리게잇에 대한 참조를 항상 가지고 있어야 한다.
account의 참조를 가지고 있다는 것은 account 애그리게잇 전체와 연결되어 있다고 할 수 있기 때문에 user에 대한 부담이 증가한다.
@Entity
public class User {
private Account account;
}
만약 account 엔티티가 아닌 id를 참조하면 어떨까?
@Entity
public class User {
private Long accountId;
}
엔티티 참조에 비해 에그리게잇 간의 경계가 명확해지며, 성능 면에서도 이점이 있다.
"추론 객체 참조를 가진 애그리게잇은 참조를 즉시 가져올 필요가 없기 때문에 당연히 더 작아진다. 인스턴스를 가져올 때 더 짧은 시간과 메모리가 필요하기 때문에, 모델의 성능도 나아진다. - 도메인 주도 설계 구현"
참고자료
https://velog.io/@jsb100800/Spring-boot-directory-package
https://velog.io/@m1naworld/Spring-Boot-패키지-구조
https://jjeongil.tistory.com/1126
https://www.popit.kr/jpa-연관-관계-조회-그리고-msa/
https://www.popit.kr/id로-다른-애그리게잇을-참조하라/
예전에 회사 인턴할 때 작성했던 글인데, 패키지 구조를 공부하다가 도메인 주도 설계까지 빠지게 된... 살짝 heavy한 글이다.
그래도 실무 관점에서 DDD의 매력을 처음 느끼게 해준 부분이라 정리해서 포스팅해봤다.