Dev Log/Java & Spring

[Spring 프레임워크] - IoC/DI, Bean 생명주기

eomoff 2026. 9. 23. 22:29

1. 프레임워크와 라이브러리

1-1 프레임워크는 왜 쓰는가

프레임워크는 구조가 이미 짜여 있고, 개발자는 그 구조 안에 코드를 추가하는 방식으로 개발합니다. 그래서 일정한 품질이 보장된 결과물을 얻을 수 있습니다.

1-2 라이브러리 vs 프레임워크

구분 라이브러리 프레임워크
주도권 개발자 프레임워크
호출 방향 내 코드 → 라이브러리 프레임워크 → 내 코드
설명 특정 기능을 제공하도록 미리 만들어 둔 클래스/메서드의 모음. 개발자가 필요한 시점에 직접 가져와 호출하고, 전체 흐름은 개발자 코드가 결정 애플리케이션의 구조와 실행 흐름을 미리 정해 둔 틀. 개발자는 정해진 규칙에 맞춰 코드를 작성하고, 그 코드를 언제 호출할지는 프레임워크가 결정
예시 Jackson, Apache Commons Spring, JUnit
// 라이브러리: 내가 필요할 때 내가 부른다
String json = objectMapper.writeValueAsString(member);

// 프레임워크: 규칙대로 만들어 두면 프레임워크가 부른다
@GetMapping("/members")
public List<Member> list() { ... }

라이브러리와 프레임워크의 차이는 주도권이 누구에게 있느냐입니다.

1-3 할리우드 원칙

프레임워크의 호출 방향을 흔히 할리우드 원칙(Don't call us, we'll call you, 뜻: 우리한테 연락하지 마세요, 필요하면 우리가 연락할게요)이라고 설명합니다.

  • 할리우드 오디션장에서 배우들에게 하던 말입니다. 오디션을 본 배우가 "저 붙었나요?" 하고 계속 전화하지 말고, 배역이 필요해지면 제작사가 먼저 연락하겠다는 뜻입니다. 즉 언제 부를지는 제작사가 정하고, 배우는 준비해 두고 기다리라는 뜻입니다.
  • 예:
    • @Controller 클래스 안에서 @GetMapping 등이 붙은 메서드(핸들러 메서드)는 요청이 오면 DispatcherServlet이 호출합니다.
    • @PostConstruct가 붙은 메서드는 의존성 주입이 끝나면 스프링 컨테이너가 호출합니다.
    • @Test가 붙은 메서드는 테스트를 실행하면 JUnit이 호출합니다.

1-4 프레임워크의 단점

러닝 커브가 길다(해당 프레임워크의 구조를 공부하는 데 시간이 오래 걸린다)는 것입니다.

1-5 스프링 프레임워크 vs 스프링 부트

  • 가장 큰 차이 가운데 하나는 부트에서는 톰캣을 따로 설정하지 않아도 된다는 점입니다(내장 톰캣).
  • 스프링 프레임워크에서는 개발자가 일일이 설정해야 하는 파일이 많았습니다. 부트는 그 설정을 프레임워크가 알아서 해 줍니다
  • 결론은 "개발자가 비즈니스 코드에 집중할 수 있는 환경을 만들어 준다" 로 맺으면 됩니다.

2. IoC (Inversion of Control, 제어의 역전)

2-1 정의

주도권의 이동, 프로그램의 제어 흐름(객체를 언제 만들고, 무엇과 연결하고, 언제 호출하고, 언제 없앨지)을 개발자가 직접 통제하지 않고 외부(프레임워크)에 넘기는 설계 원칙입니다.

프로그램의 제어 흐름을 개발자가 직접 통제하는 것이 아니라 스프링 같은 외부의 프레임워크가 통제하게 하는 설계 원칙

