개발에 AtoZ까지

[에러] required a bean of type '...' that could not be found — 긴 메시지에서 봐야 할 한 줄 본문

백엔드/Spring

[에러] required a bean of type '...' that could not be found — 긴 메시지에서 봐야 할 한 줄

AtoZ 개발자 2026. 8. 13. 19:30
반응형

새 클래스를 하나 추가하고 실행했더니 서버가 뜨지 않습니다. 콘솔에는 이런 화면이 나옵니다.

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.order.OrderService required a bean of
type 'com.example.order.OrderRepository' that could not be found.

Action:

Consider defining a bean of type 'com.example.order.OrderRepository' in your configuration.

Action에는 "빈을 정의하세요"라고만 적혀 있습니다. 그런데 코드를 보면 OrderRepository는 분명히 있습니다. 있는데 없다고 하니 막막해지죠.

이 글을 다 읽으면 세 가지가 정리됩니다. 첫째, 이 메시지에서 어디를 읽어야 하는지 알 수 있습니다. 둘째, 원인이 될 수 있는 다섯 가지 경로를 좁힐 수 있습니다. 셋째, 비슷하게 생긴 다른 메시지와 구분할 수 있습니다.

이 글은 Spring Boot·Spring Framework 공식 문서를 기준으로 정리했습니다. 버전에 따라 메시지가 조금씩 다를 수 있습니다.

메인 클래스의 패키지가 컴포넌트 스캔의 시작점이 됩니다

메시지는 세 조각으로 읽습니다

긴 스택트레이스에 겁먹기 전에 Description 한 줄만 보시면 됩니다. 여기에 필요한 정보가 다 들어 있습니다.

Parameter 0 of constructor in [주입받는 쪽] required a bean of type '[찾는 타입]' that could not be found
        ↑ 몇 번째 파라미터        ↑ 문제가 난 클래스        ↑ 없다고 판단한 타입
  • 주입받는 쪽: 이 클래스는 빈으로 등록됐습니다. 여기까지는 성공한 것이죠.
  • 찾는 타입: 이 타입의 빈이 컨테이너에 없습니다.
  • Parameter 0: 생성자의 첫 번째 파라미터입니다. 파라미터가 여러 개면 번호로 어느 것인지 알 수 있습니다.

즉 "OrderService는 빈으로 만들어지려는데, 그 재료인 OrderRepository가 컨테이너에 없다"는 뜻입니다. 클래스 파일이 없다는 말이 아니라 빈으로 등록되지 않았다는 말입니다. 이 구분이 출발점입니다.

원인 1 — 컴포넌트 스캔 범위 밖에 있다

가장 자주 나는 원인입니다. Spring Boot 공식 문서는 메인 애플리케이션 클래스를 다른 클래스들 위의 루트 패키지에 두라고 안내합니다. @SpringBootApplication이 붙은 클래스의 패키지가 암묵적인 기본 검색 패키지가 되기 때문입니다.

com
 └ example
    └ myapp
       ├ MyApplication.java        ← @SpringBootApplication (루트)
       ├ order
       │  ├ OrderService.java
       │  └ OrderRepository.java   ← 스캔됨
       └ payment
          └ PaymentService.java    ← 스캔됨

com
 └ other
    └ util
       └ MailSender.java           ← 스캔되지 않음

메인 클래스를 com.example.myapp.web 같은 하위 패키지에 두면 그 아래만 스캔합니다. 형제 패키지에 있는 클래스는 애노테이션이 붙어 있어도 등록되지 않습니다.

멀티 모듈 프로젝트에서 다른 모듈의 빈을 쓸 때도 같은 문제가 생깁니다. 이때는 스캔 범위를 명시합니다.

@SpringBootApplication(scanBasePackages = {"com.example.myapp", "com.example.common"})
public class MyApplication { }

공식 문서는 패키지 선언이 없는 default package 사용도 피하라고 합니다. 그 경우 모든 jar의 모든 클래스가 스캔 대상이 되어 예측하기 어려운 동작이 발생할 수 있습니다.

