5주차 Unit 8.2 — getBean()의 동작

Psj·2026년 5월 27일

F-lab

목록 보기
179/230

Unit 8.2 — getBean()의 동작

F-LAB JAVA · 5주차 · Phase 8 · Spring 컨테이너


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • getBean() 의 동작 흐름 은?
  • 빈 목록에서 찾기 → 없으면 생성 과정은?
  • 의존 빈 자동 생성·주입 은?
  • 처음 호출 vs 두 번째 호출 차이는?
  • @Bean 메서드 직접 호출 vs getBean 차이는?
  • 빈 조회 방식 (이름/타입) 은?
  • @Configuration 프록시 는?
  • getBean 의 싱글톤 동작 은?
  • 컨테이너 초기화와 getBean 관계는?

🎯 핵심 한 문장

getBean() 은 컨테이너의 빈 목록에서 해당 빈을 찾고, 없으면 @Bean 메서드를 호출해 생성하면서 의존 빈도 자동으로 생성·주입한 뒤 반환하며, 두 번째 호출부터는 이미 만든 같은 빈을 돌려준다 (싱글톤).
getBean("shipmentDao") 을 호출하면 컨테이너는 먼저 빈 목록 (캐시) 에서 해당 이름/타입의 빈을 찾는다.
없으면 (또는 최초 생성 시) 해당 @Bean 메서드를 호출해 객체를 생성 하고, 그 과정에서 필요한 의존 빈도 재귀적으로 생성·주입 한다.
처음 호출 에서는 빈을 생성해 목록에 저장하고, 두 번째 호출 부터는 저장된 같은 인스턴스를 반환하므로 동일한 객체다 (싱글톤, Unit 8.3).
반면 설정 클래스 안에서 @Bean 메서드를 직접 호출 하면 매번 새 객체가 될 것 같지만, @Configuration 클래스는 CGLIB 프록시로 감싸져 같은 빈을 반환하도록 보장된다.

비유 — 도서관 책 대출

getBean = 도서관 책 대출:

getBean("자바책"):
  1. 서가(빈 목록) 에서 찾기
  2. 있으면 → 그 책 대출
  3. 없으면 (첫 요청):
     - 출판사에 주문 (@Bean 호출)
     - 부록도 함께 (의존 빈 주입)
     - 서가에 등록
     - 대출

처음 vs 두 번째:
  - 처음: 주문·등록·대출
  - 두 번째: 등록된 책 대출 (같은 책)
  → 싱글톤 (같은 책 1권)

직접 호출 vs getBean:
  - @Bean 직접: "내가 만들게" 같지만
  - 프록시가 "서가 책 줄게" (같은 책)

의존 빈:
  - 책(빈) 에 부록(의존)
  - 함께 준비 (자동 주입)

→ getBean = 빈 목록 찾기 → 없으면 생성·주입 → 반환, 두 번째는 같은 빈 (싱글톤).


🧭 9개 섹션 로드맵

1. getBean 동작 흐름
2. 빈 목록 찾기
3. 없으면 생성
4. 의존 빈 주입
5. 처음 vs 두 번째 호출
6. @Bean 직접 호출 vs getBean
7. @Configuration 프록시
8. 빈 조회 방식
9. 면접 + 자기 점검

1️⃣ getBean 동작 흐름

1.1 전체 흐름

getBean 흐름:

1. 빈 목록(캐시) 에서 찾기
2. 있으면 → 반환
3. 없으면:
   a. @Bean 메서드 호출
   b. 의존 빈 생성·주입
   c. 목록에 저장
   d. 반환

1.2 흐름 시각화

getBean("shipmentDao"):

  목록에 있나?
    ├─ YES → 반환 (캐시)
    └─ NO ↓
       @Bean 호출 (생성)
          ↓
       의존 빈 주입
          ↓
       목록 저장
          ↓
       반환

1.3 호출

// getBean 호출
ApplicationContext ctx = 
    new AnnotationConfigApplicationContext(AppConfig.class);

ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 컨테이너가 위 흐름 수행

1.4 ILIC 의 맥락

// 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; }
}

1.5 자기 점검 답변

getBean() 의 동작 흐름은?

