📕 목차
1. 문제의 발단
2. 고민 과정
3. 해결 방법
4. 왜 external-api 모듈은 되고, infra 모듈은 안 됐을까?
1. 문제의 발단
📌 @SpringBootApplication 클래스의 충돌
Found multiple @SpringBootConfigration annotated classes [...]
📦pennyway
┣📦pennyway-app-external-api
┃ ┗ 📂src
┃ ┗ 📂main
┃ ┗ 📂java
┃ ┣ 📂kr.co.pennyway
┃ ┣ 📂api
┃ ┃ ┣ 📂apis
┃ ┃ ┣ 📂common
┃ ┃ ┗ 📂config
┃ ┗ 📜PennywayExternalApiApplication.java
┗📦pennyway-infra
┗ 📂src
┗ 📂main
┗ 📂java
┗ 📂kr.co.pennyway
┣ 📂infra
┃ ┣ 📂client
┃ ┣ 📂common
┃ ┗ 📂config
┗ 📜PennywayInfraApplication.java
처음에 위와 같은 패키지 구조를 정의하고, 각 모듈의 Application 파일에는 @SpringBootApplication 어노테이션을 추가하여 컴포넌트 스캔을 하도록 했다.
그러나 이렇게 하니 build는 되는데 gradlew --info test를 하면 @SpringBootConfigration 어노테이션이 중복되어 실패하는 이슈가 발생했다.
그래서 infra의 Application을 제거하고, external-api 모듈에서 infra의 config를 명시적으로 주입하는 방식을 채택했었다.
@Configuration
@EnableConfigurationProperties({
ServerProperties.class,
AppleOidcProperties.class,
GoogleOidcProperties.class,
KakaoOidcProperties.class
})
@EnablePennywayInfraConfig({
PennywayInfraConfigGroup.CACHE
})
public class InfraConfig {
}
📌 infra module 내 @CacheManager Bean resolve 실패
그랬더니 이번엔 infra 모듈에 정의한 CacheConfig에 등록한 CacheManager 빈을 컴파일 단계에서 추론하지 못하는 이슈가 생겼다. (웃기는 게 빌드는 또 된다.)
여튼 저 빨간 줄이 심하게 거슬렸던 관계로 문제를 분석해보고자 한다.
2. 고민 과정
📌 문제 분석
- Java의 switch문과 어노테이션은 컴파일 시점에 값이 결정되어야 한다.
- 그렇다면 @Cacheable 어노테이션의 cacheManager에 에러가 뜨는 이유는 CacheManager의 Bean을 컴파일 단계에서 추론할 수 없기 때문이라 판단했다.
- 그렇다면 대체 무엇이 문제였을까?
📌 CacheConfig에 @Configuration을 적용해보면?
@Configuration
@EnableCaching
public class CacheConfig {
...
}
- CacheConfig에 @Configuration을 추가해도 여전히 infra 모듈 내에서는 CacheManager Bean을 추론해내지 못한다.
- 그런데 external-api 모듈에서는 CacheManager를 추론해낸다. (???)
📌 Infra 모듈의 Application 클래스에 @SpringBootApplication을 적용해보면?
- PennywayInfraApplication에 다시 @SpringBootApplication을 추가하자 infra 모듈에서도 Bean resolve 문제가 해결되었다.
- 그러나 다시 처음 문제로 되돌아가서, 테스트가 불가능해지게 되었다.
3. 해결 방법
📌 Compile time에 Bean을 등록할 수 있을까?
- Spring 5에서는 컴파일 타임에 컴포넌트 인덱스를 생성하는 기능을 사용할 수 있다고 한다.
- 하지만 새로운 학습 비용이 부담스러웠고, 근본적인 문제의 원인을 찾아내지 못 한 채로 돌아가는 느낌이라 기각했다.
📌 @SpringBootApplication 충돌 문제는 왜 발생했던 것일까?
그렇다면 결국 @SpringBootApplication의 충돌은 왜 발생했던 걸까?
나와 관련있는 내용은 아니었지만, 스택 오버 플로우 어딘가의 글에서는 싱글 모듈에서 선택적으로 Application을 실행하는 방법에 대해 질문하는 글이 올라와 있었다.
그리고 거기에 "하나의 패키지 경로에 두 개 이상의 @SpringBootApplication이 정의되어 있어선 안 된다"라는 답변이 있었다.
무심코 넘어갔었다가, 곰곰히 생각을 해보니 @SpringBootApplication이 결국 @ComponentScan을 통해 서브 프로젝트 내의 Bean을 추론하기 위해 사용했었던 걸 떠올렸다.
그리고 @ComponentScan은 basePackage 경로를 통해 Scan을 할 경로를 지정한다.
그런데 external-api 모듈과 infra 모듈은 모두 kr.co.pennyway 경로를 기반으로 컴포넌트를 스캔하는데,
비록 다른 모듈이지만 이게 문제가 되지 않았을까?
📌 해결책
📦pennyway
┣📦pennyway-app-external-api
┃ ┗ 📂src
┃ ┗ 📂main
┃ ┗ 📂java
┃ ┣ 📂kr.co.pennyway
┃ ┣ 📂api
┃ ┃ ┣ 📂apis
┃ ┃ ┣ 📂common
┃ ┃ ┗ 📂config
┃ ┗ 📜PennywayExternalApiApplication.java
┗📦pennyway-infra
┗ 📂src
┗ 📂main
┗ 📂java
┗ 📂kr.co.pennyway
┗ 📂infra
┣ 📂client
┣ 📂common
┣ 📂config
┗ 📜PennywayInfraApplication.java // 한 단계 깊이를 높였다.
놀랍게도 infra 모듈의 깊이를 한 단계 낮추니 모든 문제가 해결되었다.
infra 모듈의 컴포넌트 스캔의 대상이 되는 패키지 경로가 kr.co.pennyway.infra가 되었으므로, 테스트 환경에서 또한 충돌 이슈가 발생하지 않게 된 것이다.
4. 왜 external-api 모듈은 되고, infra 모듈은 안 됐을까?
📌 @SpringBootApplication은 어떤 원리로 Compile time에 Bean을 추론하는가?
@EnableAutoConfiguration
@ComponentScan(basePackages = "kr.co.pennyway.infra")
public class PennywayInfraApplication {
}
문제를 해결하던 도중에 가장 의문이었던 것이 CacheManager에 @Configuration 어노테이션을 붙이는 순간, infra 모듈에서는 컴파일 시점에 Bean을 추론하지 못 하지만, external-api 모듈은 성공했다는 점이다.
그 시점의 유일한 차이는 external-api 모듈에만 @SpringBootApplication이 명시된 클래스가 존재했었다는 것인데, @SpringBootApplication에 붙어있는 어노테이션 중 @EnableAutoConfiguration과 @ComponentScan 어노테이션만 따로 infra 모듈에 정의해주니 마찬가지로 문제가 해결되었다.
(그런데 이렇게 하는 것보다 @SpringBootApplication을 사용하는 것을 권장한다고 한다.)
대략적으로 조사해본 결과 다음과 같은 이유였다.
- @ComponentScan: infra 모듈 내의 패키지의 Bean 또는 Component를 자동 스캔, 감지, 등록한다.
- @EnableAutoConfiguration: jar 종속성 기반으로 Spring을 구성하려는 방법을 추론하는데, 프로젝트가 의존성을 갖는 외부 컴포넌트까지 추론해낸다.
즉, external-api 모듈은 infra 모듈에 의존성을 가지므로 CacheConfig에 @Configuration을 명시해주면 @EnableAutoConfiguration으로 컴파일 타임에 Bean을 추론해낼 수 있었던 것 같다. (아직 확답을 못 하는 중)
⇒ (`24.04.11 내용 추가) 지금 생각해보니 멍청한 생각이었다. 내가 말하고자 했던 건 컴파일도 안 한 시점에 bean이 resolve가 되는지 안 되는지를 어떻게 판단하냐는 거였는데, 가만히 생각해보니 그건 IDE가 해주는 역할이지...🙄
하지만 infra 모듈에는 위 두 가지 어노테이션이 없었기 때문에 Bean resolve 에러가 발생했던 것이다.
물론 아직까지는 추론 단계이므로, 다음 포스팅은 @ComponentScan과 @EnableAutoConfiguration을 딥하게 뜯어볼 것이다.