Inversion of Control and Dependency Injection in Spring
0. 들어가면서
스프링(spring)을 사용하거나 공부하면 반드시 만나는 개념이 있습니다.
- 제어의 역전(Inversion of Control)
- 의존성 주입(Dependency Injection)
이번 포스트에선 두 개념에 대해 정리하고, 어떤 연관성이 있는지 정리하였습니다.
1. Inversion of Control
Inversion of Control is a principle in software engineering which transfers the control of objects or portions of a program to a container or framework.
객체(object) 생성, 사용, 제거 등의 제어를 개발자가 직접하는 것이 아니라 컨테이너(container) 혹은 프레임워크(framework)에서 수행하자는 소프트웨어 공학의 원칙(principle)입니다.
스프링 프레임워크는 IoC
원칙을 따르도록 설계되어 있습니다.
라이브러리처럼 개발자가 작성한 코드에서 호출하여 사용하는 방식이 아닙니다.
프레임워크에서 개발자가 작성한 코드를 실행시킴으로써 시스템 흐름 제어의 주도권을 프레임워크가 가져갑니다.
2. Dependency Injection
IoC
원칙과 의존성 주입(dependency injection)이 함께 언급되는 이유를 살펴보기 전에 의존성(dependency)이란 무엇인지 알아보겠습니다.
2.1. What is Dependency?
의존성이란 기능이 정상적으로 동작하기 위해 필요한 요소를 의미합니다.
- 어떤
클래스A
가 다른클래스B
또는인터페이스B
를 이용할 때클래스A
가클래스B
에 의존한다고 합니다. 클래스A
는클래스B
에 의존적(dependent)이고,클래스B
는클래스A
의 의존성(dependency)입니다.클래스A
는클래스B
없이 작동할 수 없습니다.클래스B
에 변화에클래스A
는 영향을 받지만,클래스A
의 변화에클래스B
는 영향을 받지 않는다.
Example Code
다음과 같은 코드로 표현할 수 있습니다.
클래스A
는클래스B
에 의존적입니다.클래스B
는클래스A
의 의존성입니다.
class A {
private B b;
public A () {
this.b = new B();
}
}
Class Diagram
2.2. Type of Dependency Injection
스프링 프레임워크는 다음과 같은 방법으로 의존성 주입 기능을 제공합니다. 특정 객체가 빈으로써 관리되는 경우에만 의존성 주입이 적용되는 제약 사항이 있습니다.
2.2.1. Cosntructor Injection
- 생성자를 통해 의존성을 주입 받을 수 있습니다.
- 다른 의존성 주입 방법보다 안정적인 방법입니다.
package action.in.blog.service;
import action.in.blog.domain.Delivery;
import action.in.blog.store.DeliveryStore;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DefaultDeliveryService {
private final DeliveryStore deliveryStore;
public DefaultDeliveryService(DeliveryStore deliveryStore) {
this.deliveryStore = deliveryStore;
}
public List<Delivery> getAllDeliveriesOrderByStartTime() {
return deliveryStore.getAllDeliveriesOrderByStartTime();
}
}
2.2.2. Setter Injection
- 세터(setter) 메소드를 통해 의존성을 주입 받을 수 있습니다.
- 세터 메소드 위에
@Autowired
애너테이션으로 추가합니다.
package action.in.blog.service;
import action.in.blog.domain.Delivery;
import action.in.blog.store.DeliveryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DefaultDeliveryService {
private DeliveryStore deliveryStore;
@Autowired
public void setDeliveryService(DeliveryStore deliveryStore) {
this.deliveryStore = deliveryStore;
}
public List<Delivery> getAllDeliveriesOrderByStartTime() {
return deliveryStore.getAllDeliveriesOrderByStartTime();
}
}
2.2.3. Annotation Injection
@Autowired
애너테이션을 필드 위에 추가합니다.
package action.in.blog.service;
import action.in.blog.domain.Delivery;
import action.in.blog.store.DeliveryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DefaultDeliveryService {
@Autowired
private DeliveryStore deliveryStore;
public List<Delivery> getAllDeliveriesOrderByStartTime() {
return deliveryStore.getAllDeliveriesOrderByStartTime();
}
}
2.3. Inversion of Control and Dependency Injection
의존성 주입이란 어떤 객체A
가 정상적인 기능을 하기 위해 필요한 의존성을 외부에서 제공해주는 것을 의미합니다.
개발자가 직접 객체를 생성하고 이를 전달해주는 것이 아니라 프레임워크가 객체를 생성하고 이를 전달합니다.
결론을 이야기하면 의존성 주입(DI)은 IoC
원칙을 따르는 프레임워크가 제공하는 기능의 개발 패턴(pattern)을 의미합니다.
프레임워크에 의해 객체 생성, 사용, 전달 등이 제어됩니다.
시스템의 흐름뿐만 아니라 객체의 사용도 프레임워크가 제어 주도권을 가지게 됩니다.
IoC
원칙을 따르면 여러 장점들을 얻을 수 있다고 이야기하지만, 저는 다음 내용들이 가장 공감되었습니다.
- 객체 사이의 결합도(coupling)을 낮춘다.
- 코드 유지 보수하기 쉬워진다.
간단한 예제 코드를 통해 IoC
원칙을 따르는 스프링 프레임워크가 의존성 주입을 통해 어떤 문제점을 해결해주는지 알아보겠습니다.
3. Practice
스프링 프레임워크는 의존성 주입을 위해 다음과 같은 구조를 가지고 있습니다.
- 스프링 프레임워크에서 관리하는 객체들을 빈(bean)이라고 합니다.
- 스프링 프레임워크엔 다음과 같은 역할을 수행하는
IoC
컨테이너가 존재합니다.- 스프링에서 관리하는 빈 객체들을 생성, 등록, 조회, 반환하는 등의 관리를 수행합니다.
- 의존성 주입(dependency injection)과 관련된 기능을 수행합니다.
BeanFactory
는 스프링 프레임워크의 핵심IoC
컨테이너입니다.ApplicationContext
는BeanFactory
를 확장한IoC
컨테이너입니다.IoC
컨테이너는 의존성 주입을 수행하기 때문에DI
컨테이너라고 합니다.
3.1. DefaultDeliveryService 클래스
아래 코드는 다음과 같은 문제점을 가집니다.
- 현재
MyBatisDeliveryStore
객체는 내부에서MyBatis
프레임워크를 사용해 데이터를 조회하고 있습니다. - 만약 시간이 지나
JPA
,QueryDSL
같은 기술 스택을 사용하게 된다면MyBatis
는 사용하지 못 합니다.MyBatisDeliveryStore
객체 대신JpaDeliveryStore
객체로 대체되어야 합니다.
- 이는 필연적으로
DefaultDeliveryService
클래스의 변경을 발생시킵니다. DefaultDeliveryService
객체는MyBatisDeliveryStore
객체와 강하게 결합되어 있습니다.MyBatisDeliveryStore
객체를 다른 곳에서도 사용한다면 모두 변경이 발생합니다.
package action.in.blog.service;
import action.in.blog.domain.Delivery;
import action.in.blog.store.MyBatisDeliveryStore;
import java.util.List;
public class DefaultDeliveryService {
private final MyBatisDeliveryStore deliveryStore;
public DefaultDeliveryService() {
this.deliveryStore = new MyBatisDeliveryStore();
}
public List<Delivery> getAllDeliveriesOrderByStartTime() {
return deliveryStore.getAllDeliveriesOrderByStartTime();
}
}
3.2. Make Loose Coupling
DeliveryStore
인터페이스를 만들어 MyBatisDeliveryStore
, DefaultDeliveryService
둘 사이의 결합도를 낮춰줍니다.
package action.in.blog.store;
import action.in.blog.domain.Delivery;
import java.util.List;
public interface DeliveryStore {
List<Delivery> getAllDeliveriesOrderByStartTime();
}
MyBatisDeliveryStore
클래스가DeliveryStore
인터페이스 기능을 구현하는 모습으로 변경합니다.
package action.in.blog.store;
import action.in.blog.domain.Delivery;
import java.util.Collections;
import java.util.List;
public class MyBatisDeliveryStore implements DeliveryStore {
@Override
public List<Delivery> getAllDeliveriesOrderByStartTime() {
// some queries here
return Collections.emptyList();
}
}
- 멤버 변수 타입을
DeliveryStore
인터페이스 타입으로 대체합니다. MyBatisDeliveryStore
객체를 직접 생성하는 코드를 생성자를 통해 외부에서 전달받는 방식으로 변경합니다.- 이로써 데이터를 조회하는 프레임워크나 방법이 어떻게 바뀌더라도
DefaultDeliveryService
클래스의 변경은 발생하지 않습니다.
package action.in.blog.service;
import action.in.blog.domain.Delivery;
import action.in.blog.store.DeliveryStore;
import java.util.List;
public class DefaultDeliveryService {
private final DeliveryStore deliveryStore;
public DefaultDeliveryService(DeliveryStore deliveryStore) {
this.deliveryStore = deliveryStore;
}
public List<Delivery> getAllDeliveriesOrderByStartTime() {
return deliveryStore.getAllDeliveriesOrderByStartTime();
}
}
3.3. We need IoC Container
시스템에서 DefaultDeliveryService
객체를 사용하는 곳의 코드를 다음과 같이 변경해줘야 합니다.
DefaultDeliveryService
객체를 시스템 곳곳에서 사용하고 있다면 코드 변경이 여러 군데서 발생합니다.MyBatisDeliveryStore
객체를 대체하는 작업이 시스템 여러 곳의 코드 변경을 일으키고, 영향을 줍니다.
public static void main(String[] args) {
// DefaultDeliveryService deliveryService = new DefaultDeliveryService(new MyBatisDeliveryStore());
DefaultDeliveryService deliveryService = new DefaultDeliveryService(new JpaDeliveryStore());
deliveryService.getAllDeliveriesOrderByStartTime();
// ... some business logic
}
이 문제점을 IoC
컨테이너를 통해 해결할 수 있습니다.
DefaultDeliveryService
, JpaDeliveryStore
클래스를 다음과 같이 변경합니다.
DefaultDeliveryService
클래스에@Service
애너테이션을 추가합니다.JpaDeliveryStore
클래스에@Repository
애너테이션을 추가합니다.@Service
,@Repository
애너테이션을 붙히면 각 클래스의 객체들이 빈으로써IoC
컨테이너에서 관리됩니다.- 각 빈 객체들은
IoC
컨테이너에 의해 필요한 곳으로 주입됩니다.DefaultDeliveryService
객체를 사용하는 곳도 빈으로 주입 받을 수 있도록 변경합니다.
- 기술이나 로직(logic)이 바뀜에 따라
DeliveryService
구현체 클래스가 변경되더라도 시스템 다른 곳의 코드는 크게 바뀌지 않습니다.
package action.in.blog.service;
import action.in.blog.domain.Delivery;
import action.in.blog.store.DeliveryStore;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DefaultDeliveryService {
private final DeliveryStore deliveryStore;
public DefaultDeliveryService(DeliveryStore deliveryStore) {
this.deliveryStore = deliveryStore;
}
public List<Delivery> getAllDeliveriesOrderByStartTime() {
return deliveryStore.getAllDeliveriesOrderByStartTime();
}
}
package action.in.blog.store;
import action.in.blog.domain.Delivery;
import org.springframework.stereotype.Repository;
import java.util.Collections;
import java.util.List;
@Repository
public class JpaDeliveryStore implements DeliveryStore {
@Override
public List<Delivery> getAllDeliveriesOrderByStartTime() {
// some queries here
return Collections.emptyList();
}
}
Dependency Injection by IoC Container
IoC
컨테이너는 자신이 관리하고 있는 빈들의 의존 관계를 따라 필요한 곳에 빈 객체들을 주입합니다.- 만들어진 빈 객체가 없다면 새로 만들어 주입합니다.
- 빈 객체를 만들기 위한 후보 클래스가 없다면 에러가 발생합니다.
댓글남기기