스프링 프레임워크의 제어의 역전(IoC)과 의존성 주입(DI)
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)이란 무엇인지 알아보자. 의존성이란 기능이 정상적으로 동작하기 위해 필요한 요소를 의미한다.
- 어떤
클래스A
가 다른클래스B
또는인터페이스B
를 이용할 때클래스A
가클래스B
에 의존한다고 한다. 클래스A
는클래스B
에 의존적(dependent)이고,클래스B
는클래스A
의 의존성(dependency)이다.클래스A
는클래스B
없이 작동할 수 없다.클래스B
에 변화에클래스A
는 영향을 받지만,클래스A
의 변화에클래스B
는 영향을 받지 않는다.
이해를 돕기 위해 예제 코드를 살펴보자.
클래스A
는클래스B
에 의존적이다.클래스B
는클래스A
의 의존성이다.
class A {
private B b;
public A () {
this.b = new B();
}
}
클래스 다이어그램으로 표현하면 다음과 같다.

2.2. Type of Dependency Injection
스프링 프레임워크는 다음과 같은 방법으로 의존성 주입 기능을 제공한다.
- 생성자 주입(Cosntructor Injection)
- 세터 주입(Setter Injection)
- 애너테이션 주입(Annotation 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();
}
}
세터(setter) 주입은 다음과 같이 세터 함수를 사용한다.
- 세터(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();
}
}
프레임워크에서 제공하는 애너테이션을 통해 주입이 가능하다.
- @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)을 낮춘다.
- 코드 유지 보수하기 쉬워진다.
3. Practice
간단한 예제 코드를 통해 IoC 원칙을 따르는 스프링 프레임워크가 의존성 주입을 통해 어떤 문제점을 해결해주는지 알아보자. 스프링 프레임워크는 의존성 주입을 위해 다음과 같은 구조를 가지고 있다.
- 스프링 프레임워크에서 관리하는 객체들을 빈(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 컨테이너
를 통해 해결할 수 있다. 스프링에서 제공하는 애너테이션을 사용하면 특정 객체를 생성 후 스프링 빈 객체로써 IoC 컨테이너 등록한다. 스프링 빈으로 등록된 객체는 필요한 곳에 주입된다. DefaultDeliveryService 클래스를 다음과 같이 변경한다.
- DefaultDeliveryService 클래스에 @Service 애너테이션을 추가한다.
- @Service 애너테이션을 붙히면 각 클래스의 객체들이 빈으로써 IoC 컨테이너에서 관리된다.
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();
}
}
JpaDeliveryStore 클래스를 다음과 같이 변경한다.
- JpaDeliveryStore 클래스에 @Repository 애너테이션을 추가한다.
- @Repository 애너테이션을 붙히면 각 클래스의 객체들이 빈으로써 IoC 컨테이너에서 관리된다.
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();
}
}
IoC 컨테이너는 자신이 관리하고 있는 빈들의 의존 관계를 따라 필요한 곳에 빈 객체들을 주입한다. 이해를 돕기 위해 이미지로 시각화해보자.
- 만들어진 빈 객체가 없다면 새로 만들어 주입한다.
- 빈 객체를 만들기 위한 후보 클래스가 없다면 에러가 발생한다.

댓글남기기