JAVA/Spring
[Java / Spring] - Spring IoC / DI란
nam_ji
2024. 6. 10. 20:04
IoC / DI란
IoC란
- IoC란 Inversion of Control의 줄임말이며, 제어의 역전이라고 합니다.
- 스프링 애플리케이션에서는 오브젝트(빈)의 생성과 의존 관계 설정, 사용, 제거 등의 작업을 애플리케이션 코드 대신 스프링 컨테이너가 담당합니다.
- 이를 스프링 컨테이너가 코드 대신 오브젝트에 대한 제어권을 갖고 있다고 해서 IoC라고 부릅니다.
- 따라서, 스프링 컨테이너를 IoC 컨테이너라고 부릅니다.
IoC 컨테이너란
- 스프링에서는 IoC를 담당하는 컨테이너를 빈팩토리, DI 컨테이너, 애플리케이션 컨텍스트라고 부릅니다.
- 오브젝트의 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI 관점으로 보면, 컨테이너를 빈팩토리 또는 DI 컨테이너라고 부릅니다.
- 그러나 스프링 컨테이너는 단순한 DI 작업보다 더 많은 일을 하는데, DI를 위한 빈팩토리에 여러가지 기능을 추가한 것을 애플리케이션 컨텍스트라고 합니다.
- 정리하면, 애플리케이션 컨텍스트는 그 자체로 IoC와 DI 그 이상의 기능을 가졌다고 보면 됩니다.
빈팩토리와 애플리케이션 컨텍스트
1. 빈팩토리
- 스프링 컨테이너의 최상위 인터페이스입니다.
- 스프링 빈을 관리하고 조회하는 역할을 담당합니다.
- 대표적으로 getBean() 메서드를 제공합니다.
2. 애플리케이션 컨텍스트
public interface ApplicationContext extends EnvironmentCapable,
ListableBeanFactory,
HierarchicalBeanFactory,
MessageSource,
ApplicationEventPublisher,
ResourcePatternResolver {
- 애플리케이션 컨텍스트는 빈팩토리 기능을 모두 상속받아 제공합니다.
- 위 인터페이스에서 extends한 인터페이스들은 모두 빈팩토리 인터페이스의 서브 인터페이스이며, 빈팩토리에게 없는 추가 기능을 가지고 있습니다. 따라서, 애플리케이션은 이를 혼합하여 다음과 같은 기능을 제공합니다.
- 메시지 소스를 활용한 국제화 기능
- 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력 - 환경 변수
- 로컬, 개발, 운영 등을 구분해서 처리 - 애플리케이션 이벤트
- 이벤트를 발행하고 구독하는 모델을 편리하게 지원 - 편리한 리소스 조회
- 파일, 클래스 패스, 외부 등에서 리소스를 편리하게 조회
- 메시지 소스를 활용한 국제화 기능
설정 메타 정보
- IoC 컨테이너의 가장 기초적인 역할을 오브젝트를 생성하고 이를 관리하는 것입니다.
- 스프링 컨테이너가 관리하는 이런 오브젝트는 빈이라 부릅니다.
- 설정 메타 정보는 바로 이 빈을 어떻게 만들고 어떻게 동작하게 할 것인가에 관한 정보입니다.
- 스프링 컨테이너는 자바 코드, XML, Groovy 등 다양한 형식의 설정 정보를 받아들일 수 있도록 유연하게 설계되어 있습니다.
1. 어노테이션 기반 자바 코드 설정
@Configuration
public class AppConfig {
@Bean
public Service service() {
return new ServiceImpl(repository());
}
}
- @Configuration : 1개 이상의 빈을 제공하는 클래스의 경우 반드시 명시해야 합니다.
- @Bean : 클래스를 빈으로 등록할 때 사용합니다.
2. XML 기반의 스프링 빈 설정
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://
www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="service" class="spring.example.class.ServiceImpl">
<constructor-arg name="repository" ref="repository"/>
</bean>
</beans>
- XML 기반의 설정 파일을 보면 자바 코드로 된 설정 파일과 거의 비슷하다는 것을 알 수 있습니다.
- XML 기반으로 설정하는 것은 최근에 잘 사용하지 않습니다.
스프링 빈 설정 메타 정보 - BeanDefinition
- 스프링이 이렇게 다양한 형식을 지원하는 방법 중 하나가 BeanDefinition이라는 추상화가 있습니다.
- 쉽게 말해, XML을 읽어서 BeanDefinition을 만들고, 자바 코드를 읽어서 BeanDefinition을 만듭니다.
- 따라서 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 오직 BeanDefinition만 알면 됩니다.
- BeanDefinition을 빈 설정 메타 정보라고 하는데, @Bean과 <Bean> 당 각각 하나씩 메타 정보가 생성됩니다.
- AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성합니다.
- GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition을 생성합니다.
- 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition을 생성하면 됩니다.
Dependency(의존 관계)란
- 토비의 스프링에서 의존 대상 B가 변하면, 그것이 A에 영향을 미친다고 합니다.
즉, B의 기능이 추가되거나 변경되면 그 영향이 A에 미치는 것입니다.
DI(의존 관계 주입)란
- DI란 Dependency Injection의 약자이며, 의존 관계 주입 (의존성 주입)이라고 불립니다.
- new(객체를 직접 생성)로 생성하는 것이 아닌, 외부에서 생성된 객체를 주입받아 사용하는 것입니다.
- IoC가 행하여 질때, Spring 내부의 객체들 간의 관계를 관리할 때 사용하는 기법입니다.
- 강하게 결합된 Class를 분리
- Application 실행 시점에 객체 간의 관계를 결정, 주입
- 결합도를 낮추고 유연성을 확보합니다.
DI(의존 관계 주입) 구현 방법
1. 필드 주입
@Service
public class A {
@Autowired
private B b;
}
- 변수 선언부에 @Autowired 어노테이션을 붙입니다.
- 장점
- 사용하기 편리합니다.
- 단점
- 단일 책임 원칙 위반 가능성이 커집니다.
- @Autowired 선언만 하면 되므로 의존성 주입이 쉽습니다.
- 따라서, 하나의 클래스가 많은 책임을 갖게 될 가능성이 높습니다.
- 의존성이 숨는다.
- 생성자 주입에 비해 의존 관계를 한 눈에 파악하기 어렵습니다.
- DI 컨테이너와의 결합도가 커지고, 테스트하기 어렵습니다.
- 불변성을 보장할 수 없습니다.
- 순환 참조가 발생할 수 있습니다.
- 단일 책임 원칙 위반 가능성이 커집니다.
2. 수정자 주입
@Service
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
}
- Setter를 사용한 주입입니다.
- 장점
- 선택적인 의존성을 사용할 수 있습니다.
- 단점
- 선택적인 의존성을 사용할 수 있다는 것은 A에 모든 구현체를 주입하지 않아도 B 객체를 생성하 수 있고, 객체의 메서드를 호출할 수 있습니다.
즉, 주입받지 않은 구현체를 사용하는 메서들에서 NPE가 발생합니다. - 순환 참조 문제가 발생할 수 있습니다.
- 선택적인 의존성을 사용할 수 있다는 것은 A에 모든 구현체를 주입하지 않아도 B 객체를 생성하 수 있고, 객체의 메서드를 호출할 수 있습니다.
3. 생성자 주입
@Service
public class A {
private B b;
@Autowired
public B(B b) {
this.b = b;
}
}
- 생성자에 @Autowired 어노테이션을 붙여 의존성을 주입받을 수 있으며, 가장 권장되는 주입 방식입니다.
- 장점
- 의존 관계를 모두 주입해야만 객체 생성이 가능하므로 NPE를 방지할 수 있습니다.
- 불변성을 보장할 수 있습니다.
- 순환 참조를 컴파일 단계에서 찾아낼 수 있습니다.
- 의존성 주입하기 번거롭고, 생성자 인자가 많아지면 코드가 길어져 위기감을 느낄 수 있습니다.
- 이를 바탕으로 SRP 원칙을 생각하게 되고, 리팩토링을 수행하게 됩니다.
순환 참조
- 순환 참조란 서로 다른 여러 빈들이 서로를 참조하기 있음을 의미합니다.
- A에서 B에 의존하고, B가 A에 의존하면 순환 참조라고 볼 수 있습니다.
1. 필드 주입인 경우
@Service
public class A {
@Autowired
private B b;
@Override
public void aMethod() {
b.bMethod();
}
}
@Service
public class B {
@Autowired
private A a;
@Override
public void bMethod() {
a.a();
}
}
- 이 상황은 A의 aMethod는 B의 bMethod를 호출하고 B의 bMethodsms aMethod를 호출하고 있는 상황입니다.
- 서로 호출을 반복하면서 끊임없이 호출하여 StackOverflowError를 발생시키게 됩니다.
- 이처럼 필드 주입이나 수정자 주입은 객체 생성 후 비즈니스 로직 상에서 순환 참조가 일어나기 때문에 컴파일 단계에서 순환 참조를 잡아낼 수 없습니다.
2. 생정자 주입인 경우
@Service
public class A {
private final B b;
@Autowired
public A(B b) {
this.b = b;
}
@Override
public void AMethod() {
b.bMethod();
}
}
@Service
public class B {
private final A a;
@Autowired
public B(A a) {
this.a = a;
}
@Override
public void BMethod() {
a.aMethod();
}
}
- 생성자 주입일 때 애플리케이션을 실행하면 로그가 찍히며 실행이 실패합니다.
- 이처럼 생성자 주입은 스프링 컨테이너가 빈을 생성하는 시점에 순환 참조를 확인하기 때문에 컴파일 단계에서 순환 참조를 잡아낼 수 있습니다.
@Autowired
- DI를 할 때 사용하는 어노테이션이며, 의존 관계의 타입에 해당하는 빈을 찾아 주입하는 역할을 합니다.
- 쉽게 말해, 스프링 서버가 올라갈 때 애플리케이션 컨텍스트가 @Bean이나 @Service, @Controller 등 어노테이션을 이용하여 등록한 스프링 빈을 생성하고, @Autowired 어노테이션이 붙은 위치에 의존 관계 주입을 수행하게 됩니다.
- 그렇다면, @Autowired 어노테이션이 붙은 위치에 어떻게 의존 관계를 주입할 수 있는 방법은
코드를 보면 -
/** * Note that actual injection is performed through a BeanPostProcessor which in turn means * that you cannot use @Autowired to inject references into BeanPostProcessor or * BeanFactoryPostProcessor types. Please consult the javadoc for the * AutowiredAnnotationBeanPostProcessor class (which, by default, checks for the presence * of this annotation). * Since: * 2.5 * See Also: * AutowiredAnnotationBeanPostProcessor, Qualifier, Value * Author: * Juergen Hoeller, Mark Fisher, Sam Brannen */ @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { boolean required() default true; }
- @Target : 생성자와 필드, 메서드에 적용 가능합니다.
- @Retention : 컴파일 이후(런타임 시) JVM에 의해 참조가 가능합니다. 런타임 시 이 어노테이션의 정보를 리플렉션으로 얻을 수 있습니다.
- 위 코드 상담 주석을 보면, 실제 타깃에 Autowired가 붙은 빈을 주입하는 것은 BeanPostProcessor라는 내용을 찾을 수 있고, 그것의 구현체는 AutowiredAnnotationBeanPostProcessor인 것을 확인할 수 있습니다.