Inversion of Control and Dependency Injection in Spring

5 분 소요


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 컨테이너입니다.
  • ApplicationContextBeanFactory를 확장한 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 컨테이너는 자신이 관리하고 있는 빈들의 의존 관계를 따라 필요한 곳에 빈 객체들을 주입합니다.
  • 만들어진 빈 객체가 없다면 새로 만들어 주입합니다.
  • 빈 객체를 만들기 위한 후보 클래스가 없다면 에러가 발생합니다.

TEST CODE REPOSITORY

RECOMMEND NEXT POSTS

REFERENCE

댓글남기기