서론: Spring 의 Ioc 컨테이너와 의존성 주입 (DI)
Spring 프레임워크의 핵심 가치는 제어의 역전(Inversion of Control, IoC) 컨테이너와 의존성 주입(Dependency Injection, DI) 메커니즘에 있습니다. 이러한 개념은 최신 엔터프라이즈 애플리케이션 개발에서 느슨한 결합(Loose Coupling)과 높은 테스트 용이성을 달성하기 위한 기반을 제공합니다.
핵심 개념
- 제어의 역전 (IoC): 전통적인 프로그래밍에서는 객체가 자신이 사용할 다른 객체를 직접 생성하거나 찾는 반면, IoC에서는 객체 생성, 생명주기 관리, 의존성 연결 등의 제어권이 개발자의 코드에서 외부 컨테이너(Spring IoC 컨테이너)로 이전됩니다. 이는 객체 간의 결합도를 낮추고 코드의 유연성과 재사용성을 높이는 핵심 원리입니다. 컨테이너는 설정 메타데이터(XML, 어노테이션, Java 코드)를 사용하여 객체(빈)를 관리합니다.
- 의존성 주입 (DI): DI는 IoC를 구현하는 구체적인 디자인 패턴입니다. 객체가 필요로 하는 의존성(다른 객체)을 직접 생성하거나 찾는 대신, 외부(컨테이너)에서 해당 의존성을 객체에 주입(전달)해주는 방식입니다. 의존성은 주로 생성자 인수, 팩토리 메소드 인수, 또는 객체 인스턴스가 생성된 후 설정되는 속성(setter 메소드)을 통해 주입됩니다. DI를 통해 코드는 더욱 명확해지고, 의존하는 객체의 구체적인 구현이나 위치를 알 필요가 없어 효과적인 분리가 가능해집니다.
- ApplicationContext: ApplicationContext는 Spring의 중심적인 IoC 컨테이너 인터페이스입니다. 기본적인 빈 관리 기능을 제공하는 BeanFactory 인터페이스를 확장하여, 트랜잭션 관리, 국제화(i18n) 지원, 이벤트 발행 등 다양한 엔터프라이즈급 기능을 추가로 제공합니다. 대부분의 Spring 애플리케이션에서는 ApplicationContext 구현체(예: AnnotationConfigApplicationContext, ClassPathXmlApplicationContext)를 IoC 컨테이너로 사용합니다. 이는 설정 메타데이터를 로드하고, 빈 정의를 해석하며, 빈 인스턴스를 생성하고 의존성을 주입하는 전체 과정을 관리합니다.
- 빈 (Beans): 빈은 Spring IoC 컨테이너에 의해 인스턴스화되고, 조립되고, 관리되는 객체들을 의미합니다. 컨테이너는 설정 메타데이터(빈 정의)에 기술된 정보를 바탕으로 빈을 생성하고 관리합니다.
본 문서는 Spring Framework 6.2.5 버전을 기준으로 어노테이션 기반 빈 관리 메커니즘을 상세히 설명합니다. 이 버전까지 도입된 개선 사항과 변경점들을 반영하여, 빈이 어떻게 발견되고, 정의되고, 인스턴스화되며, 의존성이 주입되어 관리되는지 전체 생명주기를 다룹니다. Spring의 핵심 설계 원칙 중 하나는 설정 메타데이터와 애플리케이션 로직의 분리이며, IoC와 DI는 이를 가능하게 하는 기반 기술입니다. 따라서 컨테이너가 빈을 관리하는 방식을 이해하는 것은 Spring 프레임워크를 효과적으로 활용하는 데 필수적입니다.
1단계: 컴포넌트 스캔을 통한 빈 발견
어노테이션 기반 설정에서 Spring은 모든 빈을 XML이나 Java @Bean 메소드로 명시적으로 정의하는 대신, 특정 어노테이션이 붙은 클래스를 자동으로 찾아 빈으로 등록하는 메커니즘을 제공합니다. 이것이 바로 컴포넌트 스캔(Component Scan)입니다.
@ComponentScan 어노테이션
- 목적: @ComponentScan은 주로 @Configuration 어노테이션이 붙은 클래스에 사용하여 컴포넌트 스캔을 활성화하고 설정하는 역할을 합니다. 이 어노테이션을 통해 Spring 컨테이너는 지정된 패키지 내에서 빈으로 관리할 후보 클래스들을 찾습니다.
- 기본 동작: @ComponentScan에 아무런 인수를 지정하지 않으면, 해당 어노테이션이 선언된 @Configuration 클래스가 위치한 패키지와 그 하위 패키지 전체를 재귀적으로 스캔합니다. 이러한 기본 동작은 프로젝트의 구성을 논리적으로 구조화하도록 유도합니다. 예를 들어, 애플리케이션의 메인 설정 클래스가 com.example.app 패키지에 있다면, Spring은 com.example.app 및 그 아래 모든 패키지에서 컴포넌트를 찾습니다.
- 설정 속성:
- basePackages / value: 스캔할 기본 패키지를 문자열 배열로 명시적으로 지정합니다. value는 basePackages의 별칭(alias)입니다. 예: @ComponentScan(basePackages = {"com.example.service", "com.example.repository"}).
- basePackageClasses: 문자열 기반 패키지 이름 대신, 타입-안전(type-safe)한 방식으로 스캔할 패키지를 지정합니다. 여기에 명시된 클래스가 속한 패키지를 스캔 대상으로 삼습니다. 예: @ComponentScan(basePackageClasses = {UserService.class, ProductRepository.class}). 이는 리팩토링 시 패키지 이름 변경에 더 안전한 대안이 될 수 있습니다.
- includeFilters / excludeFilters: 기본 스캔 대상 외에 추가적으로 포함하거나 제외할 타입을 지정하는 필터 규칙을 정의할 수 있습니다. 필터 타입으로는 어노테이션, 상속 가능한 타입(assignable type), AspectJ 표현식, 정규 표현식 등이 사용될 수 있어 매우 유연한 제어가 가능합니다. 예를 들어, 특정 커스텀 어노테이션이 붙은 클래스만 포함하거나, 특정 인터페이스를 구현하는 클래스를 제외할 수 있습니다.
- useDefaultFilters: 기본 필터(스테레오타입 어노테이션 대상)의 사용 여부를 제어합니다. 기본값은 true이며, 이 경우 @Component, @Repository, @Service, @Controller 등의 어노테이션이 붙은 클래스를 자동으로 감지합니다. false로 설정하면 기본 필터를 사용하지 않고 includeFilters에 명시된 규칙만 적용됩니다.
- basePackages / value: 스캔할 기본 패키지를 문자열 배열로 명시적으로 지정합니다. value는 basePackages의 별칭(alias)입니다. 예: @ComponentScan(basePackages = {"com.example.service", "com.example.repository"}).
- 반복 가능한 어노테이션: Java 8 및 Spring 4.3부터 @ComponentScan은 반복 가능한(repeatable) 어노테이션이 되었습니다. 따라서 동일한 설정 클래스에 여러 개의 @ComponentScan을 선언할 수 있으며, 이는 내부적으로 @ComponentScans 컨테이너 어노테이션으로 처리됩니다.
후보 컴포넌트 식별
- 스테레오타입 어노테이션: useDefaultFilters=true (기본값)일 때, Spring은 지정된 패키지 내에서 @Component 어노테이션이나 @Component를 메타 어노테이션으로 가지는 어노테이션이 붙은 클래스를 찾습니다. 대표적인 스테레오타입 어노테이션은 다음과 같습니다:
- @Repository: 데이터 접근 계층(DAO)의 빈을 나타냅니다. 데이터 접근 관련 예외를 Spring의 DataAccessException으로 변환해주는 기능도 포함합니다.
- @Service: 서비스 계층(비즈니스 로직)의 빈을 나타냅니다.
- @Controller / @RestController: 프레젠테이션 계층(웹 컨트롤러)의 빈을 나타냅니다. @RestController는 @Controller와 @ResponseBody를 합친 것입니다.
- 커스텀 어노테이션: @Component를 메타 어노테이션으로 사용하여 특정 목적을 가지는 커스텀 스테레오타입 어노테이션을 만들 수 있습니다. 예를 들어, @UseCase 어노테이션을 만들고 @Component를 붙이면, @UseCase가 붙은 클래스도 컴포넌트 스캔 대상이 됩니다.
스캔 프로세스
Spring은 설정된 패키지 내의 클래스패스(classpath)를 스캔하여 .class 파일을 찾습니다. 그런 다음 리플렉션(reflection)이나 더 효율적인 ASM 라이브러리를 사용하여 클래스 메타데이터(특히 어노테이션 정보)를 읽어 후보 컴포넌트인지 확인합니다.
Spring 6.2 특이사항 및 고려사항
- 더 엄격해진 규칙: Spring Framework 6.2부터 컴포넌트 스캔은 BeanFactory 초기화 과정에서 비교적 일찍 수행됩니다. 따라서, 빈 팩토리의 후반 단계에서 평가되는 조건(예: 다른 빈의 존재 여부에 따라 결정되는 Spring Boot의 @ConditionalOnBean)을 사용하여 @ComponentScan이 포함된 @Configuration 클래스의 활성화 여부를 제어하려고 하면 컨텍스트 로딩이 실패할 수 있습니다. 이는 스캔이 필요한 시점에는 해당 조건을 안정적으로 평가할 수 없기 때문입니다. 이 변화는 애플리케이션 컨텍스트 생명주기 단계에 대한 더 엄격한 적용을 반영하며, 예측 가능하고 오류 발생 가능성이 적은 부트스트랩 과정을 지향함을 시사합니다. 개발자는 스캔 활성화/비활성화를 위한 조건부 설계를 할 때, 컨텍스트 시작 초기에 사용 가능한 정보(예: 환경 속성, 클래스 존재 여부)에 기반한 조건을 사용해야 합니다.
컴포넌트 스캔은 명시적인 빈 설정을 크게 줄여주며, 어떤 애플리케이션 구성 요소가 Spring에 의해 관리될지를 결정하는 핵심 메커니즘입니다. basePackages나 필터와 같은 설정 옵션을 이해하는 것은 애플리케이션의 빈 관리를 제어하는 데 중요합니다.
2단계: 빈 정의(Bean Definition 생성)
컴포넌트 스캔을 통해 후보 클래스를 찾는 것은 첫 단계일 뿐입니다. Spring은 즉시 해당 클래스의 인스턴스를 생성하지 않고, 대신 각 후보 컴포넌트에 대한 BeanDefinition 객체를 생성합니다.
Bean Definition
- 목적: BeanDefinition은 빈 인스턴스를 생성하는 데 필요한 모든 메타데이터를 담고 있는 일종의 '레시피' 또는 '청사진'입니다. 이는 빈이 실제로 인스턴스화되기 전에 컨테이너 내부에 존재하는 빈의 표현입니다. BeanDefinition은 빈 생성의 구체적인 방법을 추상화하여 컨테이너가 빈의 생명주기를 일관되게 관리할 수 있도록 합니다.
- 주요 저장 메타데이터: 어노테이션으로부터 추출되어 BeanDefinition에 저장되는 주요 정보는 다음과 같습니다:
- 빈 클래스 이름: 인스턴스화될 클래스의 정규화된 이름(fully qualified name).
- 빈 이름: 클래스 이름을 기반으로 생성되거나(단순 클래스 이름의 첫 글자를 소문자로 변경) @Component("myBeanName")과 같이 명시적으로 지정된 이름. BeanNameGenerator 인터페이스 구현체를 통해 이름 생성 전략을 커스터마이징할 수도 있습니다.
- 스코프(Scope): 빈 인스턴스의 생명주기와 공유 범위를 결정합니다 (예: singleton, prototype, request, session). 기본값은 singleton입니다. @Scope 어노테이션을 통해 지정되며, ScopeMetadataResolver에 의해 해석됩니다.
- 생성자 인수 / 속성 값: 의존성 주입에 필요한 정보 (실제 의존성 해결은 나중에 수행됨).
- 지연 초기화(Lazy Initialization): 빈을 컨테이너 시작 시 즉시 생성할지(false, 기본값), 아니면 처음 요청될 때 생성할지(true) 여부. @Lazy 어노테이션으로 제어됩니다.
- Primary 상태: 동일 타입의 여러 빈 후보 중 자동 와이어링(autowiring) 시 우선적으로 선택될지 여부. @Primary 어노테이션으로 지정됩니다.
- Fallback 상태 (6.2 신규): 동일 타입의 다른 빈(Primary 또는 일반 빈)이 없을 경우에만 사용될 후보임을 나타냅니다. @Fallback 어노테이션으로 지정됩니다. 이는 기본 구현을 제공하되 사용자가 특정 구현을 제공하면 그것을 우선시하는 시나리오를 단순화합니다. 이전에는 사용자가 자신의 빈에 @Primary를 붙여 기본 구현을 덮어써야 했지만, @Fallback은 기본 구현 자체에 '후보'임을 명시적으로 표시하여 의도를 더 명확하게 합니다.
- Depends On: 해당 빈이 초기화되기 전에 먼저 초기화되어야 하는 다른 빈들의 이름. @DependsOn 어노테이션으로 지정됩니다.
- 초기화/소멸 메소드: 빈 인스턴스화 후 또는 소멸 전에 호출될 메소드. @PostConstruct, @PreDestroy (JSR-250) 어노테이션 등으로 지정됩니다.
등록
생성된 BeanDefinition 객체들은 컨테이너의 BeanDefinitionRegistry (주로 ApplicationContext 자체가 구현)에 등록됩니다. 이 레지스트리는 컨테이너가 관리하는 모든 빈의 정의를 보관하는 중앙 저장소 역할을 합니다.
BeanDefinition 단계는 컴포넌트 발견과 실제 생성 사이의 중요한 중간 과정입니다. 이를 통해 컨테이너는 전체 설정을 검증하고, 빈 간의 의존성을 파악하며, 인스턴스를 만들기 전에 복잡한 생명주기를 관리할 준비를 할 수 있습니다.
3단계: 빈 인스턴스화
BeanDefinition이 등록된 후, 컨테이너는 이 정보를 바탕으로 실제 빈 객체 인스턴스를 생성하는 단계로 진행합니다.
인스턴스화 트리거
- 기본 동작 (Singleton): 기본 스코프인 싱글톤(singleton) 빈은 일반적으로 ApplicationContext가 리프레시되거나 시작될 때 미리 인스턴스화됩니다(pre-instantiation). 이는 애플리케이션 시작 시 필요한 빈들을 준비시켜 런타임 성능을 확보하기 위함입니다.
- 지연 초기화 (Lazy): @Lazy 어노테이션이 붙은 싱글톤 빈은 컨테이너 시작 시 생성되지 않고, 해당 빈이 처음으로 요청될 때 인스턴스화됩니다.
- 프로토타입 (Prototype): 프로토타입 스코프의 빈은 컨테이너 시작 시 생성되지 않으며, 요청될 때마다 새로운 인스턴스가 생성됩니다.
인스턴스화 프로세스
- BeanDefinition 조회: 컨테이너는 생성할 빈에 해당하는 BeanDefinition을 레지스트리에서 조회합니다.
- 생성자 선택: 의존성을 생성자를 통해 주입해야 하는 경우, Spring은 사용할 생성자를 결정해야 합니다.
- 단일 생성자: 클래스에 생성자가 하나만 정의되어 있다면, Spring은 그 생성자를 사용합니다.
- @Autowired 명시: 여러 생성자가 있고 그중 하나에 @Autowired 어노테이션이 명시되어 있다면, Spring은 해당 생성자를 사용합니다.
- 다중 생성자 (암시적 선택 주의): 여러 생성자가 있는데 @Autowired가 명시되지 않은 경우, 과거 버전의 Spring이나 특정 상황에서는 의존성을 가장 많이 만족시킬 수 있는 '가장 탐욕스러운(greediest)' 생성자를 찾으려는 시도를 할 수 있었습니다. 하지만 이 동작에 의존하는 것은 권장되지 않습니다. 명확성을 위해, 특히 여러 생성자가 있는 경우에는 사용할 생성자에 @Autowired를 명시하는 것이 좋습니다. Spring 6.x 버전부터는 일반적으로 의존성 주입을 위한 단일 생성자를 권장합니다.
- 파라미터 이름 해결: Spring 6.x부터는 기본적으로 바이트코드를 파싱하여 생성자 파라미터 이름을 추론하지 않습니다. 만약 파라미터 이름을 기반으로 한 생성자 주입(예: @Qualifier 없이 이름으로 매칭)을 사용하려면, 컴파일 시 Java 컴파일러 옵션 -parameters 플래그를 반드시 추가해야 합니다. 이 플래그를 사용하면 파라미터 이름이 바이트코드에 저장되어 런타임에 리플렉션을 통해 접근할 수 있습니다. 이는 잠재적으로 불안정할 수 있는 바이트코드 분석 대신 더 명시적인 설정(어노테이션 또는 빌드 구성)을 선호하는 경향을 반영합니다.
- 팩토리 메소드 사용: BeanDefinition에 생성자 대신 정적(static) 또는 인스턴스 팩토리 메소드가 지정되어 있다면, 컨테이너는 해당 팩토리 메소드를 호출하여 빈 인스턴스를 얻습니다.
- 객체 생성: 선택된 생성자나 팩토리 메소드가 리플렉션(reflection)을 통해 호출되어 실제 new 연산이 실행되거나 팩토리 메소드가 실행됩니다.
초기 상태
이 시점에서 '원시(raw)' 빈 인스턴스가 메모리에 생성됩니다. 하지만 생성자 주입을 사용하지 않았다면, 아직 세터(setter)나 필드(field)를 통한 의존성 주입은 이루어지지 않았으며, @PostConstruct와 같은 커스텀 초기화 메소드도 호출되지 않은 상태입니다.
인스턴스화는 BeanDefinition이라는 청사진이 실제 객체로 구체화되는 단계입니다. 컨테이너가 올바른 생성자나 팩토리 메소드를 선택하는 로직은, 특히 여러 선택지가 있을 때, 정확한 빈 생성을 위해 매우 중요합니다.
4단계: 의존성 주입 (속성 채우기)
원시 빈 인스턴스가 생성된 후, 컨테이너는 세터 주입이나 필드 주입 방식을 사용하는 경우 해당 빈의 의존성을 주입(populate)하는 과정을 진행합니다.
DI 전략 및 Spring 권장 사항
- 생성자 주입 (Constructor Injection): 의존성이 생성자의 인수로 제공되며, 빈 인스턴스화 시점에 주입이 완료됩니다 (3단계에서 처리됨). Spring 팀에서 일반적으로 권장하는 방식입니다. 그 이유는 다음과 같습니다:
- 필수 의존성 강제: 객체 생성 시점에 필수 의존성이 반드시 제공되어야 하므로, null 상태의 의존성을 가질 가능성이 줄어듭니다.
- 불변성(Immutability) 확보: final 키워드를 사용하여 필드를 선언하고 생성자에서 초기화함으로써 불변 객체를 만들 수 있습니다.
- 완전한 초기화 상태: 생성자 주입을 사용하는 컴포넌트는 클라이언트 코드에 반환될 때 항상 완전히 초기화된 상태임을 보장합니다.
- 세터 주입 (Setter Injection): 빈 인스턴스가 생성된 후, 컨테이너가 세터(setter) 메소드를 호출하여 의존성을 주입합니다. 이 방식은 선택적(optional) 의존성에 주로 사용되며, 해당 의존성이 주입되지 않더라도 클래스 내에서 합리적인 기본값을 할당할 수 있는 경우에 적합합니다. 만약 주입된 의존성이 null일 가능성이 있다면, 해당 의존성을 사용하는 모든 코드 위치에서 null 검사를 수행해야 합니다 (단, Spring은 빈 초기화 완료 전에 세터 주입을 수행하므로 일반적으로 사용 시점에는 주입이 완료된 상태입니다).
- 필드 주입 (Field Injection): 리플렉션을 사용하여 필드(private 포함)에 직접 의존성을 주입합니다. 일반적으로 권장되지 않는 방식입니다. 그 이유는 다음과 같습니다:
- 테스트 어려움: 단위 테스트 시 목(mock) 객체를 주입하기 위해 리플렉션을 사용해야 하므로 테스트 코드 작성이 번거로워집니다.
- 의존성 숨김: 의존성이 생성자나 세터 메소드 시그니처에 드러나지 않아 클래스가 어떤 의존성을 필요로 하는지 파악하기 어렵습니다.
- 불변성 위반 가능성: final 필드에 주입할 수 없습니다.
어노테이션 기반 주입 메커니즘
어노테이션은 의존성 주입 프로세스를 트리거하며, 이 작업은 주로 BeanPostProcessor(6단계에서 설명)에 의해 처리됩니다.
- @Autowired (Spring 전용):
- 메커니즘: 생성자, 필드, 세터 메소드, 또는 설정 메소드(@Bean 메소드의 파라미터 등)에 표시하여 Spring DI 컨테이너에 의한 자동 와이어링(autowiring) 대상으로 지정합니다.
- 해결 전략: 주로 타입(Type)을 기준으로 의존성을 찾습니다.
- 단일 후보: 해당 타입의 빈이 컨테이너에 하나만 존재하면 그 빈이 주입됩니다.
- 다중 후보: 동일 타입의 빈이 여러 개 존재하면 추가적인 기준을 사용하여 후보를 좁힙니다:
- @Qualifier: 빈 이름이나 커스텀 한정자(qualifier) 값을 사용하여 특정 빈을 지정합니다. 예: @Qualifier("specificBeanName").
- @Primary: 여러 후보 중 @Primary 어노테이션이 붙은 빈이 우선적으로 선택됩니다.
- Spring 6.2 개선 사항: 여러 후보 중 모호성을 해결할 때, 파라미터 이름 매칭과 @Qualifier 매칭이 @jakarta.annotation.Priority 랭킹보다 우선적으로 고려됩니다. (이전 버전에서는 @Priority가 먼저 체크될 수 있었습니다). @Primary는 여전히 가장 높은 우선순위를 가집니다. 이 변경은 단일 후보를 선택할 때, @Priority의 일반적인 순위 메커니즘보다는 더 명시적이고 직접적인 한정자(Qualifier)나 이름 매칭을 선호하는 설계 의도를 반영합니다. @Priority는 주로 컬렉션에 주입되는 여러 빈의 순서를 정하는 데 더 적합합니다.
- Spring 6.2 깊어진 제네릭 매칭: Spring 6.2는 더 깊어진 제네릭 타입 매칭 기능을 제공합니다. 이전 버전에서는 관대하게 매칭되던 제네릭 시그니처가 6.2에서는 더 엄격하게 검사되어 매칭되지 않을 수 있습니다. 이는 주입 지점(예: 생성자 인수)과 빈 정의(예: @Bean 메소드의 반환 타입) 간의 제네릭 시그니처가 더 정확하게 일치해야 함을 의미하며, 타입 안정성을 강화하려는 움직임으로 볼 수 있습니다.
- 필수 여부: 매칭되는 빈이 없으면 기본적으로 예외가 발생합니다. @Autowired(required=false)로 설정하면 해당 의존성을 선택적으로 만들 수 있으며, 이 경우 매칭되는 빈이 없으면 null (또는 컬렉션/맵의 경우 빈 인스턴스)이 주입됩니다.
- @Resource (Jakarta EE / JSR-250):
- 메커니즘: 의존성 주입을 위한 표준 Java 어노테이션입니다. 필드나 세터 메소드에 적용될 수 있습니다. Spring 6.x 및 Jakarta EE 9+ 환경에서는 jakarta.annotation.Resource를 사용합니다.
- 해결 전략: 주로 이름(Name)을 기준으로 의존성을 찾습니다.
- 이름 우선: 먼저 필드 이름이나 세터 메소드의 프로퍼티 이름과 일치하는 빈 이름을 찾습니다.
- 타입 폴백: 이름으로 매칭되는 빈을 찾지 못하면, 타입을 기준으로 다시 매칭을 시도합니다 (이때 @Autowired와 유사하게 동작).
- 명시적 이름 지정: @Resource(name="specificBeanName") 속성을 사용하여 주입할 빈의 이름을 명시적으로 지정할 수 있습니다.
- 메커니즘: 의존성 주입을 위한 표준 Java 어노테이션입니다. 필드나 세터 메소드에 적용될 수 있습니다. Spring 6.x 및 Jakarta EE 9+ 환경에서는 jakarta.annotation.Resource를 사용합니다.
- @Inject (Jakarta DI / JSR-330):
- 메커니즘: 의존성 주입을 위한 또 다른 표준 Java 어노테이션으로, Jakarta Dependency Injection 사양(구 JSR-330)의 일부입니다. @Autowired와 매우 유사하게 동작합니다. Spring 6.x 및 Jakarta EE 9+ 환경에서는 jakarta.inject.Inject를 사용합니다.
- 해결 전략: 주로 타입(Type)을 기준으로 의존성을 찾습니다.
- 모호성 해결: 동일 타입의 빈이 여러 개 있을 경우, JSR-330 표준의 @Qualifier (특히 @Named 어노테이션)나 프로바이더(Spring) 고유의 메커니즘(예: Spring의 @Primary, @Qualifier)을 사용하여 해결합니다.
- 메커니즘: 의존성 주입을 위한 또 다른 표준 Java 어노테이션으로, Jakarta Dependency Injection 사양(구 JSR-330)의 일부입니다. @Autowired와 매우 유사하게 동작합니다. Spring 6.x 및 Jakarta EE 9+ 환경에서는 jakarta.inject.Inject를 사용합니다.
의존성 해결 프로세스
컨테이너는 주입 지점(생성자 파라미터, 세터 메소드 파라미터, 필드)에서 요구하는 타입 및/또는 이름(그리고 한정자)과 일치하는 빈을 찾기 위해 자신의 BeanDefinition 레지스트리를 검색합니다. 적절한 빈을 찾으면 해당 빈의 인스턴스(또는 인스턴스에 대한 참조)를 주입 지점에 전달합니다.
어떤 DI 전략(생성자, 세터, 필드)과 어노테이션(@Autowired, @Resource, @Inject)을 선택하는지는 코드의 명확성, 테스트 용이성, 디자인 원칙 준수에 영향을 미칩니다. 필수 의존성에 대해 생성자 주입을 권장하는 Spring의 가이드라인은 강력한 지침이 됩니다.
DI 어노테이션 비교
특징 | @Autowired (Spring) | @Resource (Jakarta EE / JSR-250) | @Inject (Jakarta DI / JSR-330) |
표준 | Spring 프레임워크 고유 | Jakarta EE 표준 (JSR-250) | Jakarta DI 표준 (JSR-330) |
주요 해결 방식 | 타입 (Type) | 이름 (Name) | 타입 (Type) |
폴백 해결 방식 | 이름 (파라미터/필드 이름), @Qualifier, @Primary 등 | 타입 (Type) | @Named (표준 Qualifier), 프로바이더별 메커니즘 (Spring의 @Qualifier, @Primary 등) |
필수 여부 처리 | required=false 속성 | (표준에는 없음, Spring은 @Autowired와 유사하게 처리) | (표준에는 없음, 프로바이더별 처리) |
한정자 (Qualifier) | @Qualifier | name="..." 속성 | @Named, 프로바이더별 @Qualifier 등 |
이 표는 개발자가 프로젝트의 요구사항과 표준 준수 여부에 따라 적절한 DI 어노테이션을 선택하는 데 도움을 줄 수 있습니다. @Autowired는 Spring 생태계 내에서 가장 널리 사용되지만, 표준 어노테이션인 @Resource와 @Inject를 이해하는 것은 Jakarta EE 환경과의 상호 운용성 및 표준 준수를 위해 중요합니다. 특히 이름 기반 매칭이 필요할 때는 @Resource가 유용할 수 있습니다.
5단계: 빈 후처리기 (Bean Post-Processing)
빈 인스턴스화 및 기본적인 의존성 주입 이후, Spring은 빈의 생명주기에 개입하여 추가적인 처리를 수행할 수 있는 강력한 확장 메커니즘을 제공합니다. 이것이 바로 BeanPostProcessor 인터페이스입니다.
빈 생명주기 확장
BeanPostProcessor 인터페이스는 컨테이너의 빈 생명주기에 사용자 정의 로직을 연결(plug in)할 수 있게 해주는 콜백 인터페이스입니다. 이 후처리기들은 빈 인스턴스가 생성된 후에, 그리고 커스텀 초기화 메소드(@PostConstruct 등)가 호출되기 전 (postProcessBeforeInitialization 메소드)과 후 (postProcessAfterInitialization 메소드)에 실행될 기회를 가집니다.
동작 방식
- 등록: BeanPostProcessor 자체도 Spring 컨테이너에 의해 관리되는 빈입니다. 컨테이너는 시작 시점에 자신의 설정 내에 정의된 모든 BeanPostProcessor 빈들을 감지하고 특별히 등록합니다.
- 호출: 컨테이너가 일반 빈을 생성하는 과정에서, 등록된 모든 BeanPostProcessor들이 순서대로 해당 빈 인스턴스에 대해 호출됩니다.
- postProcessBeforeInitialization(Object bean, String beanName): 빈의 초기화 콜백(예: @PostConstruct 메소드, InitializingBean의 afterPropertiesSet)이 호출되기 직전에 실행됩니다. 원본 빈 인스턴스를 반환하거나, 필요하다면 래핑(wrapping)된 인스턴스(예: 프록시)를 반환할 수 있습니다.
- postProcessAfterInitialization(Object bean, String beanName): 빈의 초기화 콜백이 호출된 직후에 실행됩니다. 마찬가지로 원본 또는 래핑된 인스턴스를 반환할 수 있습니다.
어노테이션 처리를 위한 주요 구현체
앞서 설명한 @Autowired, @Resource, @PostConstruct, @PreDestroy 등의 어노테이션 기반 기능들은 실제로는 특정 BeanPostProcessor 구현체들에 의해 처리됩니다.
- AutowiredAnnotationBeanPostProcessor: @Autowired와 @Value 어노테이션 (그리고 JSR-330의 @Inject도 처리 가능)을 감지하고 처리하는 핵심 후처리기입니다. 이 프로세서는 해당 어노테이션이 붙은 필드나 메소드를 찾아 리플렉션을 사용하여 의존성 조회 및 주입 로직을 수행합니다.
- CommonAnnotationBeanPostProcessor: JSR-250 표준 어노테이션인 @Resource, @PostConstruct, @PreDestroy를 처리합니다. @Resource 어노테이션에 대한 의존성 주입을 수행하고, @PostConstruct와 @PreDestroy로 지정된 생명주기 콜백 메소드를 적절한 시점에 호출하는 역할을 담당합니다.
실행 순서
여러 BeanPostProcessor가 등록된 경우, 실행 순서가 중요할 수 있습니다. Spring은 Ordered 인터페이스를 구현하거나 @Order 어노테이션을 사용하여 후처리기들의 실행 순서를 제어할 수 있도록 지원합니다. 순서 값이 낮을수록 먼저 실행됩니다.
BeanPostProcessor는 Spring의 어노테이션 기반 기능들이 실제로 동작하게 만드는 '마법' 뒤의 일꾼들입니다. 이들은 어노테이션 처리 로직을 핵심 컨테이너 로직과 분리하여 프레임워크의 높은 확장성을 가능하게 합니다. 이들의 역할을 이해하면 @Autowired와 같은 어노테이션이 어떻게 의존성 주입을 실제로 수행하는지 그 내부 메커니즘을 파악할 수 있습니다.
참고: AOP 프록시
빈의 생명주기 동안, 특히 빈 후처리 단계에서, Aspect-Oriented Programming (AOP) 설정에 따라 원본 빈 인스턴스가 프록시(Proxy) 객체로 대체될 수 있습니다.
AOP 통합
Spring AOP는 선언적 트랜잭션 관리(@Transactional), 보안(@Secured 등), 로깅 등과 같은 횡단 관심사(cross-cutting concerns)를 모듈화하는 데 사용됩니다. AOP가 적용되도록 설정된 빈의 경우, Spring은 해당 빈의 메소드 호출을 가로채서 부가 기능(Advice)을 적용하기 위해 원본 빈 객체를 감싸는 프록시 객체를 생성할 필요가 있습니다. Spring AOP는 기본적으로 프록시 패턴 기반으로 동작합니다.
프록시 생성 시점
이러한 프록시 객체 생성은 일반적으로 빈 후처리 단계(5단계) 에서 이루어집니다. AbstractAutoProxyCreator와 같은 특수한 BeanPostProcessor 구현체들이 이 역할을 담당하는 경우가 많습니다. 구체적으로는, 원본 빈 인스턴스가 생성되고 기본적인 의존성 주입이 완료된 후, 초기화 콜백(@PostConstruct 등)이 호출되기 전이나 후에 프록시가 생성될 수 있습니다 (주로 postProcessAfterInitialization 단계에서 발생).
영향
AOP 프록시가 생성되면, 다른 빈에 의존성으로 주입되는 것은 원본 빈 인스턴스가 아니라 이 프록시 객체입니다. 따라서 해당 빈의 메소드를 호출하면 실제로는 프록시 객체를 통해 호출이 이루어지며, 이때 AOP 어드바이스(예: 트랜잭션 시작/커밋/롤백 로직)가 먼저 실행된 후 원본 빈의 메소드가 호출될 수 있습니다.
AOP는 빈 생명주기에 매끄럽게 통합되어, 주입되는 객체 참조가 원본 객체가 아닐 수도 있다는 점을 인지하는 것이 중요합니다. 이는 특히 트랜잭션이나 보안과 같은 AOP 기능이 적용된 빈을 디버깅하거나 동작을 이해할 때 중요한 맥락 정보가 됩니다.
결론: Spring 6.2.5 의 어노테이션 기반 빈 생명주기 요약
Spring Framework 6.2.5에서 어노테이션 기반으로 관리되는 빈은 다음과 같은 단계를 거쳐 생성되고 준비됩니다.
- 스캔 (Scan): @Configuration 클래스에 선언된 @ComponentScan 설정에 따라, 지정된 패키지 내에서 @Component 및 관련 스테레오타입 어노테이션(@Service, @Repository, @Controller 등)이 붙은 클래스를 탐색합니다. (Spring 6.2부터 특정 후행 조건과 함께 @ComponentScan 사용 시 제약 강화 )
- 정의 (Define): 스캔된 각 후보 클래스에 대해, 빈의 생성 및 관리에 필요한 모든 메타데이터(클래스 정보, 스코프, 이름, 의존성 정보, 생명주기 콜백 등)를 포함하는 BeanDefinition 객체를 생성하여 컨테이너의 레지스트리에 등록합니다. (Spring 6.2에 @Fallback 빈 개념 도입 )
- 인스턴스화 (Instantiate): BeanDefinition 정보를 바탕으로, 적절한 생성자나 팩토리 메소드를 호출하여 빈의 '원시' 인스턴스를 생성합니다. (Spring 6.x는 생성자 파라미터 이름 해결을 위해 -parameters 컴파일러 플래그 권장 )
- 속성 채우기 (Populate): BeanPostProcessor (예: AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor)가 동작하여 @Autowired, @Resource, @Inject 등의 어노테이션을 처리하고, 정의된 의존성을 빈 인스턴스에 주입합니다. (Spring 6.2에서 @Autowired 모호성 해결 시 @Qualifier/이름 매칭이 @Priority보다 우선 , 제네릭 타입 매칭 강화 )
- 초기화 (Initialize): BeanPostProcessor (예: CommonAnnotationBeanPostProcessor)가 @PostConstruct와 같은 초기화 어노테이션을 처리하여 지정된 커스텀 초기화 메소드를 호출합니다. InitializingBean 인터페이스 구현 시 afterPropertiesSet 메소드도 이 단계에서 호출됩니다.
- (선택적) 프록시 생성 (Proxy): AOP 설정이 적용된 경우, 다른 BeanPostProcessor (예: AbstractAutoProxyCreator)가 원본 빈 인스턴스를 감싸는 AOP 프록시 객체를 생성할 수 있습니다.
- 준비 완료 (Ready): 모든 초기화 및 후처리 과정이 완료된 빈(원본 또는 프록시)은 이제 애플리케이션에서 사용될 준비가 된 상태이며, 다른 빈에 주입되거나 컨테이너로부터 직접 조회될 수 있습니다.
참고 자료
Spring Framework - Wikipedia
From Wikipedia, the free encyclopedia This article is about the Spring Framework. For the Spring Boot, see Spring Boot. Application framework for Java platform The Spring Framework is an application framework and inversion of control container for the Java
en.wikipedia.org
https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html
Dependency Injection :: Spring Framework
Constructor-based DI is accomplished by the container invoking a constructor with a number of arguments, each representing a dependency. Calling a static factory method with specific arguments to construct the bean is nearly equivalent, and this discussion
docs.spring.io
ComponentScan (Spring Framework 6.2.5 API)
Indicates whether automatic detection of classes annotated with @Component @Repository, @Service, or @Controller should be enabled.
docs.spring.io
https://docs.spring.io/spring-framework/reference/core/beans.html
The IoC Container :: Spring Framework
Preview
docs.spring.io
'Spring' 카테고리의 다른 글
[Spring] Spring Data - MongoDB (0) | 2025.04.18 |
---|---|
[Spring] Spring Data - JPA (0) | 2025.04.18 |
[Spring] Spring Data - JDBC (0) | 2025.04.17 |