Showpot Server을 구성할 때 멀티 모듈을 적용하기로 결정을 하였습니다. 따라서 이 게시글에서는 Showpot에서는 어떻게 멀티 모듈을 적용했는지 작성해보겠습니다. (해당 게시글을 보고 멀티 모듈을 실습할 순 없습니다.) 저는 멀티 모듈을 처음 접하였기에 멀티 모듈 구조 공부에서 멀티 모듈 구조를 선 공부를 해보았습니다.
멀티 모듈을 적용한 이유
저희 팀이 생각한 멀티 모듈을 적용한 가장 큰 이유입니다.
- 코드의 재사용성을 높이고, 각 모듈의 독립적인 개발 및 테스트가 가능합니다. 이는 팀원이 독립적으로 모듈을 개발하고 관리할 수 있으므로, 전체 프로젝트의 개발 속도와 품질을 향상시킬 수 있을 것이다!
- 각 모듈을 독립적으로 관리함으로써, 특정 모듈의 변경이나 업데이트가 전체 프로젝트에 미치는 영향을 최소화할 수 있다!
- 즉 프로젝트의 복잡성을 줄이게 되고 효율적인 협업이 가능할 것이다!
이런한 생각을 가지고 멀티 모듈을 적용해 보았습니다.
멀티 모듈 적용을 어떻게 하였는가?
- 모듈 구조의 전체는 다음과 같습니다.
주의점 : 해당 모듈 구조는 정답이 아닙니다. 팀 마다 모듈 구조는 각각 다르고, 현재 모듈 구조는 더 개선될 가능성도 많다고 생각합니다.
yapp_backend 모듈
프로젝트를 잡는 가장 큰 틀입니다.
- 하위 패키지인 .github에서는 이슈, PR 템플릿이나 애플리케이션을 github actions로 CI/CD를 할 수 있고, docker file들이 존재합니다.
app 모듈
- 애플리케이션을 실행하는 가장 큰 단위의 모듈입니다.
implementation project(":app:api")
implementation project(":app:infrastructure")
// run spring application
implementation "org.springframework.boot:spring-boot-starter-web"
// docker-compose
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
testAndDevelopmentOnly 'org.springframework.boot:spring-boot-docker-compose'
// aws logs
implementation 'ca.pjer:logback-awslogs-appender:1.6.0'
build.gradle에서는 api 모듈, infrastructure모듈을 의존하고, 애플리케이션을 실행할 spring boot starter web, docker-compose, aws cloudwatch로 로깅할 의존성을 추가해주었습니다.
@SpringBootApplication
@Import(value = {ApiConfig.class, InfrastructureConfig.class})
public class YappBackendApplication {
public static void main(String[] args) {
SpringApplication.run(YappBackendApplication.class, args);
}
}
src.main 패키지 내부에는 spring boot 애플케이션을 실행할 main 함수가 있습니다.
ApiConfig, InfrastructureConfig를 Import하여 해당 설정 파일을 읽고, 하위 모듈 전체의 Bean들을 스캔하고 spring context에 등록하여 애플리케이션을 실행하게 됩니다.
common 모듈
- 순수 Java 만 존재하는 모듈로 어떠한 의존성도 가지고 있지 않습니다. 공통 모듈에 의존성이 생기게 되면 스파게티 코드로 가는 지름길일 수 있습니다. "어떤 코드를 어느 모듈에 넣을지 애매할 때, 그냥 공통 모듈에 넣어야겠다~"라는 마인드로 공통 모듈이 두꺼워질 수 있습니다.
- 따라서 순수 자바로만 클래스들을 사용했습니다.
api 모듈
- api 모듈은 간단히 클라이언트로부터 요청을 받아서 응답을 해주는 모듈입니다.
- 대표적으로는 API 문서화를 위한 swagger와 security 등에 대한 의존성이 있습니다. + domain 모듈 의존성이 있습니다.
- controller와 service 패키지가 존재합니다.
- 여기서 service는 도메인 영역과 표현 영역을 연결해주는 창구 역할을 해줍니다.
api 모듈 내부에는 common, show, user api 모듈로 각각 나뉘는데, common은 JWT로 인증/인가 에 대한 설정과 필터 등이 존재합니다. show는 공연 도메인에 대한 api 모듈, user는 사용자 도메인에 대한 api 모듈로 나뉩니다. show와 user의 연관된 기능이 있을 수 있는데, 현재는 show-api 모듈에 담겨져 있습니다. 아직은 공통된 부분에 대한 기능이 적어서 괜찮은 것 같지만, 공통된 부분이 show-api에 몰려있으면 모듈 자체의 복잡성이 높아질 우려가 있고, show-user api 모듈을 따로 만들지는, 개선해 보면서 확인해 봐야 할 것 같습니다.
@Configuration
@Import({ShowDomainConfig.class, UserDomainConfig.class})
@ComponentScan(basePackages = "com.example")
public class ShowApiConfig {
}
각 모듈에는 다음과 같은 Config가 있습니다. 애플리케이션을 실행할 때 설정이나, 컴포넌트 스캔을 하기 위해 만들어져 있습니다.
domain 모듈
- 도메인 모듈은 showpot 서비스의 도메인에 관한 모듈입니다.
- 대표적으로 JPA, QueryDSL, 데이터베이스 연동을 위한 PostgreSQL에 대한 의존성을 가지고 있습니다. 데이터베이스, ORM 기술도 밑에 나올 Infrastructure 모듈에 있어야 하는게 아닌가 싶을 수 있는데, 일단 저희 팀은 ORM, 데이터베이스를 JPA와 PostgreSQL로 향후에도 변경하지 않을 걸 가정하여 domain 모듈에 의존성을 추가한 것입니다. 만약에 ORM, 데이터베이스도 변경할 것을 가정하고 유연하게 유지보수하기 쉬울려면 domain 모듈에 ORM이나 데이터베이스에 대한 의존성이 없이 순수 domain 자체로만 있는게 좋을 듯 해보입니다.
- 각 domain 모듈안에 entity, repository, usecase 패키지 등이 존재합니다. usecase 패키지는 api 모듈의 service layer와는 다르게 domain에 중심이 되는 로직들이 담겨지게 됩니다.
domain도 api 모듈과 마찬가지로 common, show, user domain 모듈로 나뉘게 됩니다.
common 모듈에는 BaseEntity가 존재합니다. show, user 도메인에 있는 entity들은 BaseEntity를 상속받아 id, createAt, deleteAt, updateAt 필드를 함께 가져갑니다.
domain 모듈도 api 모듈에서도 마찬가지로 show와 user의 공통 domain이 있는데, 이를 나중에는 show-user-domain 모듈로 빼야할 지는 더 고민해 봐야겠습니다.
도메인 모듈에는 java-test-fixtures 플러그인을 추가하여 testFixtures 모듈이 존재합니다. api 모듈에서 service layer을 단위 테스트 할 때 도메인 모듈에 존재하는 엔티티, dto 들이 필요한데, testFixtures 모듈에 해당 class들을 만들어서 api 모듈에서 재사용할 수 있습니다. 만약 testFixtures 모듈이 없다면, api 모듈을 테스트할 때 필요한 class들과 domain 모듈에서 테스트할 때 필요한 class들이 중복이 될 것 입니다.
infrastructure 모듈
- 외부 의존성들을 가지는 모듈의 모음입니다.
- 하나의 모듈은 최대 하나의 인프라스트처만 책임지도록 모듈을 작성하였습니다. 다양한 인프라스트럭처가 하나의 모듈에 담길 시 복잡성은 증가하게 될 것입니다.
api 모듈의 service Layer 혹은 usecase Layer에서 외부 요청에 대한 로직이 있는데, 인터페이스에 의해 의존한 상태이고, infrastrucre의 각 모듈들은 해당 인터페이스들의 구현체가 존재하게 됩니다. api모듈은 infrastruture모듈에 비해 상대적으로 고수준이기에, infrastructure은 각 모듈에서 사용되는 해당 api 혹은 domain 모듈을 의존합니다. 저수준이 고수준 모듈을 의존하는 DIP 원칙을 준수한 형태입니다. 좀 더 자세한 내용은 여기서 확인할 수 있습니다. 인터페이스를 의존해도 결국 spring context에서는 bean을 자동으로 주입을 시켜줄 수 있습니다. (message-queue의 경우 redis pub/sub을 사용했는데 추후 기술이 변경될 수 있어 모듈을 따로 뺐습니다.)
멀티 모듈을 적용한 후 느낀점
- 위에서 언급한 멀티 모듈을 적용한 이유에 대해 다시 생각해 봤을 때 해당 내용들에 대해 이점을 확실히 얻을 수 있음을 느꼈습니다.
- 각 모듈의 독립적인 개발 및 테스트가 가능해서 전체 프로젝트의 개발 속도와 품질이 향샹될 수 있었고, 저수준 모듈이 고수준 모듈을 의존하여, 유지보수하기 쉽고 변경하기 좋게 설계가 된 것이 확실히 체감되었습니다. 즉, 특정 모듈의 변경이나 업데이트가 전체 프로젝트에 미치는 영향이 최소화된 것입니다. 또한 단일 모듈로 작업했을 때는 프로젝트의 크기가 점점 커질수록 프로젝트의 복잡성이 높아지고, 의존성도 많이 꼬이게 되는 느낌을 받았는데, 멀티 모듈을 적용하고 각 관심사에 따라 모듈을 분리하고 의존성을 고려하니 프로젝트의 복잡성을 줄이게 되고 효율적인 협업이 가능하였습니다.
- 반면 멀티 모듈을 적용하며 단점(?)도 있었는데, 우선적으로 의존성에 대한 고려가 많이 필요하다고 느꼈습니다. 복잡성을 줄이기 위해 멀티 모듈을 적용한 것인데, 오히려 스파게티 모듈로 갈 수 있는 상황이 많아질 수 있음을 알게 되었습니다. 또한 프로젝트의 사이즈가 그리 크지 않다면, 단일 모듈로도 충분하기 때문에 오히려 멀티 모듈은 러닝 커브, 오버 스택이라고 생각이 들었습니다.
- 마지막으로 멀티 모듈을 적용하면 헥사고날 아키텍처를 사용하는 경우도 많이 보았는데, 이 아키텍처는 추후 더 공부하고 실습하면 좋을 것 같다는 생각이 들었습니다. (헥사고날 아키텍처가 정답은 아니니깐요~)
- 찐 마지막으로 현재의 설계가 과연 유지보수하기 쉬운 것은 계속 확인해 봐야할 것이기에 해당 구조들은 추후에도 계속 개선될 수 있을 것 같습니다.