:
1. 흐름:

  • 찾기 → 없으면 생성 → 주입 → 반환
  1. 찾기:

    • 빈 목록(캐시)
  2. 생성:

    • @Bean 호출
  3. 반환:

    • 빈 제공

2️⃣ 빈 목록 찾기

2.1 빈 목록

빈 목록 (캐시):

  컨테이너 내부:
    - Map<String, Object>
    - 생성된 빈 보관

  → 먼저 여기 찾기

2.2 찾기 우선

찾기 우선:

  getBean:
    1. 목록에 있나?
    2. 있으면 즉시 반환
    3. 없으면 생성

  → 재사용 (싱글톤)

2.3 이름/타입

// 이름으로 찾기
Object bean = ctx.getBean("shipmentDao");

// 타입으로 찾기
ShipmentDao dao = ctx.getBean(ShipmentDao.class);

// 둘 다
ShipmentDao dao2 = ctx.getBean("shipmentDao", ShipmentDao.class);

2.4 없으면 예외

// 빈 없으면 예외
try {
    ctx.getBean("nonExistent");
} catch (NoSuchBeanDefinitionException e) {
    // 빈 정의 없음
}
// 정의 자체가 없으면 예외

2.5 ILIC 의 맥락

// 빈 목록 찾기
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 { }

2.6 자기 점검 답변

빈 목록에서 찾기는?

:
1. 빈 목록:

  • Map (캐시)
  1. 찾기 우선:

    • 목록 먼저
  2. 이름/타입:

    • 둘 다 가능
  3. 없으면:

    • 예외

3️⃣ 없으면 생성

3.1 @Bean 메서드 호출

없으면 생성:

  목록에 없으면:
    - @Bean 메서드 호출
    - 객체 생성

  shipmentDao() 실행

3.2 생성 시점

생성 시점:

ApplicationContext:
  - 초기화 시 미리 생성 (eager)
  - getBean 시 이미 있음

BeanFactory:
  - getBean 시 생성 (lazy)

3.3 최초 생성

// 최초 생성 (개념)
@Bean
public ShipmentDao shipmentDao() {
    // 최초 getBean 또는 초기화 시 호출
    System.out.println("ShipmentDao 생성");
    return new ShipmentDao(connectionMaker());
}
// 한 번만 호출 (싱글톤)

3.4 생성 후 저장

생성 후 저장:

  생성된 빈:
    - 목록(Map) 에 저장
    - "shipmentDao" → 인스턴스

  → 다음엔 재사용

3.5 ILIC 의 맥락

@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; }
}

3.6 자기 점검 답변

없으면 생성 과정은?

:
1. 생성:

  • @Bean 메서드 호출
  1. 시점:

    • AC: 초기화 (eager)
    • BeanFactory: getBean (lazy)
  2. 최초:

    • 한 번만
  3. 저장:

    • 목록에

4️⃣ 의존 빈 주입

4.1 의존 빈 자동 생성

의존 빈 자동:

  빈 A 가 B 필요:
    - B 빈 먼저 생성
    - A 에 주입

  → 재귀적 해결

4.2 주입 흐름

주입 흐름:

  shipmentDao() 호출:
    - connectionMaker() 필요
    - connectionMaker 빈 생성/조회
    - ShipmentDao 에 주입
    - 완성된 ShipmentDao 반환

4.3 의존성 순서

의존성 순서:

  의존 먼저:
    1. connectionMaker 생성
    2. shipmentDao 생성 (cm 주입)
    3. shipmentService 생성 (dao 주입)

  → 의존 그래프 순서

4.4 의존 빈도 싱글톤

// 의존 빈도 싱글톤 (재사용)
@Bean
public ShipmentDao shipmentDao() {
    return new ShipmentDao(connectionMaker());   // 같은 cm
}
@Bean
public BookingDao bookingDao() {
    return new BookingDao(connectionMaker());     // 같은 cm (재사용)
}
// connectionMaker() 는 한 번만 생성 (싱글톤)
// shipmentDao, bookingDao 가 같은 cm 공유

4.5 ILIC 의 맥락

@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; }
}

4.6 자기 점검 답변

의존 빈 자동 생성·주입은?

:
1. 자동:

  • 의존 빈 먼저 생성
  1. 흐름:

    • 재귀적 해결
  2. 순서:

    • 의존 먼저
  3. 싱글톤:

    • 의존 빈 공유

