[Spring] IoC와 DI

IoC(Inversion of Control : 제어의 역전)와 DI(Dependency Injection : 의존성 주입)의 주 목적

컴포넌트의 의존성을 제공하고 이를 라이프사이클 전반에 걸쳐 관리하는 보다 간편한 메커니즘을 제공하는 것

입니다. 

의존성이 필요한 컴포넌트의존 객체(dependent object)라 하고 IoC에서는 이를 "대상(target)"이라고 합니다. 

 

일반적으로 IoC는 의존성 주입(DI)의존성 룩업(DL)의 두 가지 하위분류로 나눌 수 있습니다. 이들 하위분류는 다시 구체적인 IoC 서비스 구현체로 나뉩니다. 이 분류체계를 그림으로 간단히 나타내면 다음과 같습니다. 

위의 IoC의 종류에 대해 자세히 알아봅시다.

 

IoC의 종류

의존성 룩업(Dependency Lookup : DL)

의존성 룩업 방식에서는 

컴포넌트 스스로 의존성의 참조를 가져옵니다.

의존성 룩업은 의존성 풀문맥에 따른 의존성 룩업이라는 두 가지 방식으로 나뉩니다. 

의존성 풀(Dependency Pull)

의존성 풀에서는

필요에 따라 레지스트리에서 의존성을 가져오게 됩니다. 

아래 그림은 JNDI 룩업을 통한 의존성 풀 룩업 메커니즘을 사용하는 시나리오입니다.

💡 JNDI란?
JNDI는 Java Naming and Directory Interface의 약어로, 디렉터리 서비스에서 제공하는 데이터 및 객체를 발견(discover)하고 참고(lookup) 하기 위한 자바 API입니다. 간단히 요약하면 연결하고 싶은 데이터베이스의 DB Pool을 미리 Naming 시켜주는 방법 중 하나입니다. 우리가 저장해놓은 WAS 의 데이터베이스 정보에 JNDI를 설정해 놓으면 웹 애플리케이션은 JNDI만 호출하면 되는 것이죠.

스프링 프레임워크도 자신이 관리하는 컴포넌트를 가져오는 메커니즘 중 하나로서 의존성 풀을 제공합니다. 

다음은 스프링 애플리케이션에서 의존성 풀을 사용하는 일반적인 예제 코드입니다. 

import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DependencyPull {
    public static void main(String... args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext
           ("spring/app-context.xml");

        MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
        mr.render();
    }
}

spring/app-context.xml 파일에 등록되어 있는 "renderer"를 찾아오는(LookUp) 방식입니다.

 

문맥에 따른 의존성 룩업(Contextualized Dependency LookUp : CDL)

문맥에 따른 의존성 룩업(CDL)은 의존성 풀과 유사하지만 몇 가지 차이점이 있습니다. 

CDL은 특정 중앙 레지스트리에서 의존성을 가져오는 것이 아닌, 자원을 관리하는 컨테이너에서 의존성을 가져옵니다. 

또한, 늘 수행되는 것이 아닌 몇 가지 정해진 시점에 수행됩니다. 

아래 그림은 문맥에 따른 의존성 룩업의 메커니즘을 보여줍니다. 

CDL은 컴포넌트가 다음과 비슷한 인터페이스를 구현하는 방식으로 동작합니다.

public interface ManagedComponent {
    void performLookup(Container container);  
}

컴포넌트는 이 인터페이스를 구현해 의존 관계를 얻으려는 컨테이너에 신호를 보냅니다. 

아래 코드는 의존성 룩업 서비스를 제공하는 간단한 컨테이너 인터페이스 입니다. 

public interface Container {
    Object getDependency(String key);
}

 

컨테이너가 컴포넌트에 의존성을 전달할 준비가 되면 컨테이너는 차례대로 performLookUp() 메서드를 호출합니다. 그럼 컴포넌트는 다음과 같이 Container 인터페이스로 의존성을 룩업할 수 있습니다. 