2-2 IoC가 주는 이점

  • 객체 생성·연결 코드가 비즈니스 코드에서 빠집니다 → 비즈니스 로직에 집중할 수 있습니다.
  • 구현체를 바꿀 때 사용하는 쪽 코드를 고치지 않아도 됩니다 (OCP).
  • 테스트에서 가짜 객체(Mock/Stub)를 끼워 넣기 쉽습니다.

3. IoC 컨테이너와 빈(Bean)

3-1 정의

  • 빈(Bean): IoC 컨테이너가 생성·관리하는 객체입니다.
  • IoC 컨테이너: 빈의 생성 → 의존관계 설정 → 사용 → 소멸까지 생명주기를 관리하는 주체입니다. 스프링에서는 ApplicationContext가 이 역할을 합니다.

정리: 객체의 생성, 관계 설정, 사용, 제거에 이르는 빈의 생명주기를 관리해 주는 컨테이너가 IoC 컨테이너다.

3-2 BeanFactory vs ApplicationContext

BeanFactory ApplicationContext
위치 최상위 컨테이너 인터페이스 BeanFactory를 상속한 확장
핵심 기능 빈 조회·생성·DI + 메시지 국제화, 이벤트 발행, 리소스 로딩, 환경변수(Environment)
싱글톤 생성 시점 지연 로딩(처음 getBean 할 때) 컨테이너 시작 시점에 미리 생성(eager)
실무 직접 쓸 일 거의 없음 이것만 쓴다고 보면 됨
  • 스프링 부트의 웹 애플리케이션은 AnnotationConfigServletWebServerApplicationContext 같은 구현체를 씁니다.
  • eager 생성 덕분에 설정 오류·순환 참조를 애플리케이션 구동 시점에 발견합니다.

3-3 BeanDefinition: 빈 생성 정보(빈 메타데이터)

BeanDefinition빈을 만드는 데 필요한 정보를 담아 두는 스프링의 인터페이스입니다. 컨테이너는 이 객체에 담긴 정보를 읽고 빈을 생성합니다.

동작 순서

  1. 스프링이 설정 정보(XML의 <bean>, @Component가 붙은 클래스, @Bean 메서드)를 읽습니다.
  2. 빈 하나마다 BeanDefinition 객체를 하나씩 만들어 컨테이너에 등록합니다. 이 시점에는 아직 빈 객체가 생성되지 않습니다.
  3. 컨테이너가 등록된 BeanDefinition의 정보대로 실제 빈 객체를 생성합니다.

XML, @Component, @Bean 중 어떤 방식으로 설정하든 모두 BeanDefinition으로 바뀌어 등록되므로, 컨테이너가 빈을 만드는 과정은 같습니다.

이 흐름과 관련된 에러

에러 이름에 BeanDefinition이 들어가면 "컨테이너에 등록된 빈 정보"에 문제가 있다는 뜻입니다. 모두 빈을 생성하는 애플리케이션 구동 시점에 발생합니다.

  • NoSuchBeanDefinitionException
    • 뜻: 주입하려는 타입의 빈이 등록되지 않았습니다.
    • 흔한 원인: @Component(@Service 등)를 빠뜨렸거나, 클래스가 컴포넌트 스캔 범위(@SpringBootApplication이 있는 패키지와 그 하위) 밖에 있습니다.
  • NoUniqueBeanDefinitionException
    • 뜻: 같은 타입의 빈이 두 개 이상 등록돼 있어 무엇을 주입할지 정할 수 없습니다.
    • 흔한 원인: 한 인터페이스의 구현체 여러 개에 모두 @Component를 붙였습니다. 해결 방법은 5-4를 참고하세요.
// NoSuchBeanDefinitionException 예시
public class MemberRepository { ... }        // @Repository를 빠뜨림 → BeanDefinition이 등록되지 않음

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;   // 주입할 빈이 없어 구동 실패
}

4. 빈 등록 방식

4-1 XML (전통 방식)

<bean id="memberService" class="com.example.MemberService">
    <constructor-arg ref="memberRepository"/>
