들어가며
Spring 개발을 하다 보면 아래 에러는 거의 반드시 한 번은 만나게 된다.
No qualifying bean of type 'xxx' available
이 에러를 단순히 “Bean이 없어서 발생한 문제”로 보면 해결이 어렵다.
실제로는 대부분 의존성 주입 방식이나 구조 설계 문제에서 시작된다.
특히 @Autowired 필드 주입을 사용하고 있다면,
이 에러가 더 쉽게 발생하고 원인 파악도 어려워진다.
No qualifying bean이 실제로 발생하는 상황
실무에서 가장 흔한 케이스는 인터페이스 기반 설계에서 발생한다.
public interface MyService {
void execute();
}
구현체가 여러 개인 경우
@Service
public class AService implements MyService {
public void execute() {}
}
@Service
public class BService implements MyService {
public void execute() {}
}
이 상태에서 의존성을 주입하면 문제가 발생한다.
@Service
public class OrderService {
private final MyService myService;
public OrderService(MyService myService) {
this.myService = myService;
}
}
Spring 입장에서는 MyService 타입의 Bean이 두 개이기 때문에
어떤 것을 주입해야 할지 결정할 수 없다.
이때 발생하는 것이 바로 No qualifying bean 또는
expected single matching bean but found 2 계열의 오류다.
흔한 해결 방법과 한계
1. @Qualifier
@Service
public class OrderService {
private final MyService myService;
public OrderService(@Qualifier("AService") MyService myService) {
this.myService = myService;
}
}
특정 Bean을 지정할 수 있다.
하지만 한계가 명확하다.
- 문자열 기반이라 리팩토링에 취약함
- Bean 이름 변경 시 컴파일 에러 없음
- 실수해도 런타임에서만 문제 발생
즉, 동작은 하지만 구조적으로 안전하지 않다.
실무에서 더 많이 사용하는 해결 방법
1. @Primary
@Service
@Primary
public class AService implements MyService {
}
@Service
public class BService implements MyService {
}
기본 Bean을 지정하는 방식이다.
- 단순하다
- 유지보수가 쉽다
- 실무에서 가장 많이 사용된다
2. 커스텀 Qualifier (타입 안정성 확보)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MainService {}
@Service
@MainService
public class AService implements MyService {}
@Service
public class OrderService {
private final MyService myService;
public OrderService(@MainService MyService myService) {
this.myService = myService;
}
}
문자열이 아니라 타입 기반으로 선택하기 때문에
- 리팩토링에 안전
- 실수 가능성 낮음
그럼 @Autowired는 왜 문제인가
필드 주입을 보면 문제가 더 명확해진다.
@Service
public class OrderService {
@Autowired private MyService myService;
}
이 구조의 핵심 문제:
- 의존성이 코드에 드러나지 않는다
- 객체 생성 시 필요한 값이 명확하지 않다
- 테스트 작성이 어렵다 (Spring Context 의존)
- 문제 발생 시점이 늦다 (런타임)
즉, 문제를 숨기고 늦게 터뜨리는 구조다
생성자 주입이 해결하는 핵심 포인트
@Service
public class OrderService {
private final MyService myService;
public OrderService(MyService myService) {
this.myService = myService;
}
}
이 경우 Spring은 객체 생성 시점에
반드시 의존성을 해결해야 한다.
즉,
- 애플리케이션 시작 단계에서 바로 실패
- 문제 위치가 명확
- 잘못된 설계를 초기에 차단
@RequiredArgsConstructor로 완성되는 생성자 주입
생성자 주입의 단점은 코드가 길어진다는 점이다.
이걸 해결하는 것이 @RequiredArgsConstructor다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final MyService myService;
}
이 어노테이션은
- final 필드만을 대상으로
- 생성자를 자동 생성해 준다
즉, 아래 코드와 완전히 동일하다.
public OrderService(MyService myService) {
this.myService = myService;
}
핵심 포인트
- 코드 길이는 줄어든다
- 생성자 주입의 장점은 그대로 유지된다
- 실수 여지도 줄어든다
결과적으로
생성자 주입을 기본값처럼 사용할 수 있게 만들어준다
실무에서 체감되는 차이
필드 주입을 사용하면
- 문제가 나중에
- 예상하지 못한 위치에서 발생
생성자 주입(+ @RequiredArgsConstructor)을 사용하면
- 문제가 처음에
- 정확한 위치에서 발생
프로젝트가 커질수록
이 차이는 체감이 아니라 생산성 차이로 이어진다.
정리
- No qualifying bean 오류는 단순한 설정 문제가 아니다
→ 의존성 설계 문제의 신호 - @Autowired 필드 주입
→ 의존성을 숨기고 문제를 늦게 드러낸다 - 생성자 주입
→ 의존성을 명확히 하고 문제를 빠르게 드러낸다 - @RequiredArgsConstructor
→ 생성자 주입을 가장 간결하고 안전하게 사용할 수 있는 방법
'백엔드 개발' 카테고리의 다른 글
| JPA MyBatis 집계 한계와 Spring Batch로 해결하는 대용량 집계 처리 (0) | 2026.04.09 |
|---|---|
| Spring Snowflake ID 생성기 실무 적용기 (Auto Increment 한계와 해결 방법) (0) | 2026.04.08 |
| Spring Boot 3.x → 4.x 마이그레이션 (실무 기준 변경 포인트 정리) (0) | 2026.04.06 |
| Spring 동시성 문제 해결 (synchronized부터 Redis Lock, Lua, Redisson까지 정리) (0) | 2026.04.06 |
| 로컬 캐시(Local Cache), 언제 쓰고 어떻게 써야 할까? (Spring + Caffeine 정리) (0) | 2026.04.01 |