Spring Modular Monolith (3) - Spring Modulith로 모듈 경계 검증
velog
지난 글에서 Gradle Multi-Module을 이용해 모듈을 독립적인 빌드 단위로 나누고, 모듈 간 의존성을 컴파일 타임에 제한하는 방법을 정리했다. 그래서 다음과 같은 의문이 생겼다. Gradle로 이미 모듈을 나눴는데 Spring Modulith는 왜 필요하지? 이번 글에서는 Gradle Moduler과 Spring Modulith의 Application Module이 어떻게 다른지, 현재 참여중인 프로젝트에서는 Spring Modulith와 ArchUnit을 이용해 어떤 경계를 추가로 검증하고 있는지 정리해보려고 한다!! 현재 프로젝트에서는 모듈 경계 검증을 위한 용도로만 Spring Modulith를 사용하고 있다. Gradle이 막아주는 경계 이전 글에서 정리한 것처럼 Gradle은 서로 다른 Subproject 사이의 물리적인 의존성 경계를 만들어준다. 하지만 하나의 Gradle Module 내부라면? 하나의 Gradle Module 안에서의 경계 참여중인 프로젝트처럼 하나의 Gradle Module 내부에 여러 패키지가 존재하는 경우, Gradle의 관점에서는 내부 모든 코드가 하나의 Subproject에 속한다. 따라서 Gradle만으로는 service.impl은 외부에서 직접 사용하지 않는다 같은 패키지 수준의 세부 규칙을 표현하기가 어렵다. -> Gradle Module에 대한 dependency가 있다고 해서 그 모듈의 모든 패키지를 사용해도 되는 것인지는 별개의 문제 Application Module Gradle에서의 모듈 : settings.gradle.kts에 등록한 Subproject Spring Modultih는 Spring Application의 Java Package 구조를 기준으로 Application Module을 해석 base package ├── api ├── security ├── logging ├── db ├── common ├── 도메인1 ├── 도메인2 ├── 도메인3 └── internal 이라고 할때, Spring Modulith는 이 패키지 구조를 기준으로 도메인1/2/3, security 등을 Application Module로 해쇼ᅥᄀ한다. Gradle 경로와 Java Package가 다를수도 있는 이유 Gradle 경로를 그대로 따라가면 Spring Modulith의 Application Module 구조와 원하는 도메인 경계가 맞지 않게 될수도 있다. 현재 진행중인 프로젝트에서도 Gradle의 디렉터리 구조를 그대로 반영할 경우 원하는 도메인 패키지들을 각각의 독립적인 Application Module로 인식시키기 위해 Gradle의 디렉터리 구조를 그대로 반영하지 않았다. => Gradle Module은 빌드와 classpath 경계를 만들고, Java Pacakage는 Spring Modulith가 해석하는 Application Module 경계에도 영항ᄋ르 준다. verify() 진행중인 프로젝트에서는 다음과 같은 테스트를 통해 Application Module 구조를 검증한다. class ModularityTests { private final ApplicationModules modules = ApplicationModules.of(ServerApplication.class); @Test void verify() { modules.verify(); } } ServerApplication을 기준으로 APplication Module을 분석하고 verify()를 실행한다. 이 검증은 Application Module 사이의 Dependency를 분석하고, 경계 위반과 순환 의존 등을 검증한다. 실제로 경계가 깨진 사례 실제로 프로젝트를 진행하면서 이 경계가 깨진것이 검증된 적이 있었다. 한 클래스를 다른 모듈에서 사용할 필요가 생겼을 때, 해당 클래스는 public이며 Gradle 관점에서도 필요한 모듈 dependency가 선언되어 있었기에 컴파일 단계에서는 성공했다. 하지만 Spring Modulith의 verify()가 실패했는데, 해당 클래스의 위치가 Application Module의 내부 영역이었는데 외부 Application Module에서 이를 직접 참조 하고 있었기 때문이다. => 해당 클래스의 위치를 base package로 이동시킴으로써 패키지 구조 자체가 외부에 공개하는 APIdhk 내부 구현이라는 의도를 표현하게 되었다. public이라는 Java 개념과 Application Module이 외부에 공개하는 API는 같은 개념이 아니라는 것을 알게 되었다. CLOSED Module Spring Modulith의 기본적인 Application Module은 CLOSED 형태의 경계를 제공한다. event ├── EventService ├── EventRepository │ └── internal └── EventServiceImp 다음과 같은 구조를 생각할 수 있는데, 모듈의 base package는 외부에 공개하고, 하위 ᅦackage는 내부 구현으로 취급한다. 하지만 현재 프로젝트의 Domain Package 구조는 이 모델과 맞지 않았다. Domain Module을 OPEN으로 event └── domain ├── event │ ├── domain │ ├── repository │ └── service │ ├── event2 │ ├── domain │ ├── repository │ └── service │ └── event3 다음과 같이 공개해야 할 타입들이 모두 하위 패키지에 존재한다. 그래서 Domain Module에서는 OPEN Module을 사용했다. @ApplicationModule( type = ApplicationModule.Type.OPEN ) 이를 통해 하위 패키지에 있는 공개하는 타입을 외부 모듈에서 사용할 수 있다. 내부 구현도 열리는 문제 위에서 정한 OPEN으로 인해 service.impl까지 Modulith 수준에서 열리게 된다. 따라서 Java 접근 제어와 ArchUnit을 함께 사용했다. 구현체는 package-private Service의 공개 인터페이스는 다음과 같이 public으로 선언했다. public interface EventService { // ... } 구현체에는 public을 붙이지 않았다. @Service @RequiredArgsConstructor class EventServiceImpl implements EventService { // ... } private 클래스 이므로 다른 package에서 직접 사용할 수 없어, API는 공개 인터페이스만 의존하고 구현체에 직접 의존할 수 없다. ArchUnit 앞선 글에서도 다뤘던 것 처럼 private로 선언하자는 약속은 규칙일 뿐이다. 따라서 이를 검증하고 강제하기 위해 ArchUnit을 사용했다. ArchUnit은 Java코드의 구조적인 규칙을 테스트 코드로 작성할 수 있게 해준다. Controller가 Repository를 직접 참조하지 않나? 특정 package의 클래스가 public이 아닌가? Gradle + Modulith + ArchUnit 프로젝트에 도입한 다음 3가지는 서로 다른 경계를 담당하고 있다. 각각 Gradle Module 사이의 의존성, Application Module 사이의 공개 경계, service.impl과 같은 프로젝트 내부 규칙 Gradle 가장 큰 물리적인 경계를 담당한다. dependency를 통해 컴파일 단계 실패하도록 한다. Spring Modulith Spring Application을 Application Module로 바라본다. verify()를 통해 모듈 사이의 경계와 의존 관계를 검증한다. public이고 Dependency가 존재하더라도 Application Module의 공개 영역이 아니면 문제가 된다. ArchUnit 프로젝트가 정의한 세부적인 코드 구조를 검증한다. 의존성을 없애는 것이 목적이 아니다 모듈화를 공부하며 다른 모듈에 의존하지 않는 것이 좋은 구조인가? 라는 생각을 가졌다. 하지만 실제 코드에서는 모듈 사이의 의존성이 존재한다. 예로 controller에서 Domain Service를 사용한다. 여기서 중요한 것은 어떤 방향의 의존성을 허용할 것인지를 결정하는 것 이다. 외부 모듈이 구현체에 직접 의존하지 않고 공개된 계약을 사용하도록 설계했다. 따라서 모듈화의 목적 : 필요한 의존성을 명시적이고 안정적인 방향으로 만드는 것 이라고 이해하게 되었다. @ 실제 규칙을 강제하는 효과를 지니려면 CI까지 연결해 Merge를 막아야한다.(참고) CLOSED 구조를 사용하지 않은 이유 처음부터 CLOSED 기본 구조에 맞춰 패키지를 구성했다면 더 단순하지 않았을까? 하는 생각을 가지게 되었다. 하지만 현재 프로젝트는 하나의 Domain Module 안에서 다시 도메인과 계층을 표현하는 패키지 구조를 선택했다. Spring Modulith의 기본 모델에 프로젝트 전체 구조를 억지로 맞추기보다, 필요한 부분만 사용하고 부족한 세부 경계는 다른 도구로 보완한 것이다. 정리 지난 글에서 Gralde Multi-Module을 정리할 때는 의존하지 않은 모듈을 complie classpath에서 제거하면 아키텍처 경계를 강제할 수 있다 가 핵심이었다면 이번 글에서는 더 안쪽의 경계를 살펴봤다. Gradle은 어떤 모듈의 타입을 complie classpath에서 볼 수 있는지 결정 Spring Modulith는 Java Package를 Application Module로 해석하고 verify()를 통해 모듈 경계를 검증 ArchUnit은 프로젝트에서 정의한 세부 구조 규칙을 검증 프로젝트를 진행하며 경험한 실제 사례를 통해 컴파일 가능하다는 사실과 아키텍처상 올바른 의존은 다르다는 것을 확인할 수 있었다. Modular Monolith의 경계는 하나의 도구과 아니라 알맞은 도구를 이용해 코드 간의 공개 여부를 명시하고 검증하는 것이라고 이해하게 되었다. 다음 글에서는 이렇게 구성한 모듈 경계 안에서 비즈니스 로직을 어떻게 구성하는지 정리해보려고 한다. Port/Adapter 구조와 비즈니스 규칙을 Service가 아닌 Domain Object에 두면서 어떤 차이가 생겼는지 정리해보려고 한다!
Score: 54.37Confidence: 49%
View offer