개발이군고구마

JVM 클래스 로딩 vs Spring 컴포넌트 스캔: 같은 ‘로딩’이 아니다 본문

SERVER/Spring

JVM 클래스 로딩 vs Spring 컴포넌트 스캔: 같은 ‘로딩’이 아니다

김구황 2025. 12. 31. 08:09
728x90

Spring을 사용하다 보면 아래와 같은 질문을 자주 갖게 된다. 

  • “이 클래스는 언제 로딩되었길래 사용할 수 있는(또는 없는) 걸까?”,
  • “컴포넌트 스캔을 하면 바로 객체가 생성되는 걸까?”
  • "컴포넌트 스캔의 결과 생성된 객체는 어떤 형태로 어디에 저장되는 걸까?"

 

나 역시 컴포넌트 스캔과 JVM의 클래스 로딩을 같은 흐름으로 이해하고 있었고,
이로 인해 클래스가 메모리에 올라가는 시점Spring이 빈을 생성하는 시점을 명확히 구분하지 못하고 있었다.

이 글에서는 다음 세 가지를 중심으로 정리한다.

  • JVM의 클래스 로딩과 Spring의 컴포넌트 스캔은 어떤 점에서 다른가
  • Spring 컨테이너는 언제 빈을 정의하고 언제 생성하는가
  • JVM에 로딩된 클래스와 Spring 빈은 무엇이 다른가

 

 

1. JVM의 클래스 로딩과 Spring 컴포넌트 스캔은 별개의 과정이다

 

먼저 가장 중요한 결론부터 말하면

✅ JVM의 클래스 로딩과 Spring의 컴포넌트 스캔은 서로 전혀 다른 책임을 가진 과정이다.

 

1️⃣ JVM의 클래스 로딩

  • JVM은 클래스 파일(.class)을 필요한 시점에 메모리에 로딩한다.
    이 과정에서 클래스의 구조 정보(필드, 메서드, 상수 풀 등)가 Method Area에 저장된다.
  • 이 과정은 순수하게 JVM 관점의 동작이며,
    Spring을 사용하든 사용하지 않든 동일하게 동작한다.
즉, 어떤 클래스가 JVM에 로딩되었다는 사실만으로는
그 클래스가 Spring에서 관리되는 객체라는 의미는 아니다

 

 

2️⃣ Spring의 컴포넌트 스캔

@Service
public class UserService {
    @Autowired
    private UserRepository repository;
}
  • 반면 Spring의 컴포넌트 스캔은 IoC 컨테이너의 초기화 과정에서 수행된다.
  • Spring은 설정된 패키지를 탐색하며
    @Component, @Service, @Repository 같은 애노테이션이 붙은 클래스를 찾아낸다.
  • 그리고 이 시점에 객체를 생성하지는 않는다.
  • 대신 Spring은 해당 클래스를 기반으로
    “이런 빈이 필요하다”는 BeanDefinition(빈 정의) 을 먼저 등록한다. (Method area에 클래스 정보 등록)
컴포넌트 스캔의 결과는 객체가 아니라 ‘설계도(BeanDefinition)’ 다.

 

 

 

2. Spring 컨테이너는 언제 빈을 정의하고, 언제 생성할까?

 

이 부분이 가장 혼동되기 쉬운 지점이다. Spring의 빈 처리 과정은 크게 두 단계로 나뉜다.

 

1️⃣ 빈 정의(BeanDefinition) 등록 단계

ApplicationContext가 초기화되면서 다음 작업들이 순서대로 수행된다.

  1. 컴포넌트 스캔 실행
  2. @Component, @Service 등이 붙은 클래스 발견
  3. 각 클래스에 대한 BeanDefinition 생성 및 등록
  4. @Configuration 클래스 내부의 @Bean 메서드들도 BeanDefinition으로 등록

▶ 이 단계까지는 실제 객체는 하나도 생성되지 않는다.

Spring은 단지 “이 컨테이너에는 이런 빈들이 필요하다” 라는 메타정보만 수집한 상태다.

 

 

2️⃣ 빈 생성(Instantiation) 단계

그 다음 단계에서 Spring은 다음과 같은 순서로 동작한다.

  1. BeanFactoryPostProcessor 실행
    → BeanDefinition을 수정하거나 확장
  2. 의존관계 분석
  3. 싱글톤 빈 인스턴스 생성
  4. 의존성 주입
  5. 초기화 메서드 실행
  6. 컨테이너에 빈 등록 완료

▶ 이때 비로소 실제 객체가 Heap 메모리에 생성된다.

컴포넌트 스캔 시점 ≠ 빈 생성 시점
컴포넌트 스캔은 “정의”, 빈 생성은 “실체화” 단계다.

 

 

3️⃣ 코드로 알아보기 

// 1. Component Scan이 이 클래스를 찾음
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @PostConstruct
    public void init() {
        System.out.println("UserService 초기화!");
    }
}
// Phase 4 - BeanDefinition 등록
BeanDefinition bd = new RootBeanDefinition(UserService.class);
bd.setScope("singleton");
beanFactory.registerBeanDefinition("userService", bd);

// Phase 5 - 빈 인스턴스 생성
// Step 10-3-A: 클래스 로딩
Class<?> beanClass = Class.forName("com.example.UserService");

// Step 10-3-B: 인스턴스 생성 ⭐⭐⭐
Constructor<?> ctor = beanClass.getDeclaredConstructor();
Object instance = ctor.newInstance();  // ← new UserService() 실행되는 시점!

// Step 10-3-C: 의존성 주입
Field field = beanClass.getDeclaredField("userRepository");
field.setAccessible(true);
field.set(instance, userRepositoryBean);  // userRepository 주입

