← 모든 글
Spring

4. Customizing the Application Modules Arrangement

Spring Modulith는 2. 애플리케이션 모듈의 구성 의 하위 페이지에서 본 것처럼 메인 애플리케이션 클래스가 위치한 베이스 패키지를 기준으로 하위 패키지들을 모듈로 자동 감지해요.

by rati·

Spring Modulith는 2. 애플리케이션 모듈의 구성 의 하위 페이지에서 본 것처럼 메인 애플리케이션 클래스가 위치한 베이스 패키지를 기준으로 하위 패키지들을 모듈로 자동 감지해요.

하지만 프로젝트 상황에 따라 모듈의 물리적인 배치 구조를 다르게 가져가거나 감지 범위를 직접 세밀하게 조정해야할 수도 있어요. Spring Modulith 는 애플리케이션 모듈 배치 커스텀 기능 을 제공하여 커스텀한 설정을 지원해요. @Modulithic 를 통해 설정할 수 있어요.

package example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.modulith.Modulithic;

@Modulithic
@SpringBootApplication
class MyApplication {

  public static void main(String... args) {
    SpringApplication.run(MyApplication.class, args);
  }
}
package example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.modulith.Modulithic

@Modulithic
@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
  runApplication<MyApplication>(*args)
}

@Modulithic 의 속성에는 다음과 같은 것들이 있어요.

  • systemName : 생성되는 모듈 구조 문서(Diagram)나 로그에 표기될 애플리케이션의 이름
  • sharedModules
    • 여러 모듈 테스트 시 항상 기본으로 함께 로드되도록 설정할 공유 모듈(Shared Modules)을 지정
      • -> 해당 모듈들이 애플리케이션 모듈 통합 테스트에 항상 포함됨.
  • additionalPackages : 기본 감지 범위(메인 패키지 하위) 외에, 추가적으로 모듈 감지를 트리거할 루트 패키지 목록을 지정.

Customizing Module Detection

Spring Modulith의 기본 모듈 감지 전략을 변경하는 방법은 2가지가 있어요.

  • 기본 모듈 감지 전략 : “메인 패키지의 직속 하위 패키지을 모듈로 인식”

1. 어노테이션 기반의 명시적 감지 (explicitly-annotated)

기능 활성화

spring.modulith.detection-strategy=explicitly-annotated

적용 방법 package-info.java에 아래에 나온 애노테이션을 부착하면 이후에는 해당 애노테이션이 붙은 패키지만 모듈로 인식이 되요.

  • @ApplicationModule
  • jMolecules의 @Module
Example
 ├─ src/main/java
 │  ├─ example
 │  │  ├─ Application.java
 │  │  │
 │  │  ├─ [order]          <-- 어노테이션을 붙여서 모듈로 감지시킬 폴더
 │  │  │  ├─ package-info.java 
 │  │  │  └─ OrderManagement.java
 │  │  │
 │  │  ├─ [inventory]      <-- 어노테이션을 붙여서 모듈로 감지시킬 폴더
 │  │  │  ├─ InventoryManagement.java
 │  │  │  └─ Marker.java  (Kotlin 등을 위한 일반 클래스 파일)
 │  │  │
 │  │  └─ [trash-code]     <-- 아무 어노테이션도 안 붙인 폴더 (모듈에서 제외됨!)
 │  │     └─ LegacyService.java
// example/order/package-info.java
@org.springframework.modulith.ApplicationModule
package example.order;
// example/inventory/Marker.java 또는 InventoryManagement.java 상단
package example.inventory;

@org.springframework.modulith.ApplicationModule
public class Marker {
    // 빈 클래스이거나 기존 도메인 클래스여도 상관없음
}
// example/order/package-info.java
@org.jmolecules.architecture.layered.Module
package example.order;

2. 커스텀 감지 전략 : ApplicationModuleDetectionStrategy

ApplicationModuleDetectionStrategy 인터페이스를 이용하면 보다 세밀하게 모듈 감지를 할 수 있어요.

package example;

import java.util.stream.Stream;
import org.springframework.modulith.core.ApplicationModuleDetectionStrategy;
import org.archunit.core.domain.JavaPackage;

class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {

  @Override
  public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
    return basePackage.getSubpackages().stream()
        .filter(pkg -> pkg.getName().endsWith("module"));
  }
}
package example

class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrategy {

  override fun getModuleBasePackages(basePackage: JavaPackage): Stream<JavaPackage> {
    // 모듈 등록 로직 생략
  }
}