5️⃣ 처음 vs 두 번째 호출

5.1 처음 호출

처음 호출:

  getBean (최초):
    - 목록에 없음 (또는 초기화 시 생성)
    - @Bean 호출
    - 생성·주입·저장
    - 반환

5.2 두 번째 호출

두 번째 호출:

  getBean (재호출):
    - 목록에 있음
    - 저장된 빈 반환
    - 추가 생성 X

  → 같은 인스턴스

5.3 같은 객체

// 같은 객체 (싱글톤)
ShipmentDao dao1 = ctx.getBean(ShipmentDao.class);
ShipmentDao dao2 = ctx.getBean(ShipmentDao.class);

System.out.println(dao1 == dao2);   // true (같은 인스턴스)
// 두 번째도 같은 빈

5.4 싱글톤 예고

싱글톤 예고:

  여러 번 getBean:
    - 항상 같은 빈
    - 1개 인스턴스

  → 싱글톤 (Unit 8.3 상세)

5.5 ILIC 의 맥락

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 { }

5.6 자기 점검 답변

처음 호출 vs 두 번째 호출 차이는?

:
1. 처음:

  • 생성·저장·반환
  1. 두 번째:

    • 저장된 것 반환
  2. 같은 객체:

    • == true
  3. 싱글톤:

    • 항상 같은 빈

6️⃣ @Bean 직접 호출 vs getBean

6.1 두 방식

두 방식:

@Bean 직접 호출:
  connectionMaker()
  - 메서드 직접

getBean:
  ctx.getBean(...)
  - 컨테이너 통해

6.2 직접 호출 의문

직접 호출 의문:

  @Bean
  shipmentDao() {
    return new ShipmentDao(connectionMaker());
    //                     ↑ 직접 호출?
  }

  connectionMaker() 직접 호출:
    - 매번 새 객체?
    - 싱글톤 깨짐?

6.3 프록시가 보장

프록시가 보장:

  @Configuration 클래스:
    - CGLIB 프록시로 감쌈
    - @Bean 메서드 가로챔
    - 목록에서 반환 (같은 빈)

  → 직접 호출해도 싱글톤

6.4 동작

@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() 직접 호출해도 같은 빈 (프록시 덕분)

6.5 ILIC 의 맥락

@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; }
}

6.6 자기 점검 답변

@Bean 메서드 직접 호출 vs getBean 차이는?

:
1. 두 방식:

  • 직접 vs getBean
  1. 직접 호출 의문:

    • 매번 새 객체?
  2. 프록시:

    • CGLIB 가 가로챔
  3. 결과:

    • 직접 호출도 싱글톤

7️⃣ @Configuration 프록시

7.1 프록시

@Configuration 프록시:

  @Configuration 클래스:
    - CGLIB 프록시 생성
    - @Bean 메서드 오버라이드

  → 빈 관리 (싱글톤)

7.2 CGLIB

CGLIB:

  바이트코드 조작:
    - @Configuration 상속
    - @Bean 메서드 가로챔

  → 런타임 프록시

7.3 메서드 가로채기

메서드 가로채기:

  @Bean 메서드 호출 시:
    - 프록시가 가로챔
    - 목록 확인
    - 있으면 반환 (새 생성 X)

  → 싱글톤 보장

7.4 @Configuration 없으면

@Configuration 없으면 (lite mode):

  @Bean 만 (설정 X):
    - 프록시 X
    - 직접 호출 = 새 객체

  → 싱글톤 안 됨 (주의)

7.5 ILIC 의 맥락

// @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; }
}

7.6 자기 점검 답변

@Configuration 프록시는?

:
1. 프록시:

  • CGLIB
  • @Bean 가로챔
  1. CGLIB:

    • 바이트코드 조작
  2. 가로채기:

    • 목록 반환 (싱글톤)
  3. 없으면:

    • lite mode (새 객체)

8️⃣ 빈 조회 방식

8.1 조회 방식

빈 조회 방식:

  - 이름으로 (getBean("name"))
  - 타입으로 (getBean(Type.class))
  - 이름 + 타입
  - 여러 빈 (getBeansOfType)

8.2 타입 조회 권장

// 타입 조회 (권장)
ShipmentDao dao = ctx.getBean(ShipmentDao.class);
// 타입 안전, 캐스팅 X

