안녕하세요! 지난 번 의존성 주입에 대한 글을 작성했는데 혹시 보신 분 있으신가요?!
오늘은 이 글에 이어서, 스프링 프레임워크에서는 의존성 주입이 어떻게 일어나는지를 살펴보려고 합니다!
이를 알기 위해선 먼저 Spring Bean
과 Spring Container
의 개념에 대해 알아야 합니다!
스프링 컨테이너란?
스프링 컨테이너는 스프링 프레임워크의 핵심입니다.
어플리케이션 내에서 스프링의 객체인 Spring Bean을 생성하고, 생명주기를 관리합니다.
스프링 컨테이너는 크게 두 가지 형태로 존재하는데요.
바로 BeanFactory
와 ApplicationContext
입니다.
BeanFactory
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";
Object getBean(String name) throws BeansException;
<T> T getBean(String name, Class<T>requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
boolean containsBean(String name);
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;
String[] getAliases(String name);
}
먼저 BeanFactory 인터페이스를 살펴보면, 빈의 생성과 구성 관리를 돕는 여러 메서드를 제공합니다.
public class BeanFactoryExample {
public static void main(String[] args) {
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
MyBean bean = (MyBean) factory.getBean("myBean");
bean.foo();
}
}
class MyBean {
public void foo() {
System.out.println("foo");
}
}
해당 인터페이스는 위 코드 처럼 사용이 가능합니다. 먼저 XmlBeanFactory를 사용해서 beans.xml 파일로 부터 빈 설정을 로드하고,
BeanFactory의 getBean 메서드를 통해 해당 빈을 불러와서 사용하는 형태입니다.
하지만 보통 BeanFactory를 직접 사용할 일은 많이 없고, 그보다 더 고수준의 인터페이스인 ApplicationContext를 사용하는 경우가 더 많습니다.
ApplicationContext
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
@Nullable
String getId();
String getApplicationName();
String getDisplayName();
long getStartupDate();
@Nullable
ApplicationContext getParent();
AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}
ApplicationContext는 BeanFacotory의 확장판과 같은 개념으로 조금 더 쉽게 애플리케이션 레벨에서 스프링 빈을 잘 관리할 수 있도록 도와주고, 그외에도 국제화 기능, 이벤트 발행 등 여러가지 기능등을 추가적으로 지원합니다.
그래서 스프링 컨테이너를 보통 이야기할 때 스프링 컨테이너 == ApplicationContext라고 통용하여 사용합니다.
public class ApplicationContextExample {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
MyBean myBean = context.getBean(MyBean.class);
myBean.foo();
}
}
class MyBean {
public void foo() {
System.out.println("foo");
}
}
이 코드는 ApplicationContext를 사용하여 위의 예제를 대체한 코드입니다. 이처럼 ApplicationContext를 사용해서 BeanFactory의 기능을 사용할 수 있습니다.
public class InternationalizationExample {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MessageSource messages = (MessageSource) context;
String greeting = messages.getMessage("greeting", null, "Default Greeting", Locale.ENGLISH);
System.out.println(greeting);
String greetingSpanish = messages.getMessage("greeting", null, "Default Greeting", new Locale("es", "ES"));
System.out.println(greetingSpanish);
}
}
이 코드는 ApplicationContext를 활용한 국제화 기능의 사용 예시입니다. ApplicationContext는 BeanFactory의 기본 기능 외에도,
여러가지 편의 기능을 사용할 수 있습니다.
따라서 극히 제한적인 메모리 환경이 아니고서야 ApplicationContext를 사용하는 것이 좋습니다.
그렇다면 어떻게 자바 객체를 스프링의 빈으로 등록할 수 있을까요?
BeanDefinition
스프링 프레임워크는 빈을 정의하기 위한 여러가지 방법을 제공합니다.
1. XML 기반 구성
<bean id="myBean" class="com.example.MyBean" />
전통적으로 스프링 어플리케이션에서는 XML 파일을 사용해 빈을 등록해왔습니다.
이 설정은 애플리케이션 코드에서 빈 설정을 할 필요가 없다는 장점이 있습니다만, XML파일을 함께 유지보수 하는데 번거로움이 있습니다.
2. 어노테이션 기반 구성
@Component
public class MyBean {
...
}
스프링 2.5 버전 이후부터 어노테이션을 사용하여 빈을 정의하고 의존성을 주입할 수 있게 변경되었습니다.
@Component, @Service, @Repository, @Controller 등의 어노테이션을 사용하여 등록합니다.
이 방법은 개발 속도를 향상시키고 코드의 가독성을 높일 수 있어 많이 사용하빈다.
3. 자바 기반 구성
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
스프링 3.0 버전 이후 부터 자바의 Configuration 클래스를 사용해서도 빈을 정의할 수 있게되었습니다.
@Configuration 어노테이션이 붙은 클래스 내에서 @Bean 어노테이션을 사용하여 빈을 정의할 수 있게 되었습니다.
물론 @Configuration이 붙은 클래스 그 자체도 스프링 빈으로서 관리됩니다.
스프링 컨테이너 왜 써야 할까?
스프링 컨테이너에 대해서 간단히 알아보았는데, 잘 이해가 안되는 점이 있을 것이라고 생각이 듭니다.
굳이 스프링 컨테이너 없이도 사용이 가능할 것 같은데 왜 스프링 컨테이너를 써야할까요?
스프링 컨테이너는 빈의 생명주기를 관리하는데, 빈이 생성될 때 싱글톤 스코프로 빈을 생성합니다.
즉, 스프링 컨테이너가 해당 빈의 인스턴스를 단 하나만 생성하고 모든 요청에 대해 그 인스턴스를 재사용한다는 뜻입니다.
예를 들어보겠습니다.
public class MySingletonService {
public void serviceMethod() {
System.out.println("Service method called on instance: " + this.hashCode());
}
}
이러한 서비스 클래스가 있다고 해봅시다.
@Configuration
public class AppConfig {
@Bean
public MySingletonService mySingletonService() {
return new MySingletonService();
}
}
이렇게 Configuration 클래스에서 해당 클래스의 빈을 정의합니다.
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MySingletonService service1 = context.getBean(MySingletonService.class);
service1.serviceMethod();
MySingletonService service2 = context.getBean(MySingletonService.class);
service2.serviceMethod();
System.out.println("Are the service instances the same? " + (service1 == service2));
// 출력: true;
}
}
위 메서드를 실행시켜보면 service1 과 service2 의 해시코드 (메모리 주소)가 같은 것을 알 수 있습니다.
즉, 여러번 호출될 때 매번 인스턴스를 생성하는 것이 아닌 단 하나의 인스턴스를 재사용하는 것을 알 수 있습니다.
이를 통해 여러개의 요청을 받을때에도 객체를 하나만 생성해서 메모리를 효율적으로 관리할 수 있습니다.
싱글톤 컨테이너의 함정
싱글톤 컨테이너가 좋은걸 알았으니 무작정 사용하기만 하면 큰 문제를 맞닥뜨릴 수 있습니다.
바로 싱글톤 컨테이너는 무상태(stateless) 해야한다는 조건이 있기 때문입니다.
예시를 들어보겠습니다.
public class StatefulService {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
이런 서비스 객체가 있다고 가정해봅시다. 해당 클래스는 count라는 전역 변수를 가지고 있는 상태가 있는 클래스입니다.
@Configuration
public class AppConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
이제 StatefulService를 싱글톤 빈으로 등록해봅시다.
이렇게되면 여러개의 요청이 왔을 때 해당 서비스 인스턴스를 하나를 공유하게 되겠죠?
public class Main {
public static void main(String[] args) throws InterruptedException {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
StatefulService service1 = context.getBean(StatefulService.class);
StatefulService service2 = context.getBean(StatefulService.class);
new Thread(() -> {
service1.increment();
System.out.println("Thread 1: " + service1.getCount());
}).start();
new Thread(() -> {
service2.increment();
System.out.println("Thread 2: " + service2.getCount());
}).start();
}
// 출력
// Thread 1: 2
// Thread 2: 1
}
위 코드를 실행하면 최종 출력은 1, 2 순서이어야 할 것 같지만, 현실은 두 스레드가 어느 시점에 해당 인스턴스에 접근해서 내부 상태를 바꾸는지 정확하게 알아차리기 힘들게 됩니다.
위 코드도 직접 실행시켜보시면 아시겠지만, 실행시마다 출력값이 변동되는 등 예측하기 어렵게 동작합니다.
이러한 상황은 데이터 불일치와 같은 심각한 문제를 어플리케이션에서 야기시킬 수 있게 됩니다.
오늘은 스프링 컨테이너와 빈에 대해서 간단히 알아보았는데요!
다음 시간에는 더욱 자세히 스프링 컨테이너와 빈으로 의존관계가 어떻게 주입되는지 알아보도록 하겠습니다!
'Backend > Spring 🌱' 카테고리의 다른 글
ServiceImpl 쓰지 말까? (0) | 2024.06.14 |
---|---|
HandlerMapping과 HandlerAdapter는 왜 나뉘었나요? (0) | 2024.02.07 |
스프링부트 프로젝트 시작하는 법 (0) | 2024.02.02 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