F-LAB JAVA · 5주차 · Phase 8 · Spring 컨테이너
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
getBean() 은 컨테이너의 빈 목록에서 해당 빈을 찾고, 없으면 @Bean 메서드를 호출해 생성하면서 의존 빈도 자동으로 생성·주입한 뒤 반환하며, 두 번째 호출부터는 이미 만든 같은 빈을 돌려준다 (싱글톤).
getBean("shipmentDao") 을 호출하면 컨테이너는 먼저 빈 목록 (캐시) 에서 해당 이름/타입의 빈을 찾는다.
없으면 (또는 최초 생성 시) 해당 @Bean 메서드를 호출해 객체를 생성 하고, 그 과정에서 필요한 의존 빈도 재귀적으로 생성·주입 한다.
처음 호출 에서는 빈을 생성해 목록에 저장하고, 두 번째 호출 부터는 저장된 같은 인스턴스를 반환하므로 동일한 객체다 (싱글톤, Unit 8.3).
반면 설정 클래스 안에서 @Bean 메서드를 직접 호출 하면 매번 새 객체가 될 것 같지만, @Configuration 클래스는 CGLIB 프록시로 감싸져 같은 빈을 반환하도록 보장된다.
getBean = 도서관 책 대출:
getBean("자바책"):
1. 서가(빈 목록) 에서 찾기
2. 있으면 → 그 책 대출
3. 없으면 (첫 요청):
- 출판사에 주문 (@Bean 호출)
- 부록도 함께 (의존 빈 주입)
- 서가에 등록
- 대출
처음 vs 두 번째:
- 처음: 주문·등록·대출
- 두 번째: 등록된 책 대출 (같은 책)
→ 싱글톤 (같은 책 1권)
직접 호출 vs getBean:
- @Bean 직접: "내가 만들게" 같지만
- 프록시가 "서가 책 줄게" (같은 책)
의존 빈:
- 책(빈) 에 부록(의존)
- 함께 준비 (자동 주입)
→ getBean = 빈 목록 찾기 → 없으면 생성·주입 → 반환, 두 번째는 같은 빈 (싱글톤).
1. getBean 동작 흐름
2. 빈 목록 찾기
3. 없으면 생성
4. 의존 빈 주입
5. 처음 vs 두 번째 호출
6. @Bean 직접 호출 vs getBean
7. @Configuration 프록시
8. 빈 조회 방식
9. 면접 + 자기 점검
getBean 흐름:
1. 빈 목록(캐시) 에서 찾기
2. 있으면 → 반환
3. 없으면:
a. @Bean 메서드 호출
b. 의존 빈 생성·주입
c. 목록에 저장
d. 반환
getBean("shipmentDao"):
목록에 있나?
├─ YES → 반환 (캐시)
└─ NO ↓
@Bean 호출 (생성)
↓
의존 빈 주입
↓
목록 저장
↓
반환
// getBean 호출
ApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 컨테이너가 위 흐름 수행
// getBean 동작 (ILIC)
@Configuration
public class IlicConfig {
@Bean
public ShipmentDao shipmentDao() {
return new ShipmentDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker() {
return new CustomerAConnectionMaker();
}
}
// getBean 호출
public class App {
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(IlicConfig.class);
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 1. 목록에서 shipmentDao 찾기
// 2. (초기화 시 생성됐으면) 반환
// 3. (없으면) shipmentDao() 호출
// → connectionMaker() 호출 (의존)
// → 주입 → 저장 → 반환
}
}
class ShipmentDao { ShipmentDao(ConnectionMaker cm) {} }
interface ConnectionMaker { Connection makeConnection(); }
class CustomerAConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
getBean() 의 동작 흐름은?
답:
1. 흐름:
찾기:
생성:
반환:
빈 목록 (캐시):
컨테이너 내부:
- Map<String, Object>
- 생성된 빈 보관
→ 먼저 여기 찾기
찾기 우선:
getBean:
1. 목록에 있나?
2. 있으면 즉시 반환
3. 없으면 생성
→ 재사용 (싱글톤)
// 이름으로 찾기
Object bean = ctx.getBean("shipmentDao");
// 타입으로 찾기
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 둘 다
ShipmentDao dao2 = ctx.getBean("shipmentDao", ShipmentDao.class);
// 빈 없으면 예외
try {
ctx.getBean("nonExistent");
} catch (NoSuchBeanDefinitionException e) {
// 빈 정의 없음
}
// 정의 자체가 없으면 예외
// 빈 목록 찾기
public class BeanLookup {
public void lookup(ApplicationContext ctx) {
// 컨테이너 내부 Map:
// "shipmentDao" → ShipmentDao 인스턴스
// "connectionMaker" → ConnectionMaker 인스턴스
// "bookingDao" → BookingDao 인스턴스
// 타입으로 (권장)
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 이름으로
Object byName = ctx.getBean("shipmentDao");
// 같은 빈 (목록에서)
}
}
class ShipmentDao { }
빈 목록에서 찾기는?
답:
1. 빈 목록:
찾기 우선:
이름/타입:
없으면:
없으면 생성:
목록에 없으면:
- @Bean 메서드 호출
- 객체 생성
shipmentDao() 실행
생성 시점:
ApplicationContext:
- 초기화 시 미리 생성 (eager)
- getBean 시 이미 있음
BeanFactory:
- getBean 시 생성 (lazy)
// 최초 생성 (개념)
@Bean
public ShipmentDao shipmentDao() {
// 최초 getBean 또는 초기화 시 호출
System.out.println("ShipmentDao 생성");
return new ShipmentDao(connectionMaker());
}
// 한 번만 호출 (싱글톤)
생성 후 저장:
생성된 빈:
- 목록(Map) 에 저장
- "shipmentDao" → 인스턴스
→ 다음엔 재사용
@Configuration
public class IlicConfig {
@Bean
public ShipmentDao shipmentDao() {
log.info("shipmentDao 빈 생성"); // 한 번만
return new ShipmentDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker() {
log.info("connectionMaker 빈 생성"); // 한 번만
return new CustomerAConnectionMaker();
}
}
// ApplicationContext 초기화:
// - shipmentDao() 호출 → "shipmentDao 빈 생성" 출력
// - connectionMaker() 호출 → "connectionMaker 빈 생성" 출력
// - 목록에 저장
// getBean 여러 번 호출해도 추가 생성 X (저장된 것)
class ShipmentDao { ShipmentDao(ConnectionMaker cm) {} }
interface ConnectionMaker { Connection makeConnection(); }
class CustomerAConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
없으면 생성 과정은?
답:
1. 생성:
시점:
최초:
저장:
의존 빈 자동:
빈 A 가 B 필요:
- B 빈 먼저 생성
- A 에 주입
→ 재귀적 해결
주입 흐름:
shipmentDao() 호출:
- connectionMaker() 필요
- connectionMaker 빈 생성/조회
- ShipmentDao 에 주입
- 완성된 ShipmentDao 반환
의존성 순서:
의존 먼저:
1. connectionMaker 생성
2. shipmentDao 생성 (cm 주입)
3. shipmentService 생성 (dao 주입)
→ 의존 그래프 순서
// 의존 빈도 싱글톤 (재사용)
@Bean
public ShipmentDao shipmentDao() {
return new ShipmentDao(connectionMaker()); // 같은 cm
}
@Bean
public BookingDao bookingDao() {
return new BookingDao(connectionMaker()); // 같은 cm (재사용)
}
// connectionMaker() 는 한 번만 생성 (싱글톤)
// shipmentDao, bookingDao 가 같은 cm 공유
@Configuration
public class IlicConfig {
@Bean
public ConnectionMaker connectionMaker() {
return new CustomerAConnectionMaker(); // 1번 생성 (싱글톤)
}
@Bean
public ShipmentDao shipmentDao() {
return new ShipmentDao(connectionMaker()); // cm 주입
}
@Bean
public BookingDao bookingDao() {
return new BookingDao(connectionMaker()); // 같은 cm 주입
}
@Bean
public ShipmentService shipmentService() {
return new ShipmentService(shipmentDao(), bookingDao());
// shipmentDao, bookingDao 주입
}
}
// getBean(ShipmentService.class):
// 1. connectionMaker 생성 (싱글톤)
// 2. shipmentDao 생성 (cm 주입)
// 3. bookingDao 생성 (같은 cm 주입)
// 4. shipmentService 생성 (dao, bookingDao 주입)
// → 의존성 그래프 자동 해결
class ShipmentDao { ShipmentDao(ConnectionMaker cm) {} }
class BookingDao { BookingDao(ConnectionMaker cm) {} }
class ShipmentService { ShipmentService(ShipmentDao a, BookingDao b) {} }
interface ConnectionMaker { Connection makeConnection(); }
class CustomerAConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
의존 빈 자동 생성·주입은?
답:
1. 자동:
흐름:
순서:
싱글톤:
처음 호출:
getBean (최초):
- 목록에 없음 (또는 초기화 시 생성)
- @Bean 호출
- 생성·주입·저장
- 반환
두 번째 호출:
getBean (재호출):
- 목록에 있음
- 저장된 빈 반환
- 추가 생성 X
→ 같은 인스턴스
// 같은 객체 (싱글톤)
ShipmentDao dao1 = ctx.getBean(ShipmentDao.class);
ShipmentDao dao2 = ctx.getBean(ShipmentDao.class);
System.out.println(dao1 == dao2); // true (같은 인스턴스)
// 두 번째도 같은 빈
싱글톤 예고:
여러 번 getBean:
- 항상 같은 빈
- 1개 인스턴스
→ 싱글톤 (Unit 8.3 상세)
public class FirstVsSecond {
public void demonstrate(ApplicationContext ctx) {
// 처음 호출
ShipmentDao dao1 = ctx.getBean(ShipmentDao.class);
// (초기화 시 이미 생성됐으면 저장된 것 반환)
// 두 번째 호출
ShipmentDao dao2 = ctx.getBean(ShipmentDao.class);
// 같은 인스턴스
assert dao1 == dao2; // true
// 100번 호출해도 같은 빈
for (int i = 0; i < 100; i++) {
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
assert dao == dao1; // 항상 같음
}
// → 싱글톤 (Unit 8.3)
}
}
class ShipmentDao { }
처음 호출 vs 두 번째 호출 차이는?
답:
1. 처음:
두 번째:
같은 객체:
싱글톤:
두 방식:
@Bean 직접 호출:
connectionMaker()
- 메서드 직접
getBean:
ctx.getBean(...)
- 컨테이너 통해
직접 호출 의문:
@Bean
shipmentDao() {
return new ShipmentDao(connectionMaker());
// ↑ 직접 호출?
}
connectionMaker() 직접 호출:
- 매번 새 객체?
- 싱글톤 깨짐?
프록시가 보장:
@Configuration 클래스:
- CGLIB 프록시로 감쌈
- @Bean 메서드 가로챔
- 목록에서 반환 (같은 빈)
→ 직접 호출해도 싱글톤
@Configuration
public class AppConfig {
@Bean
public ConnectionMaker connectionMaker() {
return new NConnectionMaker(); // 한 번만
}
@Bean
public ShipmentDao shipmentDao() {
return new ShipmentDao(connectionMaker()); // 직접 호출
// 프록시가 가로채 같은 빈 반환 (싱글톤 유지)
}
@Bean
public BookingDao bookingDao() {
return new BookingDao(connectionMaker()); // 또 직접 호출
// 같은 connectionMaker 빈 (프록시)
}
}
// connectionMaker() 직접 호출해도 같은 빈 (프록시 덕분)
@Configuration // 프록시로 감싸짐
public class IlicConfig {
@Bean
public ConnectionMaker connectionMaker() {
log.info("connectionMaker 생성"); // 1번만 출력
return new CustomerAConnectionMaker();
}
@Bean
public ShipmentDao shipmentDao() {
return new ShipmentDao(connectionMaker()); // 직접 호출
}
@Bean
public BookingDao bookingDao() {
return new BookingDao(connectionMaker()); // 직접 호출
}
@Bean
public InvoiceDao invoiceDao() {
return new InvoiceDao(connectionMaker()); // 직접 호출
}
}
// connectionMaker() 를 3번 직접 호출해도
// "connectionMaker 생성" 은 1번만 (프록시가 같은 빈 반환)
// → 싱글톤 보장
class ShipmentDao { ShipmentDao(ConnectionMaker cm) {} }
class BookingDao { BookingDao(ConnectionMaker cm) {} }
class InvoiceDao { InvoiceDao(ConnectionMaker cm) {} }
interface ConnectionMaker { Connection makeConnection(); }
class CustomerAConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
@Bean 메서드 직접 호출 vs getBean 차이는?
답:
1. 두 방식:
직접 호출 의문:
프록시:
결과:
@Configuration 프록시:
@Configuration 클래스:
- CGLIB 프록시 생성
- @Bean 메서드 오버라이드
→ 빈 관리 (싱글톤)
CGLIB:
바이트코드 조작:
- @Configuration 상속
- @Bean 메서드 가로챔
→ 런타임 프록시
메서드 가로채기:
@Bean 메서드 호출 시:
- 프록시가 가로챔
- 목록 확인
- 있으면 반환 (새 생성 X)
→ 싱글톤 보장
@Configuration 없으면 (lite mode):
@Bean 만 (설정 X):
- 프록시 X
- 직접 호출 = 새 객체
→ 싱글톤 안 됨 (주의)
// @Configuration (프록시, 싱글톤 보장)
@Configuration
public class IlicConfigProxy {
@Bean
public ConnectionMaker connectionMaker() {
return new CustomerAConnectionMaker();
}
@Bean
public ShipmentDao shipmentDao() {
return new ShipmentDao(connectionMaker()); // 같은 빈
}
}
// @Configuration 없이 (lite mode, 주의)
// public class IlicConfigLite {
// @Bean
// public ConnectionMaker connectionMaker() {
// return new CustomerAConnectionMaker();
// }
// @Bean
// public ShipmentDao shipmentDao() {
// return new ShipmentDao(connectionMaker()); // 새 객체! (프록시 X)
// }
// }
// → @Configuration 권장 (싱글톤 보장)
class ShipmentDao { ShipmentDao(ConnectionMaker cm) {} }
interface ConnectionMaker { Connection makeConnection(); }
class CustomerAConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
@Configuration 프록시는?
답:
1. 프록시:
CGLIB:
가로채기:
없으면:
빈 조회 방식:
- 이름으로 (getBean("name"))
- 타입으로 (getBean(Type.class))
- 이름 + 타입
- 여러 빈 (getBeansOfType)
// 타입 조회 (권장)
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 타입 안전, 캐스팅 X
// 같은 타입 여러 빈
Map<String, ConnectionMaker> makers =
ctx.getBeansOfType(ConnectionMaker.class);
// 모든 ConnectionMaker 빈
// 특정 선택 (@Qualifier, @Primary)
실무는 DI:
getBean 직접 호출:
- 드묾 (테스트 등)
실무:
- @Autowired / 생성자 주입
- 컨테이너가 자동 주입
- getBean 안 보임
// 빈 조회 (테스트/특수 상황)
public class BeanRetrieval {
public void retrieve(ApplicationContext ctx) {
// 타입 (권장)
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 같은 타입 여럿
Map<String, ConnectionMaker> makers =
ctx.getBeansOfType(ConnectionMaker.class);
// customerA, customerB ConnectionMaker 등
}
}
// 실무 (DI, getBean 안 씀)
@Service
public class ShipmentService {
private final ShipmentDao shipmentDao;
// 생성자 주입 (컨테이너 자동, getBean X)
public ShipmentService(ShipmentDao shipmentDao) {
this.shipmentDao = shipmentDao;
}
}
class ShipmentDao { }
interface ConnectionMaker { Connection makeConnection(); }
빈 조회 방식은?
답:
1. 방식:
타입 권장:
여럿:
실무:
| Q | 핵심 답변 |
|---|---|
| getBean 흐름? | 찾기→없으면생성→주입→반환 |
| 빈 목록? | Map (캐시) |
| 없으면? | @Bean 호출 |
| 의존 빈? | 자동 생성·주입 |
| 처음 vs 두번째? | 생성 vs 재사용 (싱글톤) |
| @Bean 직접 호출? | 프록시가 같은 빈 |
| @Configuration 프록시? | CGLIB 가로챔 |
| 조회 방식? | 이름/타입 |
| 실무? | DI (getBean X) |
| lite mode? | 프록시 X (새 객체) |
답:
답:
답:
답:
답:
1. getBean 흐름
2. @Bean 직접 호출
3. 실무
이번 Unit에서 getBean 동작을 봤다면, 다음은 싱글톤 레지스트리 (★ 깊이 파기).
🌱 Phase 8 — Spring 컨테이너
✅ Unit 8.1 빈 팩토리와 ApplicationContext
✅ Unit 8.2 getBean()의 동작 ← 여기
⏭ Unit 8.3 싱글톤 레지스트리 ★깊이
⏭ Unit 8.4 의존관계 주입 DI ★깊이 (+ 졸업 시험)
✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
✅ Phase 3~7 (15 Unit)
🌱 Phase 8 — Spring 컨테이너 (2/4 진행)
총: 24/26 Unit