현재 클린 아키텍처를 학습하기 위해 코틀린을 통해 간단한 프로젝트를 진행하고 있으며, 파사드 패턴과 포트/어댑터 패턴을 적용한 헥사고날 아키텍처를 구현하려고 노력 중에 있다. 그리고 해당 과정에서 스프링 어플리케이션이 컴포넌트 스캔을 제대로 하지 못하는 문제를 발견하였고, 이를 성공적으로 해결하였다.
설명하기에 앞서서, 앞으로 설명하고자 하는 프로젝트는 총 5개의 모듈을 구축해서 운영하고 있으며, 모듈이 가지는 책임은 다음과 같다:
- domain: 순수 코틀린으로 이루어진 모듈
- independent: 모든 모듈에서 공통적으로 쓰이게 되는 모듈
- infra:persistence-db: JPA 관련 로직으로 이루어진 모듈
- infra:security: Spring Security 관련 로직이 이루어진 모듈(토큰 발급에 대한 책임을 가짐)
- presentation: 게이트웨이 서버에 대한 책임을 지니는 모듈
포트/어댑터 패턴을 적용하려고 노력하였기에, DI를 적용하기 위한 수많은 고민을 거쳤다. usecase 인터페이스를 통해 인바운드 포트를 구현해주고, 외부 API를 담당하고 있는 모듈과의 연결도 port 인터페이스를 통해 구현해주어, 모든 in/out 경우에 대한 의존성을 역전시켜 주었다. 그리고 문제는 해당 과정에서 발생하였다.
문제: 빈 오토와이어링 실패
기존의 토큰을 발급하는 로직에 DI를 적용하기 위해, 나는 다음과 같은 구조로 설계를 변경해서 진행했다.
SecurityPort와 TokenService는 domain 모듈에 위치하고, JwtProvider 클래스는 Spring Security 의존성을 통해 토큰을 생성하기에 infra:security 모듈에 위치시켰다.
TokenService 클래스에서 토큰을 발급하기 위해 JwtProvider 클래스를 직접적으로 호출하려면 domain 모듈에 불필요한 의존성이 추가되므로, SecurityPort 인터페이스를 인자로 받아 의존성을 역전해 주었다. 그리고 JwtProvider는 해당 SecurityPort 인터페이스를 하기에, 정상적인 경우라면 SecuriyPort가 IDE에서 정상적으로 컴포넌트로 인식이 되어야 했다. 하지만, 실제로는 그렇지 않았다.
Description:
Parameter 0 of constructor in site.yourevents.auth.service.AuthService
required a bean of type 'site.yourevents.auth.port.out.SocialPort' that could not be found.
Action:
Consider defining a bean of type 'site.yourevents.auth.port.out.SocialPort' in your configuration.
다음과 같은 오류가 뜨면서 빌드가 실패했다. 스프링 어플리케이션에서 자동으로 SocialPort의 Bean을 인식하는 데 실패했다는 것이다. 분명히 JwtProvider 클래스가 SecuriyPort 인터페이스를 다음과 같이 제대로 구현했기에, 빈 오토와이어링에 있어서 문제가 생기면 안 되는 상황이었다.
원인 찾기
이에 해당 문제의 원인을 찾기 시작하였고, 최우선적으로는 컴포넌트 스캔 관련하여 문제가 있을 것이라 판단해 해당 과정에서 문제를 찾기 시작했다.
시도 1: 스프링 어플리케이션의 컴포넌트 스캔 범위
스프링은 기본적으로 @SpringBootApplication 어노테이션을 통해 어플리케이션을 실행하는 메인 클래스가 위치하는 패키지가 BasePackage로 자동으로 설정된다. 그리고 해당 패키지의 하위 패키지의 컴포넌트를 가져올 수 있다.
.
├── module-domain
│ └── src
│ ├── main
│ │ ├── kotlin
│ │ │ └── site
│ │ │ └── yourevents
│ │ │ ├── auth
│ │ │ │ ├── port
│ │ │ │ │ └── out
│ │ │ │ │ ├── SecurityPort.class
│ │ │ │ ├── service
│ │ │ │ │ └── TokenService.class
├── module-infrastructure
│ └── security
│ └── src
│ ├── main
│ │ ├── kotlin
│ │ │ └── site
│ │ │ └── yourevents
│ │ │ ├── jwt
│ │ │ │ ├── JwtProvider.kt
└── module-presentation
└── src
├── main
│ ├── kotlin
│ │ └── site
│ │ └── yourevents
│ │ ├── YourEventsApplication.kt
하지만 메인 클래스인 YourEventsApplication.kt는 site.yourevents 패키지에 위치해 있었고, 문제가 되는 클래스들은 전부 해당 패키지의 하위에 위치해 있어서 문제가 되는 부분은 아니었다.
그래서 원인의 이유를 다음으로 의존성 부분에서 찾으려고 노력했고, 해당 부분에서 결국 문제를 해결할 수 있었다.
최종 원인: 모듈 간 의존성 명시
위의 패키지 구조에서 알 수 있듯이, 해당 어플리케이션은 presentation 모듈에 위치한 메인 클래스에서 실행된다. 그 뜻은 즉, 해당 모듈에서 특정 모듈에 대해 컴포넌트 스캔을 진행해주고 싶으면 해당 모듈에 대한 의존성을 명시해주어야 한다는 것이다.
그래서 presentation 모듈의 build.gradle.kts 파일을 확인해 본 결과, 다음과 같이 security 모듈에 대한 의존성 명시가 되어 있지 않는 것을 발견할 수 있었다.
따라서 다음과 같이 security 모듈에 대한 의존성을 선언해주었다.
그러면 이제 다음과 같이 SecurityPort 구현체에 대해 정상적으로 Bean 인식이 되는 모습을 볼 수 있다.
결론
멀티 모듈 구성에서는 각 모듈 간의 의존성을 명확히 선언하는 것이 무척 중요하다. 하지만 미숙함으로 인해 필요로 했던 모듈에 대한 의존성을 명시해주지 않아 벌어졌던 오류였고, 이를 명시해 주어 해결해 줄 수 있었다.
멀티 모듈을 통해 프로젝트를 설계하면, 어플리케이션을 실행하는 메인 클래스가 위치한 모듈은 필요로 하는 모듈에 대해 의존성을 선언해주어야 한다. 이를 통해 스프링은 정상적으로 컴포넌트 스캔을 진행하게 된다.
또한, 메인 클래스가 위치하는 패키지가 프로젝트의 최상단에 위치하도록 구성해 하위 패키지에 있는 모든 컴포넌트가 자동으로 인식될 수 있도록 해야 한다. 만약 특정 패키지를 추가적으로 스캔해야 한다면, @ComponentScan 어노테이션을 사용하여 명시적으로 지정해 주는 것도 또다른 방법이 될 수 있겠다!
'스프링 부트 > 오류' 카테고리의 다른 글
[Spring Security] 스프링에서 CORS를 해결하는 또다른 방법 (0) | 2024.12.15 |
---|---|
[Kotlin] Spring Security에서 코틀린 DSL 적용하기 (0) | 2024.11.25 |
[JPA] ExecutorService를 통한 엔티티가 변경되지 않는 않는 오류 (1) | 2024.11.19 |
[gradle] 멀티 모듈에서 특정 모듈을 삭제했을 때 빌드가 실패하는 오류 (0) | 2024.11.19 |