Spring Boot 공식 문서 — 메인 클래스를 다른 클래스 위의 루트 패키지에 두라는 권고 (출처: docs.spring.io, 확인일 2026-08-08)

원인 2 — 스테레오타입 애노테이션이 없다

클래스가 스캔 범위에 있어도 표식이 없으면 빈이 되지 않습니다.

// 빈으로 등록되지 않는다
public class OrderRepositoryImpl implements OrderRepository { }

// 등록된다
@Repository
public class OrderRepositoryImpl implements OrderRepository { }

@Component, @Service, @Repository, @Controller, @Configuration 중 하나가 필요합니다. 직접 만든 구현체를 인터페이스 타입으로 주입받는 구조에서 구현체 쪽 애노테이션을 빼먹는 실수가 흔합니다.

@Bean 메서드로 등록하는 경우에는 그 메서드가 있는 클래스에 @Configuration이 붙어 있어야 합니다.

@Configuration
public class ClientConfig {

    @Bean
    RestClient orderClient(RestClient.Builder builder) {
        return builder.baseUrl("https://api.example.com").build();
    }
}

원인 3 — JPA 리포지토리가 스캔되지 않았다

찾는 타입이 ...Repository인데 인터페이스만 있고 구현 클래스가 없다면 Spring Data JPA 리포지토리일 가능성이 큽니다. 이 인터페이스는 우리가 구현하지 않고 Spring Data가 런타임에 만들어 줍니다.

만들어지지 않는 경우는 두 가지입니다.

// 1) 리포지토리 인터페이스가 스캔 범위 밖
@EnableJpaRepositories(basePackages = "com.example.common.repository")

// 2) 엔티티가 스캔 범위 밖
@EntityScan(basePackages = "com.example.common.domain")

앞선 항목에서 본 것처럼 @SpringBootApplication의 패키지가 @Entity 검색 범위도 정합니다. 엔티티가 밖에 있으면 EntityManagerFactory 생성이 실패하고, 그 연쇄로 리포지토리 빈도 만들어지지 않습니다.

여기서 한 가지 더. DataSource 설정이 없어 DB 연결 자체가 실패하면 JPA 관련 빈이 모두 만들어지지 않습니다. 그때는 이 메시지 대신 Failed to determine a suitable driver class 계열 메시지가 먼저 나오는 게 보통입니다. 메모리 DB가 사라져 생기는 문제는 Database "mem:testdb" not found 글에 정리해 두었습니다. 에러 메시지 여러 개가 함께 뜨면 위쪽 것부터 해결하셔야 합니다.

원인 4 — 프로필이나 조건 때문에 비활성화됐다

빈 정의에 조건이 붙어 있으면 특정 환경에서만 등록됩니다.

@Service
@Profile("prod")                     // local 로 띄우면 등록되지 않는다
public class RealPaymentGateway implements PaymentGateway { }

@ConditionalOnProperty, @ConditionalOnMissingBean 같은 조건도 같은 결과를 만듭니다. "운영에서는 뜨는데 로컬에서만 실패한다"면 이 항목을 먼저 보세요.

// 로컬용 대체 구현을 두면 조건이 갈려도 컨텍스트가 뜬다
@Service
@Profile("!prod")
public class FakePaymentGateway implements PaymentGateway { }

원인 5 — 타입이 하나가 아니거나, 아예 다르다

메시지가 조금 다르면 원인도 다릅니다. 같이 자주 보게 되는 두 가지를 구분해 두시면 좋습니다.

# 후보가 여럿일 때
required a single bean, but 2 were found:
	- mysqlOrderRepository: defined in ...
	- redisOrderRepository: defined in ...

이건 없어서가 아니라 골라야 해서 나는 메시지입니다. @Primary로 기본을 정하거나 @Qualifier로 지정합니다.

@Primary
@Repository
public class MysqlOrderRepository implements OrderRepository { }

// 또는 주입 지점에서 지정
public OrderService(@Qualifier("redisOrderRepository") OrderRepository repo) { }
# 서로를 참조할 때
The dependencies of some of the beans in the application context form a cycle

