
자바 개발자를 위한 온라인 리소스 및 교육 플랫폼 중 "Baeldung" 내에서 제공하는 Spring 관련 Tutorial에 대해 학습해보는 시간을 갖겠습니다.
해당 내용은 전적으로 Baeldung에 기재된 내용을 바탕으로 서술되어 있으며, 관련 내용 중 필자가 부족한 개념은 추가로 포스팅할 예정입니다.
📜 참고 포스팅
이번 포스팅에서는 Spring의 핵심 개념 중 하나인 IoC(Inversion Of Control)과 DI(Dependency Injection)에 대해서 알아보겠습니다.
Inversion of Control은 유연성 향상과 테스트 용이성을 위해서 기존의 프로그램 제어 구조를 뒤집는 것을 의미합니다.
IoC는 제어의 책임을 애플리케이션에서 외부 프레임워크 또는 컨테이너로 이동합니다. 즉, IoC 컨테이너에 의해서 애플리케이션 내 객체의 인스턴스화 및 생명 주기와 종속성이 관리됩니다.
IoC를 구현하기 위해 Spring에서는 다음과 같은 기본 요소들을 가지고 있습니다.
애플리케이션 내 구성 요소들을 분리하여 요소 간의 종속성을 최소화하고 모듈화를 향상시켜줍니다. 즉, IoC는 긴밀한 결합에서 느슨한 결합으로 변경해줍니다.
이는 애플리케이션 내 구성 요소 간에 구체적인 구현 대신에 잘 정의된 인터페이스로 대체할 수 있도록 해줍니다. 이를 통해서 애플리케이션의 유연성 향상 및 유지 보수성을 높여줍니다.
기존에는 구성 요소가 종속성에 따라서 생성 및 관리되는 경우가 많았기 때문에 긴밀한 결합이 되었습니다. 하지만, IoC 종속성 관리에서는 IoC 컨테이너에 의해서 객체의 생성 및 생명 주기를 관리하고 필요 시 필요한 종속성을 주입해줍니다.
따라서 IoC는 종속성 관리를 중앙 집중화함으로써 복잡한 애플리케이션을 더 쉽게 구성함과 동시에 재사용성과 테스트 유용성을 높여줍니다.
Spring은 IoC 특정 프레임워크 및 IoC 컨테이너를 활용하여 IoC를 쉽게 구현할 수 있습니다. 개발자는 IoC 컨테이너 또는 프레임워크를 사용하여 강력하고 확장 가능한 애플리케이션을 구축함과 동시에 개발 시간과 코드 품질을 가속화 시킬 수 있게 됩니다.
IoC와 기존 제어 방식을 비교하기 위해서는 두 방식 모두의 제어 흐름과 종속성을 관리하는 방법에 대해서 이해해야 합니다.
기존의 방식은 클래스가 종속성을 사용하기 위해 💡직접 인스턴스화 하는 경우가 많아서 구성 요소 간의 긴밀한 결합이 발생합니다.
💡 직접 인스턴스화
다른 클래스의 종속성을 사용하기 위해서 다른 클래스의 인스턴스를 new 연산자를 사용하여 직접 생성하는 방식을 의미합니다.
public class A{ private B b; // 필드 // 생성자 public A(){ b = new B(); } //메서드 public void a(){ b.method(); } }
반면에, IoC를 통한 종속성 관리는 외부 컨테이너를 통해서 클래스에 종속성을 주입하기 때문에 클래스를 종속성에서 분리하여 보다 유연하고 모듈화된 디자인 기능이 가능하도록 해줍니다.
마지막으로 IoC는 테스트 유용성에서 큰 장점을 제공합니다. 이는 실제 의존성을 mock 또는 stub으로 쉽게 교체해줄 수 있기 때문입니다. 즉, 외부적으로 의존성을 제공해주기 때문에 테스트를 위한 코드만 작성해주면 됩니다.
그리고 모듈화 및 유지 관리성에 대해서는 기존의 방식은 긴밀한 결합으로 인해 코드 변경이 어려운 반면에 IoC 방식은 느슨하게 결합되어 있어 로직 변경이 상대적으로 간편합니다.
| Pros | Cons |
|---|---|
| 종속성을 동적으로 바인딩 하여 유연성을 향상시켜 줍니다. | IoC 활용을 위해서는 새로운 프레임워크나 라이브러리를 배워야 하므로 소규모 프로젝트나 팀적으로 코드가 복잡해질 수 있습니다. |
| 코드 관리 및 수정이 용이합니다. | IoC는 실행 흐름을 모호하게 만들고 시스템 작동을 파악하기 어렵게 만들 수 있습니다. |
| mock 객체를 통해 의존 주입이 가능하도록 해주므로 테스트 유용성을 향상시켜줍니다. | 일부 IoC 프레임워크는 동적 구성 변경을 제공하지만 직접적인 종속성 변경을 제한합니다. |
| 느슨한 결합을 촉집해주어 모듈화를 향상시켜 줍니다. | 일부 프레임워크에서는 동적 인스턴스화 시에 리플렉션을 사용하기 때문에 잠재적으로 성능 저하가 발생할 수 있습니다. |
참고
The advantages of this architecture are:
- decoupling the execution of a task from its implementation
- making it easier to switch between different implementations
- greater modularity of a program
- greater ease in testing a program by isolating a component or mocking its dependencies, and allowing components to communicate through contracts
의존 주입(Dependency Injection)은 IoC 구현을 위해서 사용하는 패턴입니다.
전통적인 방식에서 직접 종속성 주입을 위해 다음과 같이 구현하였습니다.
public class Store {
private Item item;
public Store() {
item = new ItemImpl1();
}
}
위의 경우, Item이라는 인터페이스의 실제 구현체를 Store 클래스 내부에 직접 구현해줬어야 했습니다.
DI를 사용한 경우, 구체적인 구현부 없이 사용이 가능합니다.
public class Store {
private Item item;
public Store(Item item) {
this.item = item;
}
}
IoC Container는 IoC를 구현한 프레임워크의 일반적인 특징을 갖습니다.
Spring 프레임워크에서는 ApplicationContext 인터페이스가 IoC Container를 나타냅니다. Spring 컨테이너는 Bean으로 알려진 객체의 인스턴스화, 구성 및 조립은 물론 해당 lifecycle을 관리해줍니다.
Spring 프레임워크는 독립적인 애플리케이션을 위해서 AnnotationConfigApplicationContext, ClassPathXmlApplicationContext 및 FileSystemXmlApplicationContext, 웹 애플리케이션을 위한 WebApplicationContext 등 여러 ApplicationContext의 구현체를 제공합니다.
Spring Container는 Bean을 조합하기 위해서 XML 설정 또는 Annotation 형식의 설정 메타데이터를 사용합니다.
Spring 에서는 AnnotationConfigApplicationContext의 인스턴스를 생성한 뒤 하나 이상의 Configuration 클래스를 제공하면 해당 클래스에서 @Bean 어노테이션 및 기타 관련 어노테이션을 검색합니다. 그 다음 정의된 Bean을 초기화 및 관리하고 종속성 설정 및 생명주기(lifecycle)을 관리합니다.
위 예제에서 item 관련 속성 설정을 위해서 메타데이터를 사용할 수 있습니다. 컨테이너는 메타데이터를 읽고 이를 통해 Runtime 시점에 관련 Bean을 조립합니다.
Spring에서 DI는 생성자, Setter 또는 필드를 통해서 수행될 수 있습니다.
@Configuration
public class AppConfig {
@Bean
public Item item1() {
return new ItemImpl1();
}
@Bean
public Store store() {
return new Store(item1());
}
}
@Configuration 어노테이션은 AppConfig 클래스가 Bean을 정의하기 위한 클래스임을 나타냅니다.
@Bean 어노테이션을 통해 Bean을 정의합니다. 사용자 정의를 통해 이름을 지정하지 않으면 Bean의 기본 명칭은 메서드 명과 동일합니다.
기본적으로 Spring에서 제공하는 Bean은 Singleton 범위를 갖습니다. 즉, 캐시에 저장된 Bean 인스턴스가 존재하는지 여부를 확인하고 없는 경우에만 새로운 인스턴스를 생성합니다.
반면에, Prototype 범위 빈은 매번 새로운 인스턴스를 생성합니다. 컨테이너가 프로토타입 범위 빈을 요청 시마다 새로운 인스턴스를 반환합니다.
Bean을 생성하는 또 다른 방법으로 XML 설정을 사용하는 것이 있습니다.
<bean id="item1" class="org.baeldung.store.ItemImpl1" />
<bean id="store" class="org.baeldung.store.Store">
<constructor-arg type="ItemImpl1" index="0" name="item" ref="item1" />
</bean>
setter 기반의 DI는 기본 생성자 또는 정적 팩토리 메서드를 통해서 객체를 생성한 뒤 setter 메서드를 호출해줍니다.
@Bean
public Store store() {
Store store = new Store();
store.setItem(item1());
return store;
}
동일한 Bean 설정 방식으로 XML을 사용할 수도 있습니다.
<bean id="store" class="org.baeldung.store.Store">
<property name="item" ref="item1" />
</bean>
Spring 문서에서는 필수적으로 사용해야 하는 종속성에 대해서 생성자 기반의 DI를 사용하고 선택적 종속성에 대해서 setter 기반의 주입을 사용할 것을 권장합니다.
필드 기반의 DI는 @Autowired 어노테이션을 사용하여 종속성 주입이 가능합니다.
public class Store {
@Autowired
private Item item;
}
위 상황에서 Store 객체 생성 시 Item 빈을 주입하는 생성자 또는 setter 메서드가 없는 경우 컨테이너는 리플렉션을 사용하여 Item 객체를 Store에 주입해줍니다.
위 방식은 코드가 더 간결해 보일 수 있지만 다음과 같은 단점을 가지고 있기 때문에 사용하는 것을 지양합니다.
만약, 동일한 타입의 Bean이 하나 이상 등록되어 있다면, @Qualifier 어노테이션을 통해서 해당 Bean의 명칭으로 구분해줄 수 있습니다.
public class Store {
@Autowired
@Qualifier("item1")
private Item item;
}
위처럼 Spring의 핵심 개념인 IoC와 DI에 대해서 간략하게 정리해봤습니다. 핵심적인 내용들이 많기 때문에 실습을 통해서 복습해보는 것이 좋을 것 같습니다.