Spring Boot 기동 실패! UnsatisfiedDependencyException 원인 6가지 5분 진단
어제까지 잘 뜨던 앱이 오늘 아침 빨간 스택트레이스 200줄과 함께 죽었습니다. Error creating bean with name ..., No qualifying bean of type ... available. 화면 가득한 nested exception을 보면 심장이 철렁하지만, 결론부터 말씀드리면 당황하지 말고 맨 아래 한 줄부터 읽으면 됩니다.
이 글은 JWT 인증 실패나 Whitelabel 404 같은 '떠 있는 앱의 런타임 에러'가 아니라, 애플리케이션이 기동 자체에 실패하는 빈(Bean) 등록·주입 단계 문제에 한정해 다룹니다. 이 단계의 실패 패턴은 사실상 6가지뿐이고, 원인만 특정하면 대부분 한 줄로 해결됩니다.
스택트레이스를 거꾸로 읽는 법 (ssh -vvv처럼)
스택트레이스는 위에서부터 읽으면 길을 잃습니다. ssh -vvv 로그 디버깅처럼 맨 아래부터 거꾸로 읽어야 진짜 원인이 보입니다. 실제 예시를 봅시다.
Error creating bean with name 'orderController': ← ① 누가: orderController 생성 중
Unsatisfied dependency expressed through constructor parameter 0; ← ② 무엇을: 생성자 0번 파라미터 주입 실패
nested exception is
org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'com.example.PaymentService' available: ← ③ 왜: PaymentService 빈이 없음
expected at least 1 bean which qualifies as autowire candidate여기서 3요소만 뽑으면 끝납니다.
- 누가 (생성 실패한 빈):
orderController - 무엇을 (주입받으려던 대상): 생성자 파라미터 0번
- 왜 (근본 원인):
NoSuchBeanDefinitionException→PaymentService빈이 컨테이너에 없음
맨 아랫줄 예외 타입에 따라 원인 방향이 갈립니다.
| 맨 아래 예외 메시지 | 의미 | 의심 원인 |
|---|---|---|
NoSuchBeanDefinition: No qualifying bean ... expected at least 1 | 빈이 0개 | 컴포넌트 누락 / 스캔 범위 밖 / 조건부 빈 미생성 |
NoUniqueBeanDefinitionException: expected single matching bean but found 2 | 빈이 2개 이상 | 같은 타입 다중 등록, @Qualifier 필요 |
Requested bean is currently in creation / circular reference | 서로를 참조 | 생성자 순환참조 |
원인 6가지 진단 표 + 복붙 해결 코드
먼저 한눈에 보는 매핑 표입니다.
| # | 에러 단서 | 원인 | 해결책 |
|---|---|---|---|
| 1 | No qualifying bean ... expected at least 1 | @Component 누락 또는 스캔 범위 밖 | 어노테이션 부착 / @ComponentScan(basePackages=...) |
| 2 | expected single matching bean but found 2 | 구현체 다중 | @Qualifier / @Primary |
| 3 | currently in creation | 생성자 순환참조 | 설계 분리 / @Lazy |
| 4 | 빈이 통째로 없음 | @ConditionalOnProperty·@Profile 미충족 | 프로퍼티/프로파일 활성화 |
| 5 | NoUniqueBeanDefinitionException | 동일 타입 빈 2개 | @Primary 또는 명시 주입 |
| 6 | 테스트에서만 실패 | 슬라이스 컨텍스트에 빈 누락 | @MockBean / 슬라이스 범위 조정 |
원인 1. 컴포넌트 누락 또는 스캔 범위 밖
가장 흔합니다. @SpringBootApplication은 자신이 위치한 패키지와 그 하위만 스캔합니다. com.example.common 같은 별도 루트에 빈이 있으면 못 잡습니다.
@SpringBootApplication
@ComponentScan(basePackages = {"com.example.app", "com.example.common"})
public class Application { }물론 클래스에 @Service/@Component 자체를 빠뜨린 경우도 많으니 먼저 확인하세요.
원인 2 & 5. 구현체 다중 → @Qualifier / @Primary
PaymentService 구현체가 KakaoPayService, NaverPayService 둘이면 스프링은 무엇을 주입할지 몰라 NoUniqueBeanDefinitionException을 던집니다.
@Service @Qualifier("kakaoPay")
public class KakaoPayService implements PaymentService { }
@Service @Primary // 기본 후보 지정
public class NaverPayService implements PaymentService { }
// 주입 시 명시 선택
public OrderController(@Qualifier("kakaoPay") PaymentService paymentService) { ... }원인 3. 생성자 순환참조 → @Lazy
A가 B를, B가 A를 생성자로 주입하면 순환참조입니다. Spring Boot 2.6+ 부터 spring.main.allow-circular-references 기본값이 false라 기동이 곧바로 막힙니다(과거 필드 주입으로 숨어 있던 문제가 생성자 주입 권장으로 조기에 드러나는 셈입니다). 정석은 책임 분리지만, 급할 땐:
public AService(@Lazy BService bService) {
this.bService = bService;
}원인 4. 조건부 빈 미생성
@ConditionalOnProperty 조건이 안 맞으면 빈 자체가 안 만들어집니다.
@Bean
@ConditionalOnProperty(name = "feature.pay.enabled", havingValue = "true")
public PaymentService paymentService() { ... }→ application.yml에 feature.pay.enabled: true를 넣어야 빈이 생깁니다. @Profile("prod")도 마찬가지입니다.
원인 6. 테스트에서만 실패
@WebMvcTest는 컨트롤러 슬라이스만 로딩하므로 서비스 빈이 없습니다. 의존 빈은 @MockBean으로 채워주세요.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@MockBean PaymentService paymentService;
@Autowired MockMvc mockMvc;
}실무 한마디
저는 신규 프로젝트 셋업 직후 가장 먼저 패키지 구조를 com.example.app 루트 하나로 정렬합니다. 멀티 루트 패키지에서 basePackages를 빠뜨려 발생하는 원인 1번이 전체 빈 주입 이슈의 절반 이상을 차지하더군요. 그리고 자동설정 빈이 안 생길 땐 라이브러리 코드가 아니라 build.gradle 의존성 누락을 먼저 의심하는 게 빠릅니다.
흔한 함정 & 검증 루틴
- 멀티모듈:
core모듈의 빈이api모듈에서 안 잡히면, 의존성과 함께 스캔 경로(@ComponentScan)에 그 패키지가 포함됐는지 확인하세요. - 의존성 누락으로 자동설정 빈 미생성: 라이브러리가 없으면
@ConditionalOnClass자동설정이 통째로 비활성화됩니다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'@Autowired(required=false)·Optional우회의 함정: 에러는 사라지지만 런타임에 NPE로 더 늦게 터집니다. 근본 원인을 가리는 임시방편일 뿐입니다.- Actuator로 실제 등록 빈 확인: 추측 대신 눈으로 봅시다.
management.endpoints.web.exposure.include=beans/actuator/beans를 호출해 문제의 빈 이름이 목록에 있는지 확인하면 "스캔이 안 된 건지, 조건 때문에 안 만들어진 건지"가 즉시 갈립니다. 참고로 Spring Boot 3.x는 Jakarta 네임스페이스(jakarta.*) 전환 후라 javax.* 기반 라이브러리는 빈 스캔/자동설정에서 누락될 수 있고, GraalVM Native Image에서는 리플렉션 기반 빈 등록이 빌드타임에 빠질 수 있어 별도 힌트가 필요합니다.
결론: 진단 체크리스트 1장 요약
- 스택트레이스 맨 아래부터 읽어 예외 타입 확인
No qualifying bean→ 컴포넌트 누락/스캔 범위/조건부 빈expected single ... found 2→@Qualifier/@Primarycurrently in creation→ 순환참조, 설계 분리 또는@Lazy- 테스트만 실패 → 슬라이스 범위 +
@MockBean - 헷갈리면
/actuator/beans로 실제 등록 여부 확인
"빨간 줄 200개"는 사실 맨 아래 한 줄을 위한 호위무사일 뿐입니다. 거꾸로 읽기를 습관화하고, 평소 Actuator로 빈 목록을 사전 점검하는 루틴을 들이면 기동 실패는 더 이상 무섭지 않습니다.
자주 묻는 질문 (FAQ)
Q. @Component를 붙였는데도 No qualifying bean이 뜹니다.
A. 거의 100% 스캔 범위 밖입니다. 해당 클래스 패키지가 @SpringBootApplication이 위치한 패키지의 하위가 아니라면 @ComponentScan(basePackages=...)에 명시하세요.
Q. @Repository는 인식되는데 제 클래스만 안 됩니다. A. 그 Repository는 자동설정/스캔 범위 안에 있고 여러분 클래스만 밖이라는 신호입니다. 패키지 위치와 어노테이션 부착 여부를 함께 확인하세요.
Q. 테스트에서만 빈 주입이 실패합니다.
A. @WebMvcTest·@DataJpaTest 같은 슬라이스는 일부 빈만 로딩합니다. 필요한 의존성은 @MockBean으로 채우거나, 전체 컨텍스트가 필요하면 @SpringBootTest를 쓰세요.
Q. 순환참조인지 어떻게 알 수 있나요?
A. 메시지에 currently in creation 또는 circular reference가 보이면 확정입니다. Spring Boot 2.6+는 기본적으로 막으므로 기동 즉시 드러납니다.
Q. 구현체가 2개일 때 무조건 @Primary를 써도 되나요?
A. "기본 구현이 명확히 하나"일 때만 @Primary가 적절합니다. 상황별로 골라 써야 한다면 주입 지점마다 @Qualifier로 명시하는 편이 안전하고 의도가 분명합니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...