안녕하세요. 개발자 stark입니다.
블로그를 시작하며 제가 과거에 공부하며 작성했던 내용들을 전부 리팩터링 하고 있습니다. ㅎㅎㅎㅎ 개발했던 코드도 리팩토링을 하는데 이제는 블로그 글도 이렇게 수정해야 할 필요성을 느낍니다. 자 그럼 이제 스프링 컨테이너에 대해서 알아봅시다!
스프링 컨테이너가 뭔가요?
스프링 컨테이너는 스프링 프레임워크의 핵심 구성 요소로, 애플리케이션의 객체(빈)를 관리하고 조정하는 역할을 합니다. 컨테이너는 스프링의 의존성 주입(Dependency Injection) 원칙을 구현하며, 애플리케이션의 모든 빈을 생성, 관리, 설정, 삭제하는 기능을 제공합니다.
스프링에서 사용하는 주요 컨테이너의 종류는 2가지가 있습니다.
- BeanFactory: 가장 기본적인 형태의 스프링 컨테이너로서, 빈의 생성, 설정, 관리 등의 역할을 담당합니다. BeanFactory는 빈의 생명주기를 관리하고, 의존성 주입(DI)을 지원하며, 빈 간의 관계를 설정하는 기능을 제공합니다.
- ApplicationContext: BeanFactory의 확장된 형태로, BeanFactory의 모든 기능을 포함(빈의 생성, 설정 관리)하고 추가적으로 메시지 소스 처리(국제화 지원), 이벤트 발행, 애플리케이션 계층 통합 지원 등 엔터프라이즈 전반에 걸친 기능을 제공합니다.
스프링 컨테이너의 주요 기능을 알아봅시다.
1. 빈 관리
- 스프링 컨테이너는 설정 파일이나 어노테이션을 통해 빈을 생성하고 의존성을 주입하며, 생명주기를 관리합니다.
- 생성: 설정 정보를 바탕으로 객체를 생성합니다.
- 의존성 주입(DI): 빈 간의 의존 관계를 주입합니다.
- 소멸 관리: 빈 사용 종료 시 적절한 자원 해제를 지원합니다.
2. 의존성 주입 (DI) : 매우 중요!!!
- 컨테이너는 객체 간의 의존성을 명시적으로 설정하여 결합도를 낮추고 테스트 용이성을 높입니다.
- 의존성 주입 방법
- 생성자 주입: 객체 생성 시 의존성을 주입.
- 세터 주입: 생성 후 세터 메서드를 통해 주입.
- 필드 주입: 필드에 직접 주입 (@Autowired 사용).
@Component
public class MyService {
public void execute() {
System.out.println("Service executed!");
}
}
@Component
public class MyController {
private final MyService myService;
@Autowired
public MyController(MyService myService) {
this.myService = myService;
}
}
3. 설정 정보 관리
- 스프링 컨테이너는 다양한 설정 형식을 지원합니다.
- XML
- Java 기반 설정 (@Configuration, @Bean)
- 어노테이션 기반 설정 (@ComponentScan 등).
주로 사용되는 ApplicationContext의 종류를 알아봅시다.
1. AnnotationConfigApplicationContext
- Java 기반 설정(@Configuration)을 읽어 들여 빈을 관리하는 데 사용됩니다. 이 방식은 엄청 많이 사용해 보셨을 거라 생각됩니다. Kafka, Feign, Rest, Redis 등의 설정을 하는 코드는 주로 이런 식으로 설정 코드를 개발자가 커스텀해서 빈으로 등록하곤 합니다.
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
2. ClassPathXmlApplicationContext
- 클래스패스에서 XML 설정 파일을 읽어 빈을 관리합니다. 이건 과거에 주로 사용했었던 일반적인 SpringFramework에서 사용하는 방식입니다. 요즘에는 SpringBoot를 사용하지만 예전에는 XML + SpringFramework로 이렇게 XML 코드로 빈 등록 코드를 작성하곤 했습니다.
<bean id="myService" class="com.example.MyService"/>
3. WebApplicationContext
- 웹 애플리케이션 전용 컨텍스트로, 서블릿과 통합됩니다.
4. FileSystemXmlApplicationContext
- 파일 시스템 경로에서 XML 설정 파일을 읽는 데 사용되는 ApplicationContext입니다.
ApplicationContext을 사용하는 이점은 어떤 것이 있을까요?
1. 빈 관리
- ApplicationContext는 빈 팩토리의 역할을 하며, 빈의 생성과 관리, 라이프사이클, 설정 등을 담당해 준다.
2. 메시지 소스 처리
- 국제화(i18n) 지원을 위해 메시지 소스 처리 기능을 제공한다.
3. 이벤트 퍼블리케이션
- 애플리케이션 이벤트를 발행하고 처리하는 메서드를 제공한다.
4. 다양한 뷰 레이어 기술 지원
- JSP, Thymeleaf, FreeMarker 등 다양한 뷰 레이어 기술에 대한 통합 지원을 제공한다.
스프링 컨테이너의 주요 기능 (빈 생명주기 관리)
빈 생성 및 초기화를 담당합니다.
- 설정 정보(@Configuration, @Bean)를 읽음.
- 빈 정의를 분석하고 객체를 생성.
- 의존성 주입(@Autowired, @Qualifier).
- 초기화 메서드(@PostConstruct).
빈 소멸도 담당합니다.
- 애플리케이션 종료 시 소멸 메서드(@PreDestroy) 호출.
- 자원 해제 및 정리.
즉, 빈의 생명주기 전체를 관리한다고 볼 수 있습니다.
- 스프링 컨테이너는 빈의 생명주기를 전담하여 개발자가 객체 생성, 관리에 신경 쓰지 않도록 합니다.
스프링의 빈 등록 예시
1. @Component 어노테이션을 클래스 상단에 적어줘서 이 클래스를 스프링 빈으로 등록시킬 수 있습니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MyComponent {
private final MyService myService;
@Autowired
public MyComponent(MyService myService) {
this.myService = myService;
myService.doSomething(); // Output: Doing something!
}
}
2. @Configuration과 @Bean을 사용해서 빈을 등록하는 예시
- @Configuration 어노테이션은 해당 클래스가 스프링의 설정 클래스임을 나타냅니다. @Bean 어노테이션은 해당 메서드가 빈을 생성하고 초기화하는 역할을 하며, 스프링 컨테이너에 의해 관리됩니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfiguration {
@Bean
public MyService myService() {
return new MyService();
}
}
class MyService {
public void doSomething() {
System.out.println("Doing something!");
}
}
이 코드의 동작 원리를 단계별로 살펴보겠습니다.
- 먼저 @Configuration 어노테이션을 확인해 봅시다. 내부에 @Component가 선언되어 있고 필드에는 proxyBeanMethods()라는 boolean이 선언되어 있는 것을 확인할 수 있습니다. (핵심!)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";
boolean proxyBeanMethods() default true;
boolean enforceUniqueMethods() default true;
}
@Configuration의 역할
- @Configuration 어노테이션은 내부적으로 @Component를 포함하고 있어서 스프링의 컴포넌트 스캔 대상이 됩니다. 그러나 일반적인 @Component와는 다르게, CGLIB를 사용한 프록시 객체를 생성합니다. 이 프록시는 빈의 싱글톤 스코프를 보장하는 데 핵심적인 역할을 합니다.
@Bean의 역할
- @Bean 어노테이션이 붙은 메서드는 스프링 컨테이너에 빈을 등록하는 팩토리 메서드로 작동합니다. 메서드의 반환 타입이 빈의 타입이 되고, 메서드 이름이 빈의 이름이 됩니다. 물론 @Bean(name = "customName")과 같이 이름을 직접 지정할 수도 있습니다.
그럼 @Configuration과 @Bean는 어떤 관계를 가지는 걸까요? 먼저 스프링의 핵심 목적을 이해해야 하는데 스프링은 객체들을 싱글톤으로 관리하고자 합니다. 한 번 만든 객체를 계속 재사용하면 메모리도 아끼고 성능도 좋아지니까요.
자, 이제 다음 코드를 봅시다.
@Configuration
public class OrderConfiguration {
@Bean
public PaymentService paymentService() {
return new PaymentService();
}
@Bean
public OrderService orderService() {
// 여기서 paymentService를 가져다 쓰려면?
PaymentService payment = paymentService(); // 메서드를 직접 호출합니다
return new OrderService(payment);
}
}
이 코드에서 orderService가 paymentService를 필요로 한다고 생각해 봅시다. 가장 자연스러운 방법은 paymentService() 메서드를 직접 호출하는 것입니다. 그런데 여기서 문제가 생깁니다. 자바의 일반적인 메서드 호출이라면, paymentService()를 호출할 때마다 새로운 PaymentService 객체가 생성될 것입니다. 이러면 싱글톤이 깨지게 되는 것입니다.
이 문제를 해결하기 위해 스프링은 @Configuration이 붙은 클래스에 특별한 마법을 부립니다. 바로 CGLIB라는 기술을 사용해서 프록시를 만드는 것입니다. 이 프록시는 다음과 같은 역할을 합니다.
// 스프링이 만드는 프록시가 내부적으로 동작하는 방식을 의사 코드로 표현하면:
public class OrderConfigurationProxy extends OrderConfiguration {
private PaymentService paymentServiceSingleton; // 싱글톤 객체를 보관할 변수
@Override
public PaymentService paymentService() {
if (paymentServiceSingleton == null) { // 아직 만들어진 적 없다면
paymentServiceSingleton = super.paymentService(); // 진짜로 한 번만 만듦
}
return paymentServiceSingleton; // 이미 있으면 그걸 재사용
}
}
즉, @Bean이 붙은 메서드를 호출할 때마다 프록시가 중간에 끼어들어서
- 이미 만들어둔 객체가 있는지 확인하고
- 있으면 그걸 반환하고
- 없으면 새로 만들어서 보관해 두고 반환합니다.
이렇게 하면 orderService에서 paymentService()를 호출하더라도, 항상 같은 PaymentService 객체를 받게 되는 것입니다. 이것이 @Configuration과 @Bean이 함께 동작하는 핵심 원리입니다. @Configuration은 이런 프록시를 만들어주는 설정이고, @Bean은 "이 메서드가 만드는 객체를 싱글톤으로 관리해야 해!"라고 알려주는 표시라고 생각하시면 됩니다.
자 우리는 OrderService도 메서드에서 @Bean으로 등록시켰습니다. 그러니 OrderService도 살펴봅시다.
// 스프링이 실제로 만드는 프록시의 동작 방식을 의사 코드로 표현하면:
public class OrderConfigurationProxy extends OrderConfiguration {
private PaymentService paymentServiceSingleton; // PaymentService 싱글톤 보관
private OrderService orderServiceSingleton; // OrderService 싱글톤 보관
@Override
public PaymentService paymentService() {
if (paymentServiceSingleton == null) {
paymentServiceSingleton = super.paymentService();
}
return paymentServiceSingleton;
}
@Override
public OrderService orderService() {
if (orderServiceSingleton == null) {
orderServiceSingleton = super.orderService(); // 이 때 내부적으로 paymentService()를 호출하더라도
// 위의 프록시 메서드가 호출되므로 싱글톤 보장
}
return orderServiceSingleton;
}
}
여기서 재미있는 점은, OrderService를 만들 때 paymentService()를 호출하더라도 프록시 버전의 paymentService()가 호출된다는 것입니다.
- OrderService가 처음 만들어질 때 PaymentService가 필요하면
- 프록시가 관리하는 PaymentService 싱글톤을 가져와서
- 그걸로 OrderService를 만들고
- 그 OrderService도 싱글톤으로 보관됩니다.
이렇게 @Configuration 안에 있는 모든 @Bean 메서드들이 프록시로 감싸져서, 전부 싱글톤으로 안전하게 관리되는 것입니다. 마치 연쇄적으로 각각의 빈들이 필요할 때 한 번씩만 만들어지고, 그다음부터는 계속 재사용되는 방식입니다.
그럼 만약 @Configuration을 사용하지 않아서 프록시가 없다면 어떻게 될까요?
- myService() 메서드가 호출될 때마다 새로운 객체가 생성됩니다.
- 그래서 빈의 싱글톤 특성이 깨지게 됩니다.
- 메모리 낭비와 일관성 문제가 발생할 수 있습니다.
"@Configuration + @Bean" 방식과 "@Component + @Bean" 방식의 차이점
@Component // 프록시 생성하지 않음
public class MyConfig {
@Bean
public MyService myService() {
return new MyService(); // 매번 새로운 인스턴스 생성 가능
}
}
@Configuration // CGLIB 프록시 생성
public class MyConfigWithProxy {
@Bean
public MyService myService() {
return new MyService(); // 항상 같은 인스턴스 반환
}
}
실제 사용 시 고려사항
- @Configuration 클래스는 보통 애플리케이션의 설정을 모듈화 하는 데 사용됩니다.
- 관련된 빈들을 하나의 설정 클래스에 모아서 관리하면 유지보수가 용이해집니다.
- @Bean 메서드들 간의 의존관계도 메서드 호출만으로 자연스럽게 표현할 수 있습니다.
조금 설명이 길었는데 이렇게 @Configuration과 @Bean의 조합은 스프링의 핵심적인 설정 메커니즘을 제공하며, 특히 프록시를 통한 싱글톤 보장은 스프링 애플리케이션의 안정성과 일관성을 유지하는 데 매우 중요한 역할을 합니다.
Spring의 빈 주입 (IOC + DI) 이해하기
빈 주입 (Dependency Injection)
- 스프링 컨테이너는 빈이 필요로 하는 의존성을 주입한다. 이 의존성 주입은 생성자 주입이나 세터 주입을 통해 이루어진다. 이를 통해 Spring Framework의 핵심 기능 중 하나인 Inversion of Control(IoC)“제어의 역전”과 Dependency Injection(DI)“의존성 주입” 이 이루어진다.
IoC (Inversion of Control)
- IoC는 객체가 자신이 사용할 객체를 직접 만들지 않고(new 사용 X) 외부에서 주입받는 개념이다(스프링 컨테이너를 통해). 이를 통해 코드 간의 결합도를 낮추고 유연성을 높일 수 있다.
DI (Dependency Injection)
- DI는 IoC의 한 형태로, 객체가 필요로 하는 의존성을 외부에서 주입하는 기법이다. 생성자 주입과 세터 주입이 대표적인 방법이다.
예시 코드 - 서비스 인터페이스와 구현체 정의
public interface MyService {
void doSomething();
}
@Component
public class MyServiceImpl implements MyService {
@Override
public void doSomething() {
System.out.println("Doing something!");
}
}
생성자 주입 예시
@Component
public class MyComponentWithConstructorInjection {
private final MyService myService;
@Autowired
public MyComponentWithConstructorInjection(MyService myService) {
this.myService = myService;
}
public void execute() {
myService.doSomething(); // Output: Doing something!
}
}
Setter 주입 예시
@Component
public class MyComponentWithSetterInjection {
private MyService myService;
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
public void execute() {
myService.doSomething(); // Output: Doing something!
}
}
- 위의 예시에서 MyComponentWithConstructorInjection 클래스는 생성자 주입을 사용하고 있고 MyComponentWithSetterInjectrion 클래스는 setter 주입을 사용합니다. 스프링은 생성자 주입을 권장합니다.
- 스프링 컨테이너는 @Autowired 어노테이션을 통해 해당 객체가 필요로 하는 의존성을 자동으로 주입해 줍니다. 이렇게 하면 개발자는 의존성을 직접 관리할 필요 없이 필요한 의존성에 대해서 명시만 하면 됩니다. 이를 통해 각 클래스는 필요한 의존성만 명시하면, 스프링 컨테이너가 해당 객체를 생성하고, 연결하는 작업을 담당합니다. (참 편하죠?)
Spring의 빈 주입 순서와 과정
1. 빈 정의 읽기 (Bean Definition)
- 스프링 컨테이너는 @Component, @Service, @Repository, @Controller 등의 어노테이션이 붙은 클래스와 @Bean 어노테이션이 붙은 메서드를 찾아 빈 정의를 읽는다.
2. 빈 생성
- 스프링 컨테이너는 빈 정의를 기반으로 빈 인스턴스를 생성한다. 이때 생성자와 필드, 세터 메서드에 붙은 @Autowired 어노테이션을 찾아 의존성을 주입할 대상을 파악한다.
3. 의존성 해석
- 스프링 컨테이너는 각 빈이 의존하는 다른 빈을 찾아 의존성 그래프를 만든다. 순환 의존성이 있는 경우 예외가 발생한다.
4. 의존성 주입
- 의존성 그래프를 기반으로 의존성을 주입한다. 주입 순서는 의존성 그래프의 특성에 따라 달라질 수 있으며, 일반적으로 루트 빈부터 리프 빈까지 주입된다.
- 생성자 주입: 생성자가 호출되는 시점에 의존성이 주입된다.
- 세터 주입: 빈이 생성된 후 세터 메서드가 호출되는 시점에 의존성이 주입된다.
- 필드 주입: 빈이 생성된 후 필드에 직접 의존성이 주입된다.
5. 빈 초기화:
- @PostConstruct 어노테이션이 붙은 메서드가 있다면 호출되어 빈이 초기화된다.
6. 빈 사용:
- 이제 응용 프로그램은 완전히 초기화된 빈을 사용할 수 있다.
7. 빈 소멸:
- 응용 프로그램이 종료되거나 컨텍스트가 닫히는 경우, @PreDestroy 어노테이션이 붙은 메서드가 호출되고 빈이 소멸된다.
이렇게 스프링은 개발자 대신에 의존성 주입을 처리하며, 이는 코드의 재사용성, 테스트 용이성 등 여러 가지 장점을 가져다준다. 참고로 Spring 4.3부터는 한 개의 생성자만 있는 경우 @Autowired를 생략해도 스프링이 자동으로 의존성을 주입해 준다. 따라서 위의 예제에서는 생성자(Constructor)에 @Autowired를 생략해도 무방하다.
빈의 범위(Scope)란?
스프링은 빈의 생성 및 공유 방식을 제어하기 위해 다양한 스코프를 제공합니다.
- Singleton: 컨테이너당 1개 인스턴스(기본값).
- Prototype: 요청 시마다 새로운 인스턴스 생성.
- Request/Session: HTTP 요청/세션 당 1개 인스턴스 생성 (웹 전용).
1. Singleton (싱글톤)
- 스프링 빈은 싱글톤이 기본값입니다. 스프링 컨테이너는 Bean을 한 번만 생성하고, 모든 요청에 대해 동일한 인스턴스를 재사용합니다. 즉, 스프링 컨테이너당 Bean 인스턴스(생성된 클래스를 의미) 하나를 갖는다는 의미입니다.
- 이 범위는 상태를 가지지 않는 빈에 적합합니다. 그러니 빈 등록할 클래스는 필드에 상태값을 선언하지 않는 것이 중요합니다. (하나의 싱글톤 객체를 여러 스레드에서 공유해서 사용하기에 여러 스레드에서 동시에 클래스에 선언된 상태값을 변경하려 하면 동시성 문제가 발생합니다.)
@Component
@Scope("singleton") // 생략 가능. 기본값이 singleton이기 때문입니다.
public class SingletonBean {
// ...
}
2. Prototype (프로토타입)
- 이 범위를 설정하면, 스프링 컨테이너는 빈 요청이 있을 때마다 새로운 인스턴스를 생성합니다. 즉, 요청할 때마다 새로운 빈 인스턴스를 반환하게 됩니다. 이 범위는 각 요청이 독립적인 상태를 필요로 하는 빈에 적합합니다.
@Component
@Scope("prototype")
public class PrototypeBean {
// ...
}
- 이 외에도 웹 애플리케이션 컨텍스트에서 사용되는 request, session, application 등의 빈 범위가 있습니다. 이들은 각각 HTTP 요청, HTTP 세션, ServletContext 수명주기에 빈의 생명주기를 연결합니다.
빈의 범위를 올바르게 설정하는 것은 애플리케이션의 성능, 동시성 관리, 메모리 관리 등에 큰 영향을 미칠 수 있으니 빈의 사용 패턴과 상태에 따라 적절한 범위를 선택해야 합니다. 아마 대부분의 개발자들은 스프링을 사용할 때 따로 빈 스코프 범위를 직접 지정하지 않고 기본값인 Singleton 스코프를 사용할 것입니다.
스프링 컨테이너 - AnnotationConfigApplicationContext
스프링 컨테이너와 AnnotationConfigApplicationContext의 이해
- 스프링 프레임워크의 핵심은 '컨테이너'입니다. 이 컨테이너는 애플리케이션의 객체들을 생성하고 관리하는 역할을 합니다. 그중에서도 AnnotationConfigApplicationContext는 자바 어노테이션을 기반으로 설정 정보를 읽어 들이는 특별한 컨테이너입니다.
@Configuration
public class AppConfig {
@Bean
public OrderService orderService() {
return new OrderService();
}
@Bean
public PaymentService paymentService() {
return new PaymentService();
}
}
// 직접 컨테이너를 생성하는 경우의 예시
public class Main {
public static void main(String[] args) {
// AppConfig 클래스를 설정 정보로 사용하여 컨테이너 생성
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
// 컨테이너에서 빈을 가져와 사용
OrderService orderService = context.getBean(OrderService.class);
}
}
컨테이너가 빈을 등록하고 관리하는 과정을 자세히 살펴봅시다.
- 먼저 @Configuration이 붙은 클래스를 찾습니다. 이 클래스들이 설정 정보를 담고 있습니다.
- 그다음 @Component, @Controller, @Service, @Repository 등의 어노테이션이 붙은 클래스들을 찾아서 빈으로 등록합니다.
- @Bean이 붙은 메서드들을 찾아 그 메서드가 반환하는 객체들도 빈으로 등록합니다.
- 이렇게 등록된 빈들의 의존관계를 분석하고 필요한 경우 자동으로 주입해 줍니다.
스프링 부트의 경우에는 이 모든 과정을 자동화해 줍니다. 다음과 같은 코드 한 줄로 모든 설정이 이루어집니다.
- 저희가 프로젝트 생성 시 볼 수 있는 일반적인 main 코드입니다.
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
이 코드 한 줄 속에서 일어나는 일들을 풀어보면
- 스프링 부트가 자동으로 AnnotationConfigApplicationContext를 생성합니다.
- @ComponentScan을 통해 모든 스프링 빈을 찾아서 등록합니다.
- 필요한 설정들을 자동으로 구성합니다.
- 웹 서버를 자동으로 설정하고 실행합니다.
이처럼 스프링 부트는 개발자가 직접 해야 할 많은 설정 작업들을 자동화해 주어, 개발자는 비즈니스 로직에만 집중할 수 있게 됩니다. AnnotationConfigApplicationContext는 이러한 자동화의 핵심에서 동작하는 컨테이너입니다.
이 내용을 이해하셨다면 아래의 빈 등록 과정의 핵심 코드를 같이 확인해보시면 좋을것같습니다.
'Spring > Spring 기초 지식' 카테고리의 다른 글
스프링은 Singleton 패턴을 어떻게 활용할까? (0) | 2023.08.07 |
---|---|
스프링의 제어의 역전 (IoC, Inversion of Control) (0) | 2023.08.07 |
[Spring] 의존성 주입(DI - Dependency Injection)과 결합도 낮추기 (0) | 2023.08.07 |
[스프링, 스프링부트] Spring - @Bean과 @Component (0) | 2023.07.20 |
왜 스프링인가? 프레임워크의 철학 가볍게 살펴보기 (0) | 2023.07.20 |