public class ContextualizedDependencyLookup implements ManagedComponent {
    private Dependency dependency;

    @Override
    public void performLookup(Container container) {
        this.dependency = (Dependency) container.getDependency("myDependency"); 
    }

    @Override
    public String toString() {
    	return dependency.toString();
    }
}

의존성 주입(DI)

의존성 주입 방식은

IoC 컨테이너가 컴포넌트에 의존성을 주입합니다. 

생성자 의존성 주입(Constructor DI)

생성자 의존성 주입은 

컴포넌트의 생성자를 이용해서 해당 컴포넌트가 필요로하는 의존성을 제공하는 방식입니다. 

컴포넌트가 의존성을 인수로 가져오도록 생성자를 선언해 IoC 컨테이너가 해당 컴포넌트가 초기화될 때 컴포넌트에 필요한 의존성을 전달합니다. 

public class ConstructorInjection {

	private Dependency dependency;

	public ConstructorInjection(Dependency dependency) {
		this.dependency = dependency;
	}

	@Override
	public String toString() {
		return dependency.toString();
	}
}

주의할 점은 생성자 주입을 사용할 때, 의존성 주입 없이는 빈(Bean)을 생성할 수 없기 때문에 반드시 의존성을 주입해야 합니다. 

 

수정자 의존성 주입(Setter DI)

수정자 의존성 주입 방식은

 IoC 컨테이너가 자바빈 방식의 수정자 메서드(Setter)를 이용해 컴포넌트의 의존성을 주입하는 방식입니다. 

컴포넌트의 수정자는 IoC 컨테이너가 관리할 수 있도록 의존성을 노출합니다. 

public class SetterInjection {

	private Dependency dependency;

	public void setDependency(Dependency dependency) {
		this.dependency = dependency;
	}

	@Override
	public String toString() {
		return dependency.toString();
	}
}

수정자 주입을 사용하면

  1. 의존성 없이도 객체를 생성할 수 있다.
  2. 해당 수정자를 호출해 의존성을 나중에 제공할 수 있다.

는 명확한 특징이 있습니다.


의존성 주입 VS 의존성 룩업

많은 경우에 현재 사용하는 컨테이너에 따라 IoC의 방식이 정해집니다. 스프링에서는 초기 빈 룩업을 제외하면 컴포넌트와 의존성은 항상 의존성 주입 방식의 IoC를 이용해 연결됩니다. 

 

위의 예시 코드들을 확인해보면, 의존성 주입은 컴포넌트에 어떠한 코드 변화도 일으키지 않는다는 것을 확인할 수 있습니다. 반면에 의존성 풀을 이용하는 경우 레지스트리에 대한 참조를 얻어와 의존성을 얻기 위해 상호작용이 필수적입니다. 또한, CDL을 사용하면 클래스는 특정 인터페이스를 구현하고 모든 의존성을 직접 가져와야 합니다. 반면 의존성 주입을 사용하면 대부분의 클래스가 해야 할 일은

생성자나 수정자로 의존성이 주입될 수 있게 하는 것 뿐입니다. 

 

또한, 의존성 주입을 이용하면 사용자 클래스는 IoC 컨테이너와 완전히 분리돼 자유롭게 사용될 수 있습니다. 하지만, 룩업을 이용하는 경우 사용자 클래스는 컨테이너에 의해 정의된 클래스와 인터페이스에 항상 의존하게 됩니다. 

 

마지막으로, 룩업은 클래스를 컨테이너와 분리시킨 채 테스트하기 어렵습니다. 주입을 이용하면 적절한 생성자나 수정자를 사용해 사용자가 직접 테스트용 의존성을 쉽게 제공할 수 있습니다. 

 

의존성 주입 코드에서 볼 수 있는 가장 특징적인 부분은 객체가 필드에만 저장된다는 것입니다. 저장소나 컨테이너에서 의존성을 가져오는 코드가 전혀 필요 없습니다. 

위의 CDL 코드를 다시 살펴보겠습니다. 