이건 순환 참조입니다. Spring Boot 2.6부터 기본으로 막혀 있어 시작이 실패합니다. 설계를 바꾸는 게 정답이고, 급할 때만 임시로 허용할 수 있습니다.

# 임시 조치일 뿐, 구조를 고치는 편이 낫습니다
spring.main.allow-circular-references=true

메시지 형태별로 좁혀 들어가는 경로

생성자 주입이라면 확인이 더 쉽습니다

Spring Framework 공식 문서는 필수 의존성에 생성자 주입을 권합니다. 컨테이너가 시작할 때 의존성이 없으면 바로 실패하므로 문제를 늦게 발견하지 않게 되죠. 지금 보고 있는 이 에러가 바로 그 장치가 작동한 결과입니다.

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;

    // 생성자가 하나면 @Autowired 를 생략할 수 있습니다
    public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
        this.orderRepository = orderRepository;
        this.paymentGateway = paymentGateway;
    }
}

필드 주입(@Autowired private OrderRepository repo;)을 쓰면 이 시점에 실패하지 않고 실제 호출 때 NullPointerException으로 나타나 원인을 찾기 어려워집니다.

선택적 의존성이라면 없을 때도 뜨게 만들 수 있습니다.

// 없으면 Optional.empty
public OrderService(Optional<AuditLogger> auditLogger) { }

// 또는 ObjectProvider 로 지연 조회
public OrderService(ObjectProvider<AuditLogger> auditLoggerProvider) { }

Spring Framework 공식 문서의 @Autowired 기반 의존성 주입 섹션 (출처: docs.spring.io, 확인일 2026-08-08)

원인을 좁히는 진단 설정

추측을 줄이는 방법이 두 가지 있습니다.

# 1) 자동설정 조건 평가 보고서 — 무엇이 왜 적용되지 않았는지 나옵니다
debug=true
// 2) 실제로 등록된 빈 이름을 눈으로 확인
@Bean
ApplicationRunner beanNamePrinter(ApplicationContext ctx) {
    return args -> Arrays.stream(ctx.getBeanDefinitionNames())
            .filter(name -> name.startsWith("order"))
            .sorted()
            .forEach(System.out::println);
}

컨텍스트가 아예 뜨지 않을 때는 2번을 쓸 수 없으니, 먼저 문제 클래스의 생성자 파라미터를 하나씩 주석 처리해 어느 의존성에서 막히는지 좁히는 방식이 빠릅니다.

확인 순서

  • [ ] Description에서 찾는 타입의 전체 패키지 경로를 확인한다
  • [ ] 그 클래스가 메인 클래스 패키지 아래에 있는지 확인한다
  • [ ] 스테레오타입 애노테이션(@Service·@Repository 등)이 붙어 있는지 확인한다
  • [ ] JPA 리포지토리면 @EnableJpaRepositories·@EntityScan 범위를 확인한다
  • [ ] @Profile·@ConditionalOn... 조건이 붙어 있는지 확인한다
  • [ ] 에러가 여러 개면 가장 위의 것부터 해결한다
  • [ ] debug=true로 자동설정 보고서를 확인한다

다음에 볼 글

애플리케이션이 뜬 다음 만나는 문제들도 이어서 정리해 두었습니다. 트랜잭션 경계에서 나는 LazyInitializationException, 그 해결 과정의 MultipleBagFetchException, 그리고 요청 파라미터 바인딩 쪽의 Spring Boot 3.2 이후 파라미터 이름 에러입니다.

잘못된 내용이나 보충이 필요한 부분이 있으면 댓글로 알려 주시면 반영하겠습니다.

참고한 자료

  • Spring Boot Reference — Structuring Your Code: 메인 클래스 위치와 기본 검색 패키지, default package 주의 (확인일 2026-08-08)
  • Spring Framework Reference — Annotation-based Container Configuration, @Autowired와 생성자 주입 (확인일 2026-08-08)
  • Spring Framework Reference — @Primary, @Qualifier로 후보를 좁히는 방법 (확인일 2026-08-08)
  • Spring Boot Reference — Auto-configuration Report(debug=true) (확인일 2026-08-08)
반응형
Comments