8.3 같은 타입 여럿

// 같은 타입 여러 빈
Map<String, ConnectionMaker> makers = 
    ctx.getBeansOfType(ConnectionMaker.class);
// 모든 ConnectionMaker 빈

// 특정 선택 (@Qualifier, @Primary)

8.4 실무는 DI

실무는 DI:

  getBean 직접 호출:
    - 드묾 (테스트 등)

  실무:
    - @Autowired / 생성자 주입
    - 컨테이너가 자동 주입
    - getBean 안 보임

8.5 ILIC 의 맥락

// 빈 조회 (테스트/특수 상황)
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(); }

8.6 자기 점검 답변

빈 조회 방식은?

:
1. 방식:

  • 이름/타입/여럿
  1. 타입 권장:

    • 타입 안전
  2. 여럿:

    • getBeansOfType
  3. 실무:

    • DI (getBean X)

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
getBean 흐름?찾기→없으면생성→주입→반환
빈 목록?Map (캐시)
없으면?@Bean 호출
의존 빈?자동 생성·주입
처음 vs 두번째?생성 vs 재사용 (싱글톤)
@Bean 직접 호출?프록시가 같은 빈
@Configuration 프록시?CGLIB 가로챔
조회 방식?이름/타입
실무?DI (getBean X)
lite mode?프록시 X (새 객체)

9.2 자기 점검 체크리스트

getBean 흐름

  • 찾기/생성/주입/반환

빈 목록

  • 캐시

의존 빈

  • 자동 주입

처음/두번째

  • 싱글톤

@Bean 직접

  • 프록시

프록시

  • CGLIB

조회

  • 타입 권장

9.3 추가 심화 질문

Q1: 순환 참조?

답:

  • A → B, B → A
  • 생성자 주입 시 오류 (감지)
  • Setter/필드 주입은 가능 (지양)
  • 설계 재검토 신호

Q2: @Lazy 빈?

답:

  • 지연 생성
  • getBean 시 생성
  • 시작 시 X
  • 무거운 빈 지연

Q3: 프로토타입 스코프?

답:

  • getBean 마다 새 객체
  • 싱글톤 아님
  • @Scope("prototype")
  • 상태 있는 빈

Q4: ObjectProvider?

답:

  • 지연 조회
  • 선택적 의존성
  • 여러 빈 처리
  • 안전한 조회

Q5: getBean 안티패턴?

답:

  • 빈에서 getBean 직접 호출
  • 컨테이너 의존
  • DI 권장
  • 테스트 어려움

🎯 핵심 요약 — 3줄 정리

1. getBean 흐름

  • 빈 목록 찾기 → 없으면 @Bean 호출 (의존 빈 자동 주입) → 저장 → 반환
  • 두 번째부터 저장된 같은 빈 (싱글톤)

2. @Bean 직접 호출

  • @Configuration 은 CGLIB 프록시로 감싸짐
  • @Bean 직접 호출해도 같은 빈 (싱글톤 보장)

3. 실무

  • getBean 직접 호출은 드묾 (테스트 등)
  • 실무는 DI (@Autowired/생성자) → 컨테이너 자동

📚 다음으로...

Unit 8.3 — 싱글톤 레지스트리 ★깊이

이번 Unit에서 getBean 동작을 봤다면, 다음은 싱글톤 레지스트리 (★ 깊이 파기).

  • getBean 100번 = 1객체
  • 왜 싱글톤 (메모리/GC)
  • stateless 필요 (4주차 동시성 연결)
  • 전통 싱글톤 vs Spring 싱글톤

Phase 8 진행 상황

🌱 Phase 8 — Spring 컨테이너
  ✅ Unit 8.1 빈 팩토리와 ApplicationContext
  ✅ Unit 8.2 getBean()의 동작 ← 여기
  ⏭ Unit 8.3 싱글톤 레지스트리 ★깊이
  ⏭ Unit 8.4 의존관계 주입 DI ★깊이 (+ 졸업 시험)

5주차 누적 진행

✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
  ✅ Phase 3~7 (15 Unit)
  🌱 Phase 8 — Spring 컨테이너 (2/4 진행)

총: 24/26 Unit
profile
Software Developer

0개의 댓글