public void performLookup(Container container) {
    this.dependency = (Dependency) container.getDependency("myDependency"); 
}

이 코드는 의존성 키가 바뀌거나, 컨테이너 인스턴스가 null이거나, 반환된 의존성이 잘못된 타입일 때처럼 여러 상황에서 에러가 발생할 수 있습니다. 의존성 룩업을 이용하면 애플리케이션의 컴포넌트들은 서로 분리할 수 있지만, 어던 작업을 하려할 때 이런 컴포넌트들을 다시 결합하는 추가 코드가 필요하기에 복잡해집니다. 

 

수정자 주입 VS 생성자 주입

이제 의존성 주입 방식이 더 낫다는 결론을 내렸지만, 수정자 주입과 생성자 주입 중 어느 방식을 택할지가 여전히 남아 있습니다. 특히, 생성자 주입컴포넌트 사용 전 해당 컴포넌트의 의존성을 반드시 갖고 있어야 하는 경우 매우 유용합니다. 생성자 주입을 이용하면 컨테이너가 의존성 점검 메커니즘을 제공하는지와 상관없이 의존성에 대한 요구사항을 지정할 수 있습니다. 또한, 빈 객체를 불변 객체로 사용할 수 있습니다. 

 

수정자 주입은 다양한 상황에서 유용합니다. 컴포넌트가 의존성을 컨테이너로 노출하지만, 기본 의존성을 제공할 땐 일반적으로 수정자 주입이 의존성 주입에 가장 좋은 방법입니다. 

 

예시를 통해 알아보죠.

defineMeaningOfLife() 라는 비즈니스 메서드 하나를 가진 비즈니스 인터페이스가 있다고 가정해봅시다. 이 메서드 외에 setEncyclopedia() 같은 의존성 주입 메서드를 인터페이스에 정의하는 것은 해당 인터페이스의 모든 구현체가 encyclopedia 의존성을 사용하거나 최소 인지하도록 강제하는 것입니다. 

하지만, 비즈니스 인터페이스에서 setEncyclopedia() 메서드를 정의할 필요가 없죠. 인터페이스를 구현하는 클래스에서 이 메서드를 정의하면 됩니다. 이런식으로 프로그래밍한다면 IoC 컨테이너는 사용자의 비즈니스 인터페이스를 사용해 사용자의 컴포넌트를 동작시키면서 구현 클래스의 의존성도 제공합니다. 

 

코드를 통해 봅시다. 

public interface Oracle {
    String defineMeaningOfLife();
}

Oracle 비즈니스 인터페이스에 의존성 주입을 위한 어떠한 수정자도 정의하지 않았습니다. 이는 다음과 같이 구현될 수 있습니다. 

public class BookwormOracle implements Oracle {
    private Encyclopedia encyclopedia;

    public void setEncyclopedia(Encyclopedia encyclopedia) {
        this.encyclopedia = encyclopedia;
    }

    @Override
    public String defineMeaningOfLife() {
        return "Encyclopedias are a waste of money -  go see the world instead";
    }
}

위 클래스는 인터페이스를 구현하고 의존성 주입을 위한 수정자도 정의했습니다. 즉, 비즈니스 인터페이스에서 의존성을 정의할 필요가 전혀 없는 것입니다. 

실제로, 주입을 위한 수정자가 사용자 인터페이스의 외부에 존재하도록 노력해야 합니다. 

수정자 주입을 사용하면 부모 컴포넌트의 인스턴스를 새로 생성하지 않고도 즉시 의존성을 다른 구현체로 교체할 수 있습니다. 

 

정리

위 내용을 정리해보자면,

수정자 주입을 사용하면 새로운 객체를 생성하지 않아도 의존성을 교체할 수 있고, 명시적으로 객체를 주입하지 않더라도 적정한 기본 값을 선택하게 할 수 있습니다. 

생성자 주입은 컴포넌트에 의존성 주입을 보장하거나 불변 객체를 설계하는 경우 좋은 선택입니다. 


참고

전문가를 위한 스프링 5