</bean>
<bean id="memberRepository" class="com.example.JpaMemberRepository"/>

예전에는 빈 하나를 지우거나 패키지 이름을 바꾸려면 이 설정을 전부 수정해야 했고, 톰캣도 따로 관리해야 했습니다.

4-2 컴포넌트 스캔 + @Component (자동 등록)

@Service   // @Component 포함
public class MemberService { ... }
  • @ComponentScan이 지정 패키지(부트에서는 @SpringBootApplication이 있는 패키지와 그 하위)를 훑어 @Component가 붙은 클래스를 빈으로 등록합니다.
  • @Controller, @Service, @Repository, @Configuration은 모두 @Component를 메타 애너테이션으로 가집니다.
    • @Repository: 데이터 접근 예외를 스프링의 DataAccessException으로 변환해 줍니다.
    • @Controller: 스프링 MVC가 핸들러로 인식합니다.
  • 빈 이름 기본값은 클래스명의 첫 글자를 소문자로 바꾼 것입니다 (MemberServicememberService).

4-3 @Configuration + @Bean (수동 등록)

@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new JpaMemberRepository();
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
}

4-4 @Bean vs @Component

@Bean은 메서드 레벨, @Component는 클래스 레벨.

@Component @Bean
선언 위치 클래스 @Configuration(또는 @Component) 클래스 안의 메서드
등록 방식 컴포넌트 스캔으로 자동 메서드 반환값을 수동 등록
소스 수정 내가 만든 클래스에만 가능 소스를 못 고치는 외부 라이브러리 클래스도 등록 가능 (ObjectMapper, RestTemplate, PasswordEncoder 등)
생성 로직 제어 생성자에 맡김 메서드 안에서 조건·설정값을 넣어 세밀하게 생성
빈 이름 클래스명 기반 메서드명 기반 (@Bean(name = "...")로 변경)

언제 무엇을 쓰나

  • 직접 만든 컨트롤러·서비스·레포지토리 → @Component 계열을 씁니다.
  • 외부 라이브러리 객체, 구현체를 설정으로 갈아끼우는 기술 지원 객체, 설정값에 따라 다르게 생성해야 하는 객체 → @Bean을 씁니다.

@Configuration의 싱글톤 보장 (CGLIB)

위 4-3 예제에서 memberService()memberRepository()를 직접 호출하는데도 JpaMemberRepository는 한 번만 만들어집니다.

  • @Configuration 클래스는 스프링이 CGLIB으로 상속한 프록시 클래스(AppConfig$$SpringCGLIB$$0)를 빈으로 등록합니다.
  • 프록시는 @Bean 메서드 호출을 가로채 "이미 컨테이너에 있으면 그걸 반환, 없으면 생성 후 등록"합니다.
  • @Configuration 없이 @Component@Bean을 쓰거나 @Configuration(proxyBeanMethods = false)로 두면 이 보장이 사라져 호출할 때마다 새 객체가 생깁니다(이른바 lite 모드).

5. DI (Dependency Injection, 의존성 주입)

5-1 정의

객체가 필요로 하는 의존 객체를 자기가 new로 만들지 않고 외부(컨테이너)에서 넣어 받는 것입니다. IoC를 구현하는 대표적인 방법입니다.

// DI 없이: MemberService가 구현체를 직접 선택 → 강한 결합
public class MemberService {
    private final MemberRepository repo = new JpaMemberRepository();
}

// DI: 어떤 구현체가 올지 MemberService는 모른다 → 느슨한 결합
public class MemberService {
    private final MemberRepository repo;
    public MemberService(MemberRepository repo) { this.repo = repo; }
}

5-2 주입 방식 4가지

// 1) 생성자 주입 — 권장
@Service
public class OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자가 하나면 @Autowired 생략 가능 (Spring 4.3+)
    public OrderService(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

// 2) 세터 주입 — 선택적·변경 가능한 의존관계
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) { this.discountPolicy = discountPolicy; }

// 3) 필드 주입 — 간결하지만 비권장
@Autowired
private MemberRepository memberRepository;

// 4) (일반) 메서드 주입 — 아무 메서드에 @Autowired, 여러 의존을 한 번에
@Autowired
public void init(MemberRepository m, DiscountPolicy d) { ... }

실무에서는 롬복의 @RequiredArgsConstructorfinal 필드의 생성자를 자동 생성하는 형태가 표준입니다.

@Service
@RequiredArgsConstructor
public class OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

5-3 왜 생성자 주입인가

이유 설명
불변성 생성자는 객체 생성 시 딱 한 번 호출됩니다 → 필드를 final로 둘 수 있고 이후 바뀌지 않습니다. ️
필수 의존관계 보장 의존 객체 없이는 객체를 만들 수 없습니다 → NullPointerException을 예방합니다. final이면 누락 시 컴파일 에러가 납니다. ️
순환 참조 조기 발견 A↔B 생성자 순환은 컨테이너가 빈을 만드는 애플리케이션 구동 시점BeanCurrentlyInCreationException으로 실패합니다. 스프링 부트 2.6부터는 필드·세터 주입 순환 참조도 기본으로 금지됩니다. ️
테스트 용이성 스프링 없이 new OrderService(fakeRepo, fakePolicy)로 순수 자바 단위 테스트를 할 수 있습니다.
프레임워크 비의존 필드 주입은 컨테이너(리플렉션) 없이는 값을 넣을 방법이 없습니다.

순환 참조는 애플리케이션 구동 시점(빈 생성 시점)에 에러가 발생해서 바로 알 수 있다"

필드 주입의 단점

  • final을 쓸 수 없어 불변이 보장되지 않습니다.
  • 외부에서 값을 넣을 방법이 없어 순수 단위 테스트가 어렵습니다(리플렉션 필요).
  • 의존성이 많아져도 티가 나지 않습니다. 생성자 파라미터가 10개면 "이 클래스 책임이 너무 많다"는 신호가 보이는데, 필드 주입은 그 신호를 숨깁니다.

5-4 같은 타입의 빈이 여러 개일 때

public interface DiscountPolicy {}
@Component class FixDiscountPolicy implements DiscountPolicy {}
@Component class RateDiscountPolicy implements DiscountPolicy {}
// → DiscountPolicy 주입 시 NoUniqueBeanDefinitionException
해결 방법
@Primary 기본으로 쓸 빈에 붙입니다
@Qualifier("rate") 주입받는 쪽에서 이름을 지정합니다. @Primary보다 우선합니다
파라미터 이름 매칭 타입 매칭 후 여러 개면 파라미터/필드 이름과 빈 이름을 비교합니다
전부 받기 List<DiscountPolicy>, Map<String, DiscountPolicy>로 주입합니다 → 전략 패턴에 유용합니다

6. 빈 스코프

스코프 생존 범위 비고
singleton (기본) 컨테이너 시작 ~ 종료 컨테이너당 1개
prototype 요청할 때마다 새로 생성 컨테이너는 생성·주입·초기화까지만 관여, 소멸 콜백 호출 안 함
request HTTP 요청 하나 웹 전용
session HTTP 세션 하나 웹 전용
application ServletContext 웹 전용
websocket 웹소켓 세션 웹 전용

6-1 싱글톤 빈은 무상태(stateless)로

싱글톤 빈 하나를 여러 요청 스레드가 동시에 씁니다. 인스턴스 필드에 요청별 값을 저장하면 다른 사용자의 값이 섞입니다.

@Service
public class PriceService {
    private int price;              // ❌ 공유 필드 — 동시성 버그
    public int order(int p) {
        int localPrice = p;         // ✅ 지역 변수 / 파라미터 / ThreadLocal 사용
        return localPrice;
    }
}

6-2 싱글톤이 프로토타입(또는 request) 빈을 의존할 때

싱글톤은 생성 시점에 한 번만 주입받으므로, 프로토타입을 주입받아도 계속 같은 인스턴스를 쓰게 됩니다. 쓸 때마다 새로 받으려면 다음과 같이 합니다.

@Service
@RequiredArgsConstructor
public class ClientService {
    private final ObjectProvider<PrototypeBean> provider;
    public void logic() {
        PrototypeBean bean = provider.getObject(); // 매번 컨테이너에서 새로 조회 (DL)
    }
}
  • 또는 @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)로 가짜 프록시를 주입해 두고, 실제 호출 시점에 진짜 빈을 찾게 할 수도 있습니다.

6-3 스프링 싱글톤 vs 싱글톤 패턴

스프링 빈이 기본적으로 싱글톤이라고 해서, 클래스에 싱글톤 패턴을 직접 구현한 것과 같지는 않습니다.

// 싱글톤 패턴: 클래스가 스스로 인스턴스를 하나만 만들도록 강제
public class MemberService {
    private static final MemberService INSTANCE = new MemberService();
    private MemberService() {}                          // 외부에서 new 금지
    public static MemberService getInstance() { return INSTANCE; }
}

// 스프링 싱글톤: 평범한 클래스, 컨테이너가 하나만 만들어 관리
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    public MemberService(MemberRepository memberRepository) { ... }
}

싱글톤 패턴의 문제

  • 클래스 안에 싱글톤을 위한 코드(private 생성자, static 필드·메서드)가 들어가야 합니다.
  • 사용하는 쪽이 MemberService.getInstance()로 구체 클래스를 직접 호출하므로, 구현체를 바꾸기 어렵습니다(DIP 위반).
  • private 생성자 때문에 테스트용 객체를 새로 만들거나 상속하기 어렵습니다.
  • static 상태가 테스트 사이에 공유되어, 테스트끼리 서로 영향을 줍니다.

스프링 싱글톤이 해결한 방식

  • 클래스는 평범한 자바 클래스로 두고, 인스턴스를 하나만 만드는 책임은 스프링 컨테이너가 집니다. 스프링 컨테이너가 싱글톤 객체를 생성·보관하는 역할을 해서 "싱글톤 레지스트리"라고 부릅니다.
  • 의존 객체는 DI로 받으므로 인터페이스에 의존할 수 있고, 테스트에서는 new MemberService(fakeRepository)로 직접 만들 수 있습니다.
  • 단, 인스턴스가 하나라는 점은 같으므로 6-1의 무상태 원칙은 똑같이 지켜야 합니다.

7. 빈 생명주기 ⭐

7-1 한 줄 요약

스프링 컨테이너 생성 → 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료

7-2 상세 흐름

스프링은 아래 순서대로 빈을 만들고 없앱니다. 각 단계에서 스프링이 무엇을 하는지를 먼저 쓰고, 그 단계에서 실행되는 코드가 있으면 이어서 적었습니다.

애플리케이션 시작

  1. 정의 등록이어서 BeanFactoryPostProcessor가 실행되어 등록된 정보를 수정할 수 있습니다. (예: ${} 플레이스홀더를 실제 설정값으로 바꿈)
  2. 스프링이 설정(@Component, @Bean, XML)을 읽어 BeanDefinition으로 등록합니다. 아직 빈 객체는 만들어지지 않았습니다.

빈 생성·초기화 (싱글톤 빈 하나하나마다 반복)

  1. 객체 생성
  2. 스프링이 클래스의 생성자를 호출해 객체를 만듭니다. 생성자 주입을 썼다면 의존 객체도 이때 함께 들어갑니다.
  3. 의존관계 주입
  4. @Autowired가 붙은 세터와 필드에 의존 객체를 넣습니다.
  5. 컨테이너 정보 전달
  6. 클래스가 BeanNameAware, ApplicationContextAware 같은 인터페이스를 구현했다면, 스프링이 그 메서드를 호출해 빈 이름이나 ApplicationContext를 넘겨줍니다.
  7. @PostConstruct 실행
  8. BeanPostProcessor가 동작하면서 @PostConstruct가 붙은 메서드를 호출합니다.
  9. 초기화 메서드 실행
  10. InitializingBean을 구현했다면 afterPropertiesSet()을, @Bean(initMethod = "...")로 지정했다면 그 메서드를 순서대로 호출합니다.
  11. 프록시로 교체
  12. BeanPostProcessor가 한 번 더 동작합니다. 클래스에 @Transactional, @Async 같은 애너테이션이 있으면 원본 객체 대신 프록시 객체를 만들어 컨테이너에 등록합니다.

사용

  1. 사용
  2. 등록된 빈(또는 프록시)이 다른 빈에 주입되어 요청을 처리합니다.

애플리케이션 종료 (close())

  1. 소멸 메서드 실행
  2. @PreDestroy가 붙은 메서드 → DisposableBeandestroy()@Bean(destroyMethod = "...")로 지정한 메서드 순서로 호출합니다. 싱글톤 빈만 해당되고, 프로토타입 빈은 호출하지 않습니다.

7-3 왜 생성자와 초기화를 분리하나

  • 생성자 시점에는 주입이 다 끝나지 않았을 수 있습니다. 필드·세터 주입은 생성자 호출 에 일어나므로, 생성자에서 주입받을 필드를 쓰면 null입니다.
  • 단일 책임: 생성자는 필수 값을 받아 객체를 "만드는" 일만 하고, 외부 연결(DB 커넥션, 캐시 워밍업, 소켓 연결 등) 같은 무거운 "동작"은 초기화 콜백으로 분리하는 것이 유지보수에 좋습니다.
  • 초기화 콜백은 모든 의존관계 주입이 끝난 뒤 호출되는 것이 보장됩니다.

7-4 초기화·소멸 콜백 3가지 방법

// 방법 1) 인터페이스 — 스프링 전용 인터페이스에 의존, 메서드명 변경 불가 → 지금은 거의 안 씀
public class NetworkClient implements InitializingBean, DisposableBean {
    @Override public void afterPropertiesSet() { connect(); }
    @Override public void destroy() { disconnect(); }
}

// 방법 2) @Bean 속성 — 코드를 고칠 수 없는 외부 라이브러리에 적합
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() { return new NetworkClient(); }

// 방법 3) 애너테이션 — 권장 (자바 표준, Boot 3부터 jakarta.annotation 패키지)
public class NetworkClient {
    @PostConstruct public void init()  { connect(); }
    @PreDestroy    public void close() { disconnect(); }
}
방법 장점 단점
InitializingBean / DisposableBean 스프링 인터페이스 의존, 이름 고정, 외부 라이브러리 적용 불가
@Bean(initMethod, destroyMethod) 외부 라이브러리에도 적용 가능, 스프링 비의존 설정 코드에 따로 적어야 함
@PostConstruct / @PreDestroy 가장 간편, 자바 표준(JSR-250), 컴포넌트 스캔과 잘 어울림 외부 라이브러리 클래스엔 못 붙임

결론: 기본은 @PostConstruct/@PreDestroy를 쓰고, 코드를 못 고치는 외부 라이브러리는 @Bean(initMethod, destroyMethod)를 씁니다.

  • 세 개를 동시에 쓰면 실행 순서는 다음과 같습니다.
    • 초기화: @PostConstructafterPropertiesSet()initMethod
    • 소멸: @PreDestroydestroy()destroyMethod
  • destroyMethod 추론: @BeandestroyMethod 기본값은 (inferred)여서, closeshutdown이라는 이름의 메서드가 있으면 자동으로 호출됩니다. 외부 라이브러리의 커넥션 풀 등은 대부분 close()를 가지고 있으므로 따로 적지 않아도 정리됩니다. 막으려면 destroyMethod = ""로 둡니다.

7-5 순서 확인 예제

public class LifecycleBean implements BeanNameAware, InitializingBean, DisposableBean {

    private Dependency dependency;

    public LifecycleBean() { log("1. 생성자 — dependency=" + dependency); } // null

    @Autowired
    public void setDependency(Dependency d) { this.dependency = d; log("2. 세터 주입"); }

    @Override public void setBeanName(String name) { log("3. BeanNameAware: " + name); }

    @PostConstruct public void postConstruct()  { log("4. @PostConstruct"); }
    @Override public void afterPropertiesSet()  { log("5. afterPropertiesSet"); }
    public void customInit()                    { log("6. initMethod"); }

    @PreDestroy public void preDestroy()        { log("7. @PreDestroy"); }
    @Override public void destroy()             { log("8. DisposableBean.destroy"); }
    public void customDestroy()                 { log("9. destroyMethod"); }

    private void log(String s) { System.out.println(s); }
}

@Configuration
class Config {
    @Bean(initMethod = "customInit", destroyMethod = "customDestroy")
    LifecycleBean lifecycleBean() { return new LifecycleBean(); }
    @Bean Dependency dependency() { return new Dependency(); }
}

// ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
// ac.close();  → 1 ~ 9 순서로 출력

7-6 자주 틀리는 포인트

  1. @PostConstruct 안에서는 @Transactional이 적용되지 않습니다.
    @PostConstruct는 7-2의 4단계(원본 객체에서 실행)에서, 트랜잭션 프록시는 6단계에서 만들어집니다. 게다가 this.method() 호출은 프록시를 거치지 않습니다(자기 호출). 초기 데이터 적재처럼 트랜잭션이 필요한 초기화는 @EventListener(ApplicationReadyEvent.class)ApplicationRunner에서 하거나, 별도 빈의 @Transactional 메서드를 호출합니다. → AOP·프록시는 8장 다음인 9장을 참고하세요.
  2. 프로토타입 빈은 @PreDestroy가 호출되지 않습니다. 컨테이너가 초기화까지만 하고 클라이언트에게 넘기므로, 정리는 받은 쪽의 책임입니다.
  3. 소멸 콜백은 컨테이너가 정상 종료(close(), 부트는 JVM 셧다운 훅)될 때만 호출됩니다. kill -9처럼 강제 종료하면 호출되지 않습니다.
  4. 싱글톤 빈 생성 시점은 컨테이너 시작 시점입니다. @Lazy를 붙이면 처음 사용할 때로 미룰 수 있지만, 설정 오류를 늦게 발견하게 됩니다.
  5. ApplicationContextAware로 컨테이너를 직접 꺼내 getBean() 하는 코드(Service Locator)는 DI의 장점을 버리는 것이라 가급적 피합니다.

8. 스프링이 빈을 등록하는 과정 (부트 기준 흐름)

  1. IoC 컨테이너 생성: 애플리케이션을 실행하면 스프링이 IoC 컨테이너(ApplicationContext)를 만듭니다.
  2. 컴포넌트 스캔: 메인 클래스가 있는 패키지와 그 하위에서 @Component가 붙은 클래스를 찾아 빈으로 등록합니다.
  3. 오토 컨피그레이션: 추가한 라이브러리와 설정값을 보고, 필요한 빈을 스프링 부트가 대신 등록합니다.
  4. 빈 생성: 등록된 정보대로 싱글톤 빈을 만들고 의존관계를 주입합니다.
  5. 내장 톰캣 시작: 모든 빈이 준비되면 요청을 받을 수 있는 상태가 됩니다.

정리: 애플리케이션을 실행하면 스프링이 IoC 컨테이너를 만들고, 컴포넌트 스캔으로 @Component가 붙은 클래스를 찾아 빈으로 등록합니다. 스프링 부트는 여기에 오토 컨피그레이션으로 필요한 빈을 자동으로 등록해 줍니다. 등록이 끝나면 싱글톤 빈을 생성하고 의존관계를 주입합니다.