이후 해당 구현체들에 대한 등록이 필요해요.

spring.modulith.detection-strategy=example.CustomApplicationModuleDetectionStrategy
#패키지명.구현체 이름으로.

ApplicationModuleDetectionStrategy 의 위치는 다음 내용을 고려해서 위치를 시켜야 해요.

  • 검증(Verify)과 문서화만 할 것인가?
  • 런타임 기능(Initializers, 액추에이터 등)도 쓸 것인가?

A. 검증(Verify) 와 문서화만 할 것임.

  • spring modulith의 verify() 와 의존성 다이어그램를 자동 생성하는 기능은 테스트에서 실행해요. 즉, 개발용/테스트용 기능으로 볼 수 있어요. 그렇기 때문에 해당 경우에서는, 서버 코드에 포함시킬 필요가 없으므로 src/test/java 에 위치하는 것을 권장해요. B. 런타임 기능도 사용할 것임.
  • 런타임 기능에 대해서는 다음 문서를 확인하세요! Spring Modulith Docs : Production-ready features
  • 런타임 기능을 사용함에도 ApplicationModuleDetectionStrategy 구현체를 테스트에 둘 경우, 배포용 jar 에 해당 구현체가 담기지 않아 오류가 발생해요.

Contributing Application Modules

1.3 버전부터는 @Modulithic 설정의 additionalPackages로 패키지명을 하드코딩하지 않더라도, ApplicationModuleSource와 ApplicationModuleSourceFactory 추상화를 활용해 외부에서 유연하게 애플리케이션 모듈을 설정할 수 있는 방법을 지원해요.

Example
╰─  src/main/resources
   ╰─  META-INF
      ╰─  spring.factories  // CustomApplicationModuleSourceFactory를 등록해요!

src/main/resources/META-INF/spring.factories

org.springframework.modulith.core.ApplicationModuleSourceFactory=example.CustomApplicationModuleSourceFactory

example/CustomApplicationModuleSourceFactory.java

// example/CustomApplicationModuleSourceFactory.java
package example;

import java.util.List;
import org.springframework.modulith.core.ApplicationModuleSourceFactory;
import org.springframework.modulith.core.ApplicationModuleDetectionStrategy;

public class CustomApplicationModuleSourceFactory implements ApplicationModuleSourceFactory {

  @Override
  public List<String> getRootPackages() {
    // 감지를 적용할 추가 루트 패키지를 지정해요.
    return List.of("com.acme.toscan");
  }

  @Override
  public ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
    // 적용할 감지 전략을 반환할 수 있어요.
    return ApplicationModuleDetectionStrategy.explicitlyAnnotated();
  }

  @Override
  public List<String> getModuleBasePackages() {
    // 모듈로 생성할 구체적인 베이스 패키지를 직접 명시할 수도 있어요.
    return List.of("com.acme.module");
  }
}

Customizing Named Interface Detection

패키지에 일일이 @NamedInterface를 선언하기 번거롭다면, ApplicationModuleDetectionStrategy에서 detectNamedInterfaces를 재정의(Override)하여 특정 패턴의 하위 패키지를 자동으로 Named Interface로 지정할 수도 있어요!

// example/CustomApplicationModuleDetectionStrategy.java
package example;

import java.util.stream.Stream;
import org.springframework.modulith.core.ApplicationModuleDetectionStrategy;
import org.springframework.modulith.core.NamedInterfaces;
import org.springframework.modulith.core.ApplicationModuleInformation;
import org.archunit.core.domain.JavaPackage;

class CustomApplicationModuleDetectionStrategy implements ApplicationModuleDetectionStrategy {

  @Override
  public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
    return basePackage.getSubpackages().stream();
  }

  @Override
  public NamedInterfaces detectNamedInterfaces(JavaPackage basePackage, ApplicationModuleInformation information) {
    // 모듈 하위 패키지 중 "api"로 끝나는 패키지를 자동으로 Named Interface로 빌드해요!
    return NamedInterfaces.builder()
        .recursive()
        .matching("api")
        .build();
  }
}

이렇게 하면 개발자가 매번 어노테이션을 붙이지 않아도 규칙에 맞는 패키지가 외부 통신 인터페이스(api)로 자동 지정되어 모듈 노출 범위 제어가 더욱 간결해집니다.

끝까지 읽어주셔서 감사합니다.
DISCUSSION

이 글에 대해 이야기해요

질문이나 다른 관점을 남겨 주세요.