멀티모듈 프로젝트에서 Base package를 똑같게 만들어줘야 하는 이유

땡글이·2023년 10월 21일
1

결론부터 말하면, BasePackage 경로가 다르면 빈을 못 찾는다!

문제 상황 분석


시작은 위 그림처럼, ProductService 타입의 빈을 찾지 못했다라는 문제를 발견하게 된다. 다만, ProductService 클래스에는 당연히 @Service 어노테이션으로 빈 등록을 해줬는데 못 읽을 이유가 없었다.

멀티모듈 설정 문제인가?

juju-api
ㄴ com.juloungjuloung.juju.api
   ㄴ application
      ㄴ product
         ㄴ AdminProductApplicationService.kt
juju-domain
ㄴ com.juloungjuloung.juju.domain
   ㄴ application
      ㄴ product
         ㄴ ProductService.kt

멀티모듈 구성 시에는 위처럼 구성해줬고, 당연히 juju-api 모듈에서는 juju-domain 모듈을 참조하도록 구성해줬다. 아래처럼 말이다.

// juju-api 모듈의 build.gradle.kts

...

dependencies {
    implementation(project(":juju-domain"))
	...
}

처음 IDEA의 빨간줄이 그어진 이미지를 보면 알 수 있듯이, ProductService 클래스를 참조하지 못하는 게 아니다. 정상적으로 import 가 된다. 다만, Bean 조회가 안되는 것이다. 이 때 멀티모듈 관련해서 얼핏 들었던 내용들..

멀티모듈 설정할 땐, base package가 같아야 돼..

사실 이 내용을 듣기는 했었지만 그래야하는 이유가 없다고 느껴 굳이 base package를 같게 해주지 않았다. 그리고 package 를 구분해두면, 추후에 package를 기준으로 클래스나 메서드를 조회(@EntityScan, Swagger의 packageToScan 기능)할 때, 다르게 구분해주는 것이 스캔 대상 범위를 줄여주므로, 훨씬 이점이 많다고 생각했다.

사소한 부분에서 약간이나마 성능 개선을 했다는 생각에 싱글벙글해서 중요한 부분인 Bean 조회 대상이 되는 base package를 고쳐서 문제가 생겼다. Bean 조회 대상이 되는 Base package 만 고쳐주면 잘 동작하겠네? 맞다. 아래처럼 Application 에서 @ComponentScan 대상을 지정해주면 문제 해결 가능하다.

근데 뭔가 좀 코드가 더러워졌다… (마음에 안들어…) 그리고 모든 모듈마다 이렇게 해줄 수도 없는 노릇 아닌가? 그래서 base package 를 수정해줘서 아래와 같이 디렉터리 구조를 바꿔줬더니 정상적으로 빈을 조회할 수 있었다! 물론 아까 말했던 상황에서의 약간의 성능향상은 있을 수 있어도, 이걸로 인해 성능이슈는 없을 것이라고 판단해서 base package 자체를 수정해줘서 해결했다!


조금 더 파보자!

@ComponentScan 을 안넣어주면 조회 대상을 어떻게 지정해줬을까?

문제는 해결됐지만, 사용자가 직접 @ComponentScan 대상을 지정해주지 않았을 때는 어떻게 빈 조회 대상을 지정해줬을지 궁금해졌다! @SpringBootApplication 어노테이션에 적용된 @ComponentScan을 봐도, 대상 패키지를 지정해주지 않고 있다. 그럼 어디에서 넣어줄까?


이유는 @SpringBootApplication 에 적용된 @EnableAutoConfiguration 에 적용된@AutoConfigurationPackage 어노테이션에 있었다. (찐찐찐 흑막)

@AutoConfigurationPackage 어노테이션에 대한 설명을 읽어보면 base package 를 자동으로 등록해준다고 명시되어 있다.

@AutoConfigurationPackage 어노테이션은 어떻게 base package를 등록해주는가?

또 궁금해졌다 ㅎㅎ 어떻게 base package를 자동으로 등록해주는거지? 어노테이션을 참조하고 사용하는 클래스의 코드를 까보자!

찾아보니 AutoConfigurationPackages 라는 추상클래스에서 base package를 등록 해주고 있었다. 등록해주는 코드를 살펴보자!

register 메서드 코드해석

	public static void register(BeanDefinitionRegistry registry, String... packageNames) {
		
		// registry 에 이미 AutoConfigurationPackages 타입의 빈이 등록되어 있다면, 
		// 해당 Bean을 조회해와 base package 를 추가해준다.
		if (registry.containsBeanDefinition(BEAN)) {
			addBasePackages(registry.getBeanDefinition(BEAN), packageNames);
		}

		// 아니라면,
		// BasePackages 클래스 타입으로 BeanDefinition을 정의하고,
		// 스프링 내부에서 사용되는 빈이라는 의미로 롤을 ROLE_INFRASTRUCTURE 로 지정한다.
		// 해당 Bean Definition에 base package를 더해준 뒤,
		// BEAN, 즉 AutoConfigurationPackages 클래스 이름으로 Bean Factory 에 해당 빈을 등록해준다
		else {
			RootBeanDefinition beanDefinition = new RootBeanDefinition(BasePackages.class);
			beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
			addBasePackages(beanDefinition, packageNames);
			registry.registerBeanDefinition(BEAN, beanDefinition);
		}
	}
  • 위와 같이 동작함으로써, base package를 직접 빈으로 등록해줬고 해당 패키지를 기준으로 컴포넌트 대상이 정해지는 것이다.
  • 즉, base package 도 빈으로 등록된다는 사실을 알아냈다!
    • 추가설명
      • 스프링에서는 Bean 에도 3가지의 ROLE 이 정해져있다!
        • ROLE_APPLICATION : 유저가 직접 정의한 빈
        • ROLE_INFRASTRUCTURE : 스프링 프레임워크 내부에서 사용되는 빈
        • ROLE_SUPPORT : 스프링 프레임워크 내부에서 사용되는 빈 (ROLE_INFRASTRUCTURE 을 지원해주는 지원 역할)

해당 메서드 설명에도 나와있긴 한데, 해당 메서드를 오버라이딩해서, 수동으로 기본 패키지를 바꿀 수도 있다고 알려주지만 권장하지 않는다고 나와 있다!

살짝 뇌절 ㅎㅎ 패키지 이름은 어디에서 받아오지??

register 메서드를 이용하고 있는 곳을 찾으면 되는데, 찾아보니 AutoConfigurationPackages 클래스 내의 static 클래스인 Registrar 클래스에서 해당 메서드를 사용해서 base package를 입력해주고 있었다!

그리고 또, AnnotationMetadata 타입 변수에서 Package 정보를 가져오는 것을 알 수 있는데, AnnotationMetadataPackageImports 클래스 생성자에서 직접 metadata를 이용해서 base package를 파싱해주는 것을 알 수 있다.

PackageImports 생성자 부분을 보면, metadata 에서 @AnnotationConfigurationPackage 어노테이션이 적용된 클래스(우리의 base package에 위치한 클래스!)의 메타데이터 정보를 받아와서 base package를 지정해주는 것을 알 수 있다!


해결방법 정리

  • 모듈별 base package 똑같게 해주기
  • @ComponentScan 어노테이션으로 컴포넌트 조회 대상 패키지 지정
  • AutoConfigurationPackages 추상클래스의 register 메서드 오버라이딩
profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글