클래스를 만들었다고 해서 Spring이 그 클래스를 자동으로 아는 건 아니에요.지난 글에서는 ApplicationContext가 빈(bean)을 만들고 연결한다고 했어요. 그러면 자연스럽게 다음 질문이 생겨요.
“그럼 어떤 클래스가 빈으로 등록되는 거지?"처음에는 “Annotation을 붙이면 되겠지” 정도로 넘어가기 쉬워요. 그런데 실제 프로젝트에서는 빈이 안 잡혀서 앱이 시작하지 않거나, 반대로 원하지 않은 빈이 잡혀서 테스트가 이상해지는 일이 자주 생겨요. 오늘은 이 지점을 볼게요. 목표는 모든 등록 방식을 외우는 게 아니에요. Spring Boot 앱에서 빈이 ApplicationContext에 들어오는 대표적인 길을 나누고, “Spring이 못 찾는다”는 말을 더 정확한 질문으로 바꾸는 거예요.
"@Service를 붙였는데 왜 못 찾는다고 하지?"
"@Bean이랑@Component는 뭐가 다른 거지?"
"자동 설정(auto-configuration)은 내가 만든 빈이랑 어떤 관계지?”
이 글은 Spring Boot 4.1.0 공식 문서의
@SpringBootApplication, 코드 구조, 자동 설정 설명과 Spring Framework 공식 문서의 클래스패스 스캔(classpath scanning), @Bean 설명을 확인해 작성했어요. 컴포넌트 스캔과 빈 등록 개념은 Spring Boot 전반에서 이어지는 이야기지만, 정확한 조건 Annotation이나 자동 설정 작성 규칙은 뒤 글에서 더 깊게 다룰게요.먼저 흔한 시작 실패부터 볼게요
주문 서비스가 결제 클라이언트를 필요로 한다고 해볼게요.PaymentClient 클래스도 있고, @Component도 붙어 있으니까요.
그런데 앱을 실행하면 이런 방향으로 실패할 수 있어요.
PaymentClient 빈이 등록되지 않았다는 뜻이에요.
여기서 질문을 바꿔야 해요.
“클래스가 있나?”가 아니라, “그 클래스가 빈으로 등록되는 경로 안에 있나?”
빈이 들어오는 길은 하나가 아니에요
Spring Boot 애플리케이션에서 빈이 등록되는 대표적인 길은 세 가지예요.
이 세 길은 모두 결과적으로 ApplicationContext에 빈 정의를 넣어요. 하지만 “어디서 찾는지”, “누가 만들기로 결정하는지”, “언제 물러나는지”가 달라요.
이 그림에서 핵심은 빈이 “한 방식”으로만 들어오지 않는다는 점이에요. 내가 만든 서비스는 스캔으로 들어오고, 외부 클라이언트는
@Bean으로 들어오고, Boot 기본값은 조건부 자동 설정으로 들어올 수 있어요.
컴포넌트 스캔은 “정해진 범위 안”에서만 찾아요
Spring Boot 프로젝트에는 보통 시작 클래스가 있어요.@SpringBootApplication은 여러 기능을 한 번에 켜는 편의 Annotation이에요. 그중 하나가 컴포넌트 스캔이에요.
처음에는 이렇게 기억하면 좋아요.
시작 클래스가 있는 패키지와 그 아래 패키지에서 컴포넌트 후보를 찾아요.예를 들어 시작 클래스가
com.example.order에 있으면 아래 구조는 자연스러워요.
com.example.order
OrderApplication.java
controller
OrderController.java
service
OrderService.java
repository
OrderRepository.java
controller, service, repository가 모두 com.example.order 아래에 있으니까 스캔 범위 안에 들어와요.
반대로 이런 구조는 조심해야 해요.
com.example.order
OrderApplication.java
com.example.payment
PaymentClient.java
PaymentClient는 com.example.order 아래에 있지 않아요. 클래스가 있고 @Component가 붙어 있어도, 기본 스캔 범위 밖이면 빈으로 등록되지 않을 수 있어요.
그래서 보통은 시작 클래스를 더 위쪽 공통 패키지에 둬요.
com.example
OrderApplication.java
order
OrderController.java
OrderService.java
payment
PaymentClient.java
order와 payment가 모두 com.example 아래에 있으니 훨씬 예측하기 쉬워요.
@Service와 @Component는 역할 표시이기도 해요
컴포넌트 스캔은 아무 클래스나 전부 빈으로 만들지 않아요. 후보가 되는 표시가 필요해요.
가장 기본은 @Component예요.
이 Annotation들은 단순한 장식이 아니에요. 컴포넌트 스캔의 후보가 되게 만들고, 동시에 읽는 사람에게 역할을 알려줘요.
예를 들어
OrderService에는 @Component를 붙여도 빈 등록은 될 수 있어요.
@Service가 더 낫죠.
@Service는 “이 클래스는 업무 규칙을 담는 곳이에요”라는 의도를 더 잘 보여줘요.
컨트롤러는
@RestController, 서비스는 @Service, 저장소는 @Repository처럼 역할을 드러내는 편이 좋아요. @Component는 역할 이름이 따로 맞지 않는 일반 컴포넌트에 쓰면 충분해요.@Bean은 “이 메서드 결과를 빈으로 등록해줘”라는 뜻이에요
컴포넌트 스캔은 내가 만든 클래스에 Annotation을 붙이는 방식이에요.
그런데 모든 객체에 @Component를 붙일 수 있는 건 아니에요. 예를 들어 Java 표준 라이브러리의 Clock을 애플리케이션에서 공통으로 쓰고 싶다고 해볼게요. Clock 클래스에 우리가 @Component를 붙일 수는 없죠.
이럴 때 @Bean을 쓸 수 있어요.
이제 다른 빈은clock()메서드가 돌려주는 객체를clock이라는 이름의 빈으로 등록해줘요.
Clock을 생성자로 받을 수 있어요.
Clock 자체는 @Component가 붙은 클래스가 아니에요. 하지만 TimeConfig의 @Bean 메서드가 빈 정의를 만들어줬기 때문에 컨테이너가 주입할 수 있어요.
컴포넌트 스캔과 @Bean은 책임이 달라요
둘 다 빈을 등록할 수 있으니 처음에는 아무거나 써도 되는 것처럼 보일 수 있어요.
하지만 설계 의도가 달라요.
예를 들어
OrderService는 보통 컴포넌트 스캔이 자연스러워요.
PaymentClient를 만드는 결정이 서비스 안에 숨어 있지 않고, 설정 영역에 올라와 있다는 점이에요.
자동 설정은 조건을 보고 “기본 빈”을 넣어요
Spring Boot를 쓰면 내가 직접 등록하지 않았는데도 많은 빈이 준비돼요. 웹 스타터를 넣으면 웹 요청 처리에 필요한 기본 구성이 들어오고, 데이터 접근 스타터를 넣으면 관련 자동 설정이 후보로 올라와요. 이건 Spring Boot가 조건을 보고 기본 설정을 적용하기 때문이에요. 여기서 조건이라는 말이 중요해요. 자동 설정은 보통 “이 클래스가 클래스패스에 있나요?”, “이미 사용자가 같은 역할의 빈을 등록했나요?”, “이 프로퍼티가 켜져 있나요?” 같은 질문을 해요. 예를 들어 Boot의 자동 설정이나 라이브러리 설정에서는 이런 식의 조건을 자주 볼 수 있어요.사용자가그러면 사용자는 필요할 때 자기 빈을 등록해서 기본값을 바꿀 수 있어요.PaymentClient빈을 직접 등록하지 않았다면 기본PaymentClient를 만들어줘요.
지금은 자동 설정이 조건을 보고 빈을 등록한다는 감각만 잡으면 충분해요. 스타터, 의존성 관리, 조건 리포트, 사용자 빈으로 기본값을 바꾸는 흐름은 자동 설정 글에서 자세히 이어갈게요.
”못 찾는다”는 말을 더 작게 쪼개볼게요
앱이 시작하지 않고 “빈을 찾을 수 없다”는 메시지가 나오면, 바로 Annotation을 더 붙이기 전에 원인을 나눠보는 게 좋아요.
예를 들어 아래 코드는 인터페이스만 보고는 빈을 만들 수 없어요.
PaymentClient 타입이 필요할 때 HttpPaymentClient 빈을 후보로 볼 수 있어요.
반대로 구현체가 둘이면 문제가 달라져요.
@ComponentScan으로 범위를 넓히는 건 마지막에 가까워요
스캔 범위 밖 클래스가 문제라면 @ComponentScan으로 범위를 직접 지정할 수 있어요.
com.example
OrderApplication.java
order
payment
@ComponentScan 설정 없이도 읽는 사람이 범위를 예측할 수 있어요.
물론 예외는 있어요. 여러 모듈을 조립하는 애플리케이션, 외부 패키지의 컴포넌트를 의도적으로 포함해야 하는 구조, 프레임워크나 라이브러리 코드에서는 명시적인 스캔 범위가 필요할 수 있어요.
중요한 기준은 이거예요.
스캔 범위를 넓히기 전에, 시작 클래스 위치와 패키지 경계가 먼저 맞는지 보세요.
실무에서는 등록 경로가 코드 리뷰 포인트가 돼요
빈 등록 방식은 단순 취향이 아니에요. 나중에 테스트, 설정, 운영 추적성까지 이어져요. 서비스 클래스에 필요한 객체를 직접 만들면 컨테이너가 볼 수 없어요.PaymentClient를 바꾸기 어렵고, 설정값도 서비스 안에 숨어들기 쉬워요.
반대로 빈으로 등록하고 생성자로 받으면 관계가 드러나요.
이 질문들은 “Spring이 알아서 해주겠지”를 더 구체적인 설계 판단으로 바꿔줘요.
오늘의 핵심 경계를 한 번에 놓아볼게요
빈 등록을 읽을 때는 아래 순서로 보면 좋아요.1
필요한 클래스가 실제로 있는지 확인해요
이름이 비슷한 클래스나 test source가 아니라, 실행 클래스패스에 기대한 구현이 들어 있는지 봐요. 컴파일됐다는 사실만으로 빈 등록까지 보장되지는 않아요.
2
등록 경로가 표시됐는지 확인해요
@Service, @Component, @Bean처럼 Spring이 빈 정의를 만들 수 있는 표시를 찾아요. 외부 설정이라면 @Import나 자동 설정을 통해 들어오는지도 확인해요.3
스캔 또는 import 범위를 확인해요
시작 클래스의 하위 패키지인지, 명시한 scan 범위에 포함되는지 봐요. 등록 표시가 있어도 탐색 범위 밖이면 빈 정의가 만들어지지 않아요.
4
등록 조건을 확인해요
profile, property, classpath, missing bean 조건이 현재 환경에서 참인지 살펴봐요.
--debug의 조건 평가 리포트가 자동 설정을 추적할 때 도움이 돼요.5
주입 후보가 하나로 결정되는지 확인해요
필요한 타입의 빈이 없거나 여러 개라면 주입이 실패해요. 후보가 여러 개일 때는 역할을 다시 나누거나
@Qualifier, @Primary로 의도를 드러내요.자, 정리해볼까요?
- 클래스가 있다고 해서 자동으로 빈이 되는 건 아니에요. ApplicationContext에 등록되는 경로 안에 있어야 해요.
-
컴포넌트 스캔(component scan)은 시작 클래스 패키지와 그 하위 패키지에서
@Service,@Controller,@Repository,@Component같은 후보를 찾아요. -
@Bean은 메서드가 돌려주는 객체를 빈으로 등록하는 방식이에요. 외부 라이브러리 객체나 생성 과정이 있는 객체에 잘 어울려요. - 자동 설정(auto-configuration)은 클래스패스, 기존 빈, 프로퍼티 같은 조건을 보고 기본 빈을 넣거나 물러나요.
- “빈을 찾을 수 없다”는 말은 클래스가 없다는 뜻이 아닐 수 있어요. 스캔 범위 밖이거나, 설정 클래스가 등록되지 않았거나, 조건 때문에 빠졌을 수 있어요.
- 실무에서는 빈 등록 방식이 패키지 구조, 테스트 대역, 설정 관리, 운영 추적성까지 이어지는 설계 포인트가 돼요.