// Step 10-3-D: 초기화 콜백
Method initMethod = beanClass.getMethod("init");
initMethod.invoke(instance);  // @PostConstruct 실행

// Step 10-4: 싱글톤 캐시에 저장
singletonObjects.put("userService", instance);

 

 

 

3. JVM에 로딩된 클래스와 Spring 빈의 차이

  JVM에 로딩된 클래스 Spring Bean
관리 주체 JVM Spring IoC 컨테이너
저장 내용 클래스 메타정보 실제 객체 인스턴스
메모리 영역 Method Area Heap
생성 시점 필요할 때 컨테이너 초기화 과정
역할 코드 실행을 위한 준비 애플리케이션 객체 관리
  • JVM은 클래스를 메타정보로 메모리에 올림
  • Spring은 해당 클래스가 IoC 컨테이너에서 필요하다면 인스턴스로 생성해 빈으로 관리

JVM에 클래스가 로딩되었다고 해서

Spring이 그 클래스를 빈으로 관리한다는 보장은 없다.

 

▶ Spring 빈은 JVM 객체 중에서도 IoC 컨테이너가 생명주기를 관리하는 객체다.

 

 

 

4. 전체 흐름 정리 (타임라인)

Spring 초기화 타임라인을 통해  “정의(Definition) → 생성(Instantiation)” 을 명확하게 구분
[JVM 시작] 
  ↓
[Environment 준비] 
  ↓
[BeanFactory 초기화]
  ↓
[Context 생성] 
  ↓
[컴포넌트 스캔 수행]
    - 클래스패스를 스캔하여 .class 파일들을 찾음
    - BeanDefinition 등록
    - (아직 인스턴스 X)
  ↓
[BeanDefinition 등록 📝] ←  Method Area: Spring 관련 클래스들, 스캔된 빈 클래스들 로드됨
  ↓
[BeanFactoryPostProcessor 실행]
  ↓
[싱글톤 빈 생성 시작 🎯]
    ├─ 의존성 분석
    ├─ 의존성부터 재귀적 생성
    ├─ 클래스 로딩 (필요시)
    ├─ ⭐빈 인스턴스 생성 🎯  new UserService() ← 여기!
    ├─ 의존성 주입 💉 ←  @Autowired
    ├─ 초기화 콜백 🔧  ← @PostConstruct
    └─ 싱글톤 캐시 저장
    ↓
[모든 빈 생성 완료]
    ↓
[ApplicationContext 준비 완료 ✅]
 

 

 

5. 정리하며

  • JVM 클래스 로딩과 Spring 컴포넌트 스캔은 책임과 목적이 다르다
  • 컴포넌트 스캔은 빈 정의를 등록하는 과정이지 객체 생성이 아니다
  • Spring 빈은 JVM 객체 중에서도 컨테이너가 생명주기를 관리하는 특별한 객체

이 차이를 명확히 이해해야 Spring의 초기화 과정이나 의존성 주입을 직관적으로 이용하고 활용할 수 있게 된다. 

 

 

 


 

6. 참고 - spring framework 작동 순서와 application context 구조 

[Tomcat 시작]
    ↓
[web.xml 또는 WebApplicationInitializer 읽기]
    ↓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase 2: Root WebApplicationContext 생성
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ↓
[ContextLoaderListener.contextInitialized()]
    ↓
[Root WebApplicationContext 생성 🎯]
    ↓
[RootConfig 로드 및 Component Scan]
    ↓
[Service, Repository 빈 생성]
    - new UserService() ⭐
    - new UserRepository() ⭐
    ↓
[Root Context → ServletContext에 저장]
    ↓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase 3: DispatcherServlet 초기화
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ↓
[DispatcherServlet 인스턴스 생성]
    - new DispatcherServlet() ⭐
    ↓
[DispatcherServlet.init() 호출]
    ↓
[Servlet WebApplicationContext 생성 🎯]
    - parent = Root Context
    ↓
[WebConfig 로드 및 Component Scan]
    ↓
[Controller 빈 생성]
    - new UserController() ⭐
    - UserService 주입 (Root Context에서)
    ↓
[MVC 인프라 빈 생성]
    - RequestMappingHandlerMapping
    - RequestMappingHandlerAdapter
    - ViewResolver
    ↓
[HandlerMapping 초기화]
    - @RequestMapping 정보 수집
    - URL → Controller 메서드 매핑 등록
    ↓
[다른 전략들 초기화]
    ↓
[DispatcherServlet 준비 완료 ✅]
    ↓
[요청 처리 가능 상태]

 

전체 컨텍스트 계층 구조

┌─────────────────────────────────────────────┐
│ ServletContext (Tomcat)                      │
│                                              │
│  ┌────────────────────────────────────┐     │
│  │ Root WebApplicationContext         │     │
│  │ (ContextLoaderListener가 생성)      │     │
│  │                                     │     │
│  │ 빈들:                               │     │
│  │ - dataSource                        │     │
│  │ - userRepository                    │     │
│  │ - userService                       │     │
│  │ - transactionManager                │     │
│  └────────────────────────────────────┘     │
│           ↑ parent                           │
│  ┌────────────────────────────────────┐     │
│  │ Servlet WebApplicationContext       │     │
│  │ (DispatcherServlet이 생성)          │     │
│  │                                     │     │
│  │ 빈들:                               │     │
│  │ - userController ──→ userService   │     │
│  │ - productController                 │     │
│  │ - handlerMapping                    │     │
│  │ - handlerAdapter                    │     │
│  │ - viewResolver                      │     │
│  └────────────────────────────────────┘     │
│           ↓ owns                             │
│  ┌────────────────────────────────────┐     │
│  │ DispatcherServlet 인스턴스          │     │
│  └────────────────────────────────────┘     │
└─────────────────────────────────────────────┘