IoC 개념 · Spring Container가 Bean을 만드는 순서 · 의존성 그래프 · Singleton 보장 원리 · ApplicationContext 구조
07b에서 클래스·생성자를 이해했습니다. 이제 "스프링이 어떻게 그 생성자를 자동으로 호출하는가"를 봅니다.
IoC(제어의 역전) → Spring Container → Bean 스캔 → 의존성 분석 → Bean 생성 순서 결정 → DI 실행 → Singleton 유지.
이 파일을 이해하면 "스프링이 알아서 해준다"가 구체적으로 보입니다.
DI의 상위 개념. DI는 IoC를 구현하는 방법 중 하나입니다. IoC를 모르면 "왜 스프링이 개발자 대신 객체를 만드나"가 이해가 안 됩니다.
// 개발자가 모든 객체 생성을 직접 제어 public static void main(String[] args) { // 개발자가 순서 직접 결정 PaymentService ps = new PaymentService(); // 직접 생성 OrderService os = new OrderService(ps); // 직접 주입 os.order(); }
개발자가 어떤 객체를 언제 만들지, 어떤 순서로 주입할지 전부 직접 결정.
코드가 복잡해질수록 의존 관계 관리가 매우 힘들어짐.
테스트할 때마다 전체 설정을 바꿔야 함.
// 개발자는 정의만 함 @Service public class PaymentService { ... } @Service public class OrderService { public OrderService(PaymentService ps) { this.ps = ps; } } // 생성, 주입, 순서 → 스프링이 알아서 결정
제어권이 개발자에서 스프링으로 역전(Inversion)됨.
개발자는 "이런 클래스 있고, 이렇게 연결해줘" 표시만.
실제 생성·주입·순서는 스프링이 담당.
IoC (제어의 역전): 개념. "제어권을 개발자에서 프레임워크로 넘긴다"는 원칙.
DI (의존성 주입): 구현. IoC를 실현하는 방법 중 하나. 의존 객체를 외부에서 주입받음.
DI는 IoC의 구체적인 구현 방식입니다. IoC 없이는 DI도 없습니다.
스프링 컨테이너가 바로 IoC 컨테이너이고, DI를 자동으로 처리해주는 주체입니다.
SpringApplication.run()이 실행되면 스프링이 내부에서 어떤 순서로 일하는지 전부 봅니다.
각 단계에서 JVM 메모리가 어떻게 변하는지 함께 추적합니다.
main()에서 SpringApplication.run(BoardApplication.class, args) 실행 →@SpringBootApplication 위치의 패키지부터 하위 패키지 전체 탐색 →@Component, @Service, @Repository, @Controller, @RestController, @Configuration 찾기 →// [5-1] PaymentService: 의존성 없음 → 먼저 생성 // 내부: new PaymentService() // → Stack: 기본 생성자 프레임 push/pop // → Heap: PaymentService 객체 생성 (주소: 0x1A2B) // → ApplicationContext에 등록: "paymentService" → 0x1A2B // [5-2] OrderService: PaymentService 필요 → 5-1 완료 후 생성 // 내부: new OrderService(paymentServiceBean) // ↑ 5-1에서 만든 0x1A2B를 생성자에 넣음 // → Stack: 생성자 프레임 push // → this.paymentService = 0x1A2B (DI 실행!) // → Stack: 생성자 프레임 pop // → Heap: OrderService 객체 생성 (주소: 0x3C4D) // OrderService.paymentService = 0x1A2B 연결됨 // → ApplicationContext에 등록: "orderService" → 0x3C4D
@Transactional이나 @Aspect 대상 Bean → CGLIB Proxy 객체로 교체@PostConstruct 붙은 초기화 메서드 실행
스프링은 생성자의 매개변수를 분석합니다.
UserService(UserRepository repo, BCryptPasswordEncoder encoder)를 보면 →
"UserService를 만들려면 UserRepository와 BCryptPasswordEncoder가 먼저 있어야 한다"고 파악.
재귀적으로 의존성을 분석해서 최종 생성 순서를 결정합니다.
그래서 개발자가 생성 순서를 신경 쓰지 않아도 됩니다.
// ApplicationContext 내부 (개념적 코드) public class ApplicationContext { // Bean들을 이름 → 객체 로 저장하는 Map private final Map<String, Object> singletonBeans = new HashMap<>(); public Object getBean(String beanName) { // 이미 만들어진 Bean이 있으면 그걸 반환 if (singletonBeans.containsKey(beanName)) { return singletonBeans.get(beanName); // 기존 객체 반환 } // 없으면 새로 만들어서 저장 후 반환 Object bean = createBean(beanName); singletonBeans.put(beanName, bean); // 저장 → 이후 재사용 return bean; } } // 결과: // userService 요청 1번 → new UserService() → Heap 0x3C4D → 저장 // userService 요청 2번 → 이미 있음 → 0x3C4D 반환 (new 안 함) // userService 요청 1000번 → 여전히 0x3C4D 반환 // → 항상 같은 객체 → Singleton 보장
Bean이 Singleton(딱 하나)이므로 → 동시에 수천 개의 요청이 같은 Bean 객체를 사용 →
Bean 안에 상태를 저장하면 안 됩니다 (멀티스레드 문제).
나쁜 예: private int requestCount = 0; → 여러 스레드가 동시에 읽고 쓰면 값이 뒤섞임.
좋은 예: 메서드의 지역변수 사용 → Stack에 저장 → 스레드마다 독립적 → 안전.
DI로 주입받은 final 필드는 초기화 후 읽기만 하므로 안전합니다.
| Scope 종류 | 설명 | 사용 시점 | 메모리 |
|---|---|---|---|
| singleton (기본) | 컨테이너에 딱 하나 | Service, Repository, Controller — 대부분 | Heap — 서버 살아있는 한 유지 |
| prototype | 요청할 때마다 새 객체 | 상태를 가진 객체 (거의 안 씀) | Heap — 요청마다 생성, GC 대상 |
| request | HTTP 요청마다 새 객체 | 웹 요청별 상태 (거의 안 씀) | Heap — 요청마다 생성, 요청 끝나면 GC |
@Service/@Component는 클래스에 붙이면 스프링이 자동 스캔합니다. @Configuration + @Bean은 개발자가 직접 Bean 생성 방법을 코드로 정의할 때 씁니다. 3차 프로젝트의 SecurityConfig, BCryptPasswordEncoder 등록이 이 방식입니다.
@Configuration // ↑ 이 클래스가 Bean 설정 클래스임을 표시 // @Component 계열이므로 스프링이 스캔해서 Bean으로 등록 // 내부적으로 CGLIB Proxy로 만들어짐 (Singleton 보장 위해) public class AppConfig { @Bean // ↑ 이 메서드의 반환값을 Bean으로 등록 // 메서드 이름이 Bean 이름이 됨 (passwordEncoder) public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // 스프링이 이 메서드를 딱 한 번 호출 → 반환된 객체를 Heap에 저장 // 이후 BCryptPasswordEncoder 필요한 곳에 자동 주입 } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // HttpSecurity는 스프링이 자동 주입 (메서드 매개변수 DI) return http .authorizeHttpRequests(...) .build(); // 반환된 SecurityFilterChain → Bean으로 등록 } } // 실행 시점: // 스프링 기동 시 → AppConfig Bean 생성 → @Bean 메서드들 실행 // → passwordEncoder() 실행 → BCryptPasswordEncoder 객체 → Heap // → filterChain() 실행 → SecurityFilterChain 객체 → Heap