빈이란 무엇인가: 객체에서 빈으로
들어가며
1편에서 스프링 컨테이너가 "빈을 생성하고 조립하고 관리한다"고 썼다.
그런데 그 빈이 도대체 뭔가. @Component를
붙이면 빈이 된다고는 배웠지만, 그게 그냥 자바 객체와 어떻게 다른지는
설명하기 어렵다. 어떤 사람은 "JavaBeans 규약을 따르는 객체"라고 하고,
어떤 사람은 "스프링이 관리하는 객체"라고 한다. 둘 다 반쯤만 맞다.
이 주제에서 특히 막히기 쉬운 지점이 몇 가지 있다.
- 컨테이너 밖에서
new한 객체는@Autowired도@Transactional도 안 먹힌다 — 빈이 아니기 때문이다 @Component안에@Bean메서드를 넣으면 싱글톤이 깨진다 — lite mode라는 함정이 있다@Qualifier와@Primary가 같이 있으면@Qualifier가 이긴다 — 반대로 알고 있는 경우가 많다@Bean메서드 이름을 리네이밍하면 다른 곳이 조용히 부서진다 — 메서드 이름이 빈 이름이기 때문이다- Spring 6.1+에서
-parameters플래그가 없으면 이름 매칭이 실패한다 — IDE에서는 돌다가 CI에서 깨진다
이 글은 유래 → 객체 vs 빈 → BeanDefinition → 등록 → 탐색 순으로, Spring Framework 6.x(Java 17+) 기준으로 정리한다.
목차
- 1) JavaBeans에서 Spring Bean으로: 이름만 빌린 단어
- 2) 그냥 객체와 빈의 차이: 컨테이너가 아는 객체
- 3) BeanDefinition: 빈의 설계도
- 4) 등록과 생성은 다른 사건
- 5) 빈 등록 세 가지 길: @Component, @Bean, XML
- 6) @Configuration의 비밀: CGLIB와 lite mode
- 7) 이름과 타입으로 빈 찾기: @Autowired의 내부 규칙
- 8) 실무에서 이렇게 읽고 쓴다
- 9) 한 줄 정리
1) JavaBeans에서 Spring Bean으로: 이름만 빌린 단어
1-1) JavaBeans 규약이란 무엇이었는가
"빈(Bean)"이라는 말은 스프링이 만든 단어가 아니다. 1997년에 Sun Microsystems가 발표한 JavaBeans 스펙이 원조다. 당시 자바에서 재사용 가능한 GUI 컴포넌트(예: 버튼, 체크박스)를 IDE에 드래그 앤 드롭으로 붙이기 위해 만든 규약이었다. 규약은 세 가지였다.
- no-arg 생성자 — 툴이 리플렉션으로 인스턴스를 만들 수 있어야 한다
- getter/setter 기반 프로퍼티 — 속성을 일관된 방식으로 읽고 쓴다
Serializable구현 — 상태를 파일에 저장하고 네트워크로 옮길 수 있다
이 규약은 GUI 컴포넌트 모델이라는 당시의 요구사항에서 나온 것이지, 객체 지향 설계의 보편 원리가 아니었다. 2000년대 들어 GUI 빈 시장이 사라지면서 규약 자체는 잊혀졌고, 남은 건 "bean = 컨테이너가 다루는 컴포넌트"라는 개념적 껍데기뿐이었다.
1-2) Spring Bean은 규약을 따르지 않는다
스프링은 이 껍데기만 가져왔다. 철학만 계승했지
규약은 강제하지 않는다. Spring Bean은 no-arg 생성자가
없어도 되고(생성자 주입이 오히려 권장된다), Serializable이
아니어도 되고, getter/setter가 없어도 된다. 즉 Spring Bean은 JavaBeans가
아니다. 단어만 같을 뿐이다.
그럼 Spring Bean은 뭔가. 스프링 공식 레퍼런스의 문장이 가장 정확하다.
"In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container."
정리하면 이렇다 — Spring Bean은
ApplicationContext가 생성하고, 조립하고, 관리하는 객체
인스턴스다. 이 정의에는 "JavaBeans 규약"도, "특정
인터페이스"도, "상속"도 들어있지 않다. 오직 "컨테이너가 아는
객체인가" 하나만 묻는다.
2) 그냥 객체와 빈의 차이: 컨테이너가 아는 객체
2-1) 객체 vs 빈: 차이는 컨테이너에 달렸다
같은 클래스의 인스턴스라도 어디서 만들어졌느냐에
따라 운명이 갈린다. 컨테이너가 만든 인스턴스는 빈이고,
new로 만든 인스턴스는 그냥 객체다. 클래스 파일은 똑같지만
사용할 수 있는 기능이 완전히 다르다.
| 항목 | new 객체 |
스프링 빈 |
|---|---|---|
| 생성 주체 | 개발자 코드 | 컨테이너 |
| 생명주기 관리 | JVM/GC에 의존 | 컨테이너가 init/destroy 관리 |
| 의존성 주입(DI) | 불가 | 가능 |
| AOP 프록시 | 불가 | 가능 |
@Transactional |
무시됨 | 동작 |
| 다른 빈에서 참조 | 불가 | @Autowired로 가능 |
ApplicationContextAware |
불가 | 가능 |
컨테이너는 자신이 만든 객체에만 DI를 넣고, 프록시를 감싸고, 생명주기
콜백을 붙인다. new로 만든 객체는 컨테이너의 시야
밖이기 때문에 어떤 추가 기능도 붙지 않는다.
2-2) 코드로 보는 "빈이 아닐 때"의 함정
아래 코드는 기술적으로 컴파일되고 실행도 된다. 그러나 의도한 대로 동작하지 않는다.
@Service
public class OrderService {
// 피하기: new로 직접 만든 EmailSender
private final EmailSender emailSender = new EmailSender();
public void placeOrder(Order order) {
// ...
emailSender.send(order); // @Transactional이 안 붙는다
}
}
public class EmailSender {
@Transactional
public void send(Order order) {
// 트랜잭션이 시작될 것 같지만, 시작되지 않는다
}
}EmailSender에 @Transactional을 붙여놨지만,
스프링은 이 객체를 모른다. 컨테이너를 거치지 않고
new EmailSender()로 만들었기 때문이다. 프록시가 없으면
어노테이션은 그냥 메타데이터일 뿐, 아무것도 가로채지 못한다. 1편에서
다룬 self-invocation 함정과 같은 계열의 문제다 — 프록시를
통과하지 않은 호출은 AOP가 죽는다.
선호 패턴은 EmailSender를 빈으로 등록하고 주입받는
것이다.
@Service
public class OrderService {
private final EmailSender emailSender;
public OrderService(EmailSender emailSender) {
this.emailSender = emailSender; // 컨테이너가 넣어준 프록시
}
}
@Component
public class EmailSender {
@Transactional
public void send(Order order) { /* ... */ }
}2-3) 역할 컴포넌트 vs 데이터 객체
그렇다고 모든 객체를 빈으로 등록해야 하는 건 아니다. 역할을 가진 컴포넌트와 데이터를 담는 객체는 구분해야 한다.
- 빈으로 등록: 서비스, 리포지토리, 컨트롤러, 설정 클래스, 포트/어댑터 — 다른 의존성이 필요하거나 AOP가 걸려야 하는 행위 객체
- 빈 아님:
Entity,DTO,VO,Money,Address— 상태만 담는 값 객체. 요청마다 새로 만들어지고, 의존성 주입이 필요 없다
JPA Entity를 @Component로 등록한다는 발상은
빈과 값 객체를 혼동한 대표적인 실수다. Entity는 요청마다
수백 개씩 만들어졌다 사라지는 데이터 구조이고, 컨테이너가 관리할 대상이
아니다.
3) BeanDefinition: 빈의 설계도
3-1) 빈은 "객체가 아닌 명세"로 먼저 등록된다
스프링 컨테이너가 기동할 때 가장 먼저 하는 일은 객체를 만드는
것이 아니라 설계도를 수집하는 것이다. @Component가
붙은 클래스를 찾으면, 컨테이너는 그 자리에서 객체를 인스턴스화하지
않는다. 대신 "이 클래스를 이런 조건으로 만들어달라"는
명세를 BeanFactory에 등록한다. 이 명세가
바로 BeanDefinition이다.
공식 레퍼런스의 표현이 직관적이다.
"A bean definition is essentially a recipe for creating one or more objects. The container looks at the recipe for a named bean when asked and uses the configuration metadata encapsulated by that bean definition to create (or acquire) an actual object."
레시피다. 재료 목록이지 요리 자체가 아니다. 요리는 나중에 주문이 들어오면 시작한다.
3-2) BeanDefinition이 담는 것
BeanDefinition 인터페이스는 빈 하나에 대한 메타데이터를
전부 담는다. 주요 속성은 다음과 같다.
| 속성 | 의미 |
|---|---|
beanClassName |
만들어야 할 클래스의 FQCN |
scope |
singleton / prototype /
request / session 등 |
lazyInit |
기동 시 미리 만들지, 첫 요청 때 만들지 |
autowireMode |
자동 주입 전략 |
dependsOn |
이 빈보다 먼저 만들어져야 하는 빈 이름 |
constructorArgumentValues |
생성자 인자 |
propertyValues |
setter로 주입할 프로퍼티 |
initMethodName / destroyMethodName |
생명주기 콜백 메서드 이름 |
애플리케이션 코드에서 BeanDefinition을 직접 다룰 일은
거의 없다. 그러나 이 구조를 독해 모델로 갖고 있으면,
스프링이 왜 기동 시 어노테이션을 파싱하고 메타데이터를 먼저 모으는지
이해할 수 있다.
3-3) 왜 객체가 아니라 명세인가
만약 컨테이너가 기동 시에 객체를 바로 만들어서 등록했다면, 스프링의 여러 기능은 아예 불가능했을 것이다.
lazy-init— "첫 요청 때 만들어라"는 지시는 명세 단계에 있어야 가능하다. 객체가 이미 있으면 "늦게 만든다"가 성립하지 않는다prototype스코프 — 요청마다 새 인스턴스를 만들려면 매번 명세로 돌아가야 한다BeanFactoryPostProcessor— 빈이 만들어지기 전에BeanDefinition을 조작할 수 있는 확장점이다. 객체 상태라면 손댈 수가 없다- 순환 참조 해결 — 컨테이너가 "누가 누구를 필요로 하는지"를 명세 단계에서 파악해야 조립 순서를 정할 수 있다
**"명세 먼저, 객체는 나중"**이라는 두 단계 구조가 스프링 확장성의 핵심이다.
4) 등록과 생성은 다른 사건
4-1) 기동 타임라인 다섯 단계
@Component를 붙이면 기동 시점에 객체가 만들어진다고
묶어서 이해하기 쉽지만, 실제로는 등록과 생성이 분리된 두
사건이다. ApplicationContext.refresh() 내부에서
벌어지는 순서를 단순화하면 이렇다.
- 스캔 —
@ComponentScan으로 classpath를 훑어@Component계열 어노테이션이 붙은 클래스를 발견한다 - BeanDefinition 생성 — 발견한 클래스마다
BeanDefinition메타데이터를 만든다 - BeanFactory 등록 — 이 명세를
DefaultListableBeanFactory에 넣는다. 여기까지는 객체가 단 하나도 없다 - BeanFactoryPostProcessor 실행 — 등록된
BeanDefinition을 수정할 기회를 준다 (예:@Value의 placeholder 치환) - pre-instantiate singletons —
refresh()의 끝부분에서 싱글톤 빈들을 실제로 인스턴스화한다. 이때BeanPostProcessor가 끼어들어 프록시 래핑 등을 수행한다
4-2) 확장점이 끼어드는 자리
이 타임라인을 기억하면 확장점들의 역할이 자연스럽게 읽힌다.
BeanFactoryPostProcessor(BFPP)는 2~3 사이, 즉 명세를 다루는 단계에서 동작한다. 객체는 아직 없다BeanPostProcessor(BPP)는 5단계, 객체가 만들어진 직후에 동작한다. 초기화 전/후에 훅이 걸리고, AOP 프록시가 여기서 씌워진다
빈 생명주기의 전체 흐름은 3편에서 자세히 다룰 예정이고, 2편에서는
**"등록과 생성이 다른 단계"**라는 사실 하나만 붙잡으면 충분하다. 이걸
알고 있으면 @PostConstruct가 왜 생성자보다 나중에
호출되는지, @Transactional 프록시가 언제 씌워지는지가
머릿속에서 제자리를 찾는다.
5) 빈 등록 세 가지 길: @Component, @Bean, XML
스프링에서 빈을 컨테이너에 등록하는 방법은 본질적으로 세 가지다. 각 방법의 용도가 다르고, 혼용해서 쓴다.
5-1) @Component 스캔: 내 코드를 빈으로
내가 직접 작성한 클래스에 어노테이션 하나만 붙이면 된다.
@ComponentScan이 classpath를 훑어 자동으로 등록해준다.
@Service
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}스테레오타입 어노테이션은 네 가지가 있다.
| 어노테이션 | 의미 | 차이 |
|---|---|---|
@Component |
일반 컴포넌트 | 기본형 |
@Service |
비즈니스 로직 | 의미 구분용 (동작 동일) |
@Repository |
데이터 접근 | 예외 변환(PersistenceExceptionTranslationPostProcessor)이 추가로 붙음 |
@Controller |
웹 요청 처리 | Spring MVC가 HandlerMapping으로 찾음 |
@Repository만 추가 동작이 있다는 점은 자주 잊힌다.
JDBC/JPA의 네이티브 예외(SQLException 등)를 스프링의
DataAccessException 계층으로 번역해주는 장치다. 공식
레퍼런스의 표현을 빌리면 이렇다.
"Spring can automatically detect stereotyped classes and register corresponding BeanDefinition instances with the ApplicationContext."
스캔은 BeanDefinition을 자동으로 만드는 편의 장치이지, 빈 등록 메커니즘 자체가 다른 것은 아니다.
5-2) @Bean: 남의 코드를 빈으로
3rd-party 라이브러리의 클래스를 빈으로 만들고 싶을 때는
@Component를 붙일 수 없다. 소스가 내 것이 아니기 때문이다.
이때 쓰는 게 @Bean이다.
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost/app");
return new HikariDataSource(config);
}
}공식 문서의 표현이다.
"@Bean is a method-level annotation and a direct analog of the XML
element. It supports some of the attributes offered by , such as init-method, destroy-method, autowiring, name and alias."
메서드가 팩토리가 되어 객체를 만들고, 그 반환 값이 빈으로 등록된다. 스프링은 이 객체를 자기가 만든 것처럼 다룬다. 생명주기 콜백도 붙이고, AOP 프록시도 씌운다.
여기서 틀리기 쉽다. @Bean 메서드의
이름이 곧 bean name이다. 메서드를 리네이밍하면 빈
이름이 바뀌고, 다른 곳에서 @Qualifier("oldName")으로
참조하던 코드가 조용히 부서진다.
// 피하기: 나중에 리네이밍하면 참조하던 곳이 깨진다
@Bean
public DataSource dataSource() { /* ... */ }
// 선호: 이름을 고정해두면 메서드 리네이밍이 안전하다
@Bean(name = "primaryDataSource")
public DataSource dataSource() { /* ... */ }리팩토링이 잦은 프로젝트라면 @Bean(name = "...")으로
이름을 고정해두는 편이 안전하다.
5-3) XML: 역사적 배경
Spring 1.x~2.x 시대에는 XML이 유일한 빈 등록 방법이었다. 지금도
<bean>, <context:component-scan>
같은 태그가 동작하긴 하지만, 신규 코드에서 쓰는 경우는 거의 없다. 레거시
코드를 읽을 때 "아, 이건 기존 자바 설정의 XML 버전이구나" 정도만 알면
된다. @Configuration이 등장한 뒤로 XML의 유일한 장점(코드
밖 설정)은 거의 사라졌다.
5-4) 어느 길을 고를 것인가
| 상황 | 방식 | 이유 |
|---|---|---|
| 내 코드, 단일 인스턴스 | @Component 스캔 |
자동 등록, 가장 간결 |
| 3rd-party 클래스 | @Bean in @Configuration |
소스 수정 불가 |
| 같은 클래스로 여러 빈 | @Bean 메서드 여러 개 |
스캔으로는 불가능 |
| 런타임 조건부 등록 | @Conditional /
BeanDefinitionRegistryPostProcessor |
동적 결정 |
| 레거시 유지보수 | XML | 기존 구조 유지 |
경험적으로 내 코드는 @Component, 남의 코드는
@Bean 이 기본이다. 나머지는 필요할 때 추가하면
된다.
6) @Configuration의 비밀: CGLIB와 lite mode
6-1) @Configuration vs @Component: 같아 보이지만 다르다
@Configuration은 내부적으로 @Component를
품고 있어서, 둘 다 컴포넌트 스캔에 걸리고 둘 다 @Bean
메서드를 담을 수 있다. 그래서 초보자는 "그냥 @Component에
@Bean을 넣어도 되는 거 아닌가?"라고 생각한다. 실제로
동작하긴 한다. 그러나 동작 방식이 전혀 다르다. 이
차이를 모르면 싱글톤이 조용히 깨진다.
6-2) CGLIB 프록시로 @Bean 호출 가로채기
@Configuration이 붙은 클래스는 기동 시점에 CGLIB로
서브클래스가 만들어져서 등록된다. 공식 레퍼런스의
문장이다.
"All @Configuration classes are subclassed at startup-time with CGLIB. In the subclass, the child method checks the container first for any cached (scoped) beans before it calls the parent method and creates a new instance."
즉 @Configuration 클래스 안에서 @Bean
메서드를 호출하면, CGLIB 서브클래스가 그 호출을 가로채서
컨테이너에 이미 있는 빈을 돌려준다. 직접
new를 호출하지 않는다.
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public JdbcTemplate jdbcTemplate() {
// 이 dataSource() 호출은 CGLIB가 가로챈다
// 컨테이너에 등록된 싱글톤 DataSource를 돌려준다
return new JdbcTemplate(dataSource());
}
}jdbcTemplate() 안에서 dataSource()를 그냥
호출했는데도 매번 같은 DataSource 인스턴스가 돌아온다.
이것이 Full mode다. 그래서 @Configuration
클래스는 CGLIB가 서브클래싱할 수 있어야 하고,
final이 붙을 수 없다. Kotlin에서 클래스가
기본 final이라 open을 붙이거나
kotlin-spring 플러그인을 쓰는 이유도 여기에 있다.
6-3) Lite mode: @Component 안의 @Bean은 싱글톤이 아니다
@Component(혹은 평범한 POJO) 안에 @Bean
메서드를 넣으면 CGLIB 서브클래싱이 일어나지 않는다. 이 상태를
Lite mode라 부른다. @Bean 메서드끼리
호출하면 진짜로 자바 메서드 호출이 되고, 매번 새
인스턴스가 만들어진다.
// 피하기: lite mode 함정
@Component // NOT @Configuration
public class LiteConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public JdbcTemplate jdbcTemplate() {
// dataSource()는 프록시가 아니므로 매번 new HikariDataSource()
// 컨테이너에 등록된 빈과 다른 인스턴스가 JdbcTemplate에 주입된다
return new JdbcTemplate(dataSource());
}
}결과가 어떻게 되느냐 — 컨테이너에는 DataSource 싱글톤이
하나 등록되고, 그와 **별개로 JdbcTemplate이 품고 있는 또
하나의 DataSource**가 존재한다. 커넥션 풀이 두 개 생긴다.
겉으로는 멀쩡히 돌아가지만, 리소스가 두 배로 새는 상태다.
6-4) Lite mode가 나쁜 것만은 아니다
재미있는 건 Spring Boot의 auto-configuration이 일부러 lite
mode를 쓴다는 점이다.
@Configuration(proxyBeanMethods = false)가 기본이다. 이유는
성능이다. CGLIB 서브클래싱은 기동 시간과 메모리를 먹기 때문에,
inter-bean 호출이 없는 설정 클래스라면 프록시를 걸지
않는 게 더 효율적이다.
@Configuration(proxyBeanMethods = false)
public class MyAutoConfig {
@Bean
public Foo foo() {
return new Foo(); // 다른 @Bean 메서드를 호출하지 않음
}
@Bean
public Bar bar(Foo foo) {
// 함정 피하기: 필요한 의존성은 파라미터로 받는다
return new Bar(foo);
}
}lite mode의 안전 규칙: @Bean 메서드
안에서 다른 @Bean 메서드를 직접 호출하지 말
것. 필요한 빈은 메서드 파라미터로 받아라.
그러면 컨테이너가 알아서 주입해주고, 싱글톤이 깨지지 않는다. 이 규칙만
지키면 proxyBeanMethods = false로 성능 이득을 챙기면서도
함정을 피할 수 있다.
7) 이름과 타입으로 빈 찾기: @Autowired의 내부 규칙
7-1) getBean(name) vs getBean(Class)
ApplicationContext는 빈을 두 가지 방식으로 꺼낼 수 있다.
이름으로(getBean("orderService")) 혹은
타입으로(getBean(OrderService.class)).
그러나 실무에서 직접 getBean을 부를 일은 거의 없다.
@Autowired가 대신 해준다.
@Autowired의 동작 원칙은 간단하다 — 타입 우선,
필요할 때 이름으로 좁힌다. 한 레퍼런스의 표현을 빌리면
이렇다.
"@Autowired is fundamentally about type-driven injection with optional semantic qualifiers."
타입이 유일하면 그대로 주입한다. 같은 타입이 여럿이면 이름이나
@Qualifier로 좁혀야 한다.
여기서 틀리기 쉬운 지점이 있다. Spring 6.1+에서는
생성자 파라미터 이름 매칭이 컴파일러의
-parameters 플래그에 의존한다. 플래그가 없으면 클래스
파일에 파라미터 이름이 안 들어가고, 스프링이 arg0,
arg1 같은 합성 이름만 보게 된다. 결과적으로 이름 기반
폴백이 실패한다.
// build.gradle
tasks.withType(JavaCompile) {
options.compilerArgs << "-parameters"
}Spring Boot 3.2+는 이 플래그를 기본으로 넣어준다. 하지만 Spring Framework 단독이거나 오래된 Boot 버전을 쓰면 직접 넣어야 한다. IDE의 Run에서는 돌다가 CI 빌드에서만 깨지는 전형적인 버그다.
7-2) @Qualifier와 @Primary: 누가 이기는가
같은 타입의 빈이 여러 개면 주입 지점에서 하나를 골라야 한다. 스프링은 두 가지 장치를 제공한다.
@Primary— 빈 정의 쪽에 붙인다. "같은 타입 후보 중 기본으로 골라달라"는 뜻이다@Qualifier("name")— 주입 사용 쪽에 붙인다. "이 이름의 빈을 달라"는 뜻이다
공식 문서의 표현이다.
"When more than one bean qualifies to be autowired, use @Qualifier to indicate the specific bean that should be autowired."
"@Primary indicates that a bean should be given preference when multiple candidates are qualified to autowire a single-valued dependency."
그럼 둘이 동시에 있으면? 결론은
@Qualifier가 이긴다. @Primary는 "후보 중
기본"이고, @Qualifier는 "후보를 이름으로
좁히는(narrowing) 의미"이기 때문에 호출 지점의 명시적
지시가 우선한다.
public interface PaymentGateway { /* ... */ }
@Component
@Primary
public class KakaoPayGateway implements PaymentGateway { /* ... */ }
@Component("toss")
public class TossPayGateway implements PaymentGateway { /* ... */ }
@Service
public class OrderService {
private final PaymentGateway gateway;
public OrderService(@Qualifier("toss") PaymentGateway gateway) {
// @Primary가 Kakao에 붙어있지만, @Qualifier가 이긴다 → Toss
this.gateway = gateway;
}
}7-3) 같은 타입 빈이 여럿일 때의 우선순위 스택
스프링이 후보를 좁혀나가는 순서를 정리하면 다음과 같다.
| 우선순위 | 방식 | 의미 |
|---|---|---|
| 1 | @Qualifier |
호출 지점이 이름을 명시 — 가장 강함 |
| 2 | 파라미터/필드 이름 매칭 | 타입 매칭 후보 중 변수 이름과 같은 빈 |
| 3 | @Primary |
후보 중 기본으로 지정된 빈 |
| 4 | @Fallback (6.2+) |
다른 후보가 전부 없을 때만 선택 |
외울 필요는 없다. 주입이 실패하면 에러 메시지에
후보가 나열되고, 그때 위에서부터 하나씩 추가하면 된다.
@Qualifier로 시작해서 안 되면 이름을 맞추고, 그래도 설계상
애매하면 @Primary를 붙이는 식이다. @Fallback은
Spring 6.2에서 추가된 최신 기능으로, "다른 후보가 하나도 없으면 이것을
써라"는 가장 약한 우선순위를 의미한다. 테스트용
스텁이나 기본 구현을 등록할 때 유용하다.
8) 실무에서 이렇게 읽고 쓴다
- 내 코드는
@Component, 남의 코드는@Bean, 나머진 필요할 때만. 99% 경우 이 두 가지로 충분하다 @Transactional이 안 먹힌다 = 빈이 아니거나 self-invocation. 먼저 "이 객체가 컨테이너가 만든 것이 맞는가"부터 확인하라- 같은 타입 빈 충돌 =
@Qualifier먼저.@Primary는 기본값 의미가 있어야 쓴다. 디버깅이 꼬이면@Qualifier가 읽기 쉽다 -parameters플래그 확인. Spring Boot 3.2+면 자동이지만, 구버전이나 Framework 단독이면build.gradle에 직접 넣어야 한다- Kotlin +
@Configuration=open또는kotlin-spring플러그인. CGLIB 서브클래싱 때문에final클래스는 프록시가 안 된다 - Spring Boot 설정 클래스에
proxyBeanMethods = false를 봐도 놀라지 말 것. inter-bean 호출이 없으면 안전하고, 성능 이득이 있다. 단,@Bean메서드가 다른@Bean메서드를 직접 호출하면 싱글톤이 깨진다는 사실만 기억하면 된다 @Bean(name = "...")이름 고정. 메서드 리네이밍이@Qualifier("...")참조를 조용히 부수는 걸 막는 간단한 방어- JPA Entity / DTO는 빈이 아니다.
@Component를 붙이는 실수는 빈과 값 객체를 혼동한 신호다
9) 한 줄 정리
스프링 빈은 "컨테이너가 아는 객체"다. 등록은 객체가
아닌 명세(BeanDefinition)로 이뤄지고, 생성은 그보다
나중에 일어난다. @Component는 내 코드,
@Bean은 남의 코드, @Configuration은
@Bean들이 서로를 참조할 때 싱글톤을 지키는
장치다. 같은 타입 빈이 여럿이면
@Qualifier → 이름 매칭 → @Primary → @Fallback 순으로
좁혀 들어간다.
태그: Spring Framework, Spring Bean, BeanDefinition, @Component, @Bean, @Configuration, @Qualifier, @Primary, CGLIB, ApplicationContext
'CS > Spring' 카테고리의 다른 글
| 빈 초기화 순서: @DependsOn·@Lazy·@Order로 기동을 통제하기 (0) | 2026.04.12 |
|---|---|
| 조건부 빈 등록: @Conditional과 Auto-configuration의 뿌리 (1) | 2026.04.12 |
| 빈 스코프: singleton이 전부가 아니다 (0) | 2026.04.12 |
| 빈 라이프사이클: 태어나서 사라지기까지 (1) | 2026.04.12 |
| 스프링 컨테이너와 IoC/DI/AOP — 빈이 태어나는 자리 (1) | 2026.04.11 |