본문 바로가기
Spring

DI(Dependency Injection)

by 지금 느낌 그대로 2021. 1. 3.

애플리케이션 코드를 작성할 때, 애플리케이션에 특정한 기능이 필요하다면 외부 라이브러리를 호출하여 사용하곤합니다. 개발자가 프로그램의 흐름의 주도권을 가지고 제어하는 구조입니다. 하지만, 스프링과 같은 프레임워크에서의 개발은 프레임워크에서 필요할 때 애플리케이션 코드를 호출하는 형태로 동작하므로 프레임워크가 흐름의 주체가 됩니다.

 

이렇게 제어권이 개발자에게서 프레임워크로 흐름이 바뀌었다고 하여 IoC(Inversion of Control : 제어의 역전)이라고 부릅니다. 이 때, 프레임워크에서 제어권을 가지는 것이 컨테이너(Container)입니다. 컨테이너는 객체의 생성과 생명주기 관리(Life Cycle) 관리등을 도맡아서 하게됩니다. 

 

출처 : nextree.co.kr/p11247/

 

세 가지 DI 컨테이너로 향하는 저녁 산책

애플리케이션 코드를 작성할 때, 특정기능이 필요하면 라이브러리를 호출하여 사용하곤 합니다. 프로그램의 흐름을 제어하는 주체가 애플리케이션 코드인 셈이지요. 하지만 프레임워크(Framework

www.nextree.co.kr

 

DI(Dependency Injection) 개념 이해하기

DI는 의존관계를 가지고 있는 클래스에 대한 설정을 외부에 두고, 설정 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것을 말합니다. 개발자가 객체를 생성하고 객체에 의존정보를 설정하지 않고, 개발자는 의존관계가 필요하다는 정보만 설정해주면 됩니다. 객체는 외부에 존재하는 설정정보로부터 참조해야할 클래스를 주입받아, 실행 시에 동적으로 의존관계가 생성됩니다. 애플리케이션 코드의 의존관계를 주입해주는 흐름의 주체가 개발자가 아니라 컨테이너가 되어 IoC를 구현한 형태가 됩니다. 정리하면, 제어권의 흐름이 개발자에서 컨테이너로 바뀌는 것을 IoC라고 부르고, IoC를 구현하는 방법 중 하나가 DI라고 할 수 있겠습니다.

 

예제를 통해 조금 더 자세히 살펴보겠습니다.

 

IoC/DI가 적용되지 않은 경우

package kr.co.sample

public class Hello {

    private HelloKr helloKr;
    
    public Hello() {
    	helloKr = new HelloKr();
    	helloKr.printHello("woony");
    }
}
package kr.co.sample;

public class HelloKr {
	public void printHello(String str) {
    	System.out.println(str + "안녕하세요");
    }
}

위의 코드에서 Hello 클래스 안에는 HelloKr 클래스의 메소드인 printHello()를 포함하고 있습니다. 그래서 HelloKr클래스를 참조(상호작용)하기위해 new연산자를 통해 구체적인 클래스의 이름을 이용해 초기화시킵니다. 이 경우, 프로그램에 변경이 생겨 "woony Hello"라는 문자열을 출력해야 하는 상황이 생겼다고 생각해봅시다.

 

그렇게되면 아래와 같도록 변경될 것입니다.

package kr.co.sample

public class Hello {

    private HelloEn helloEn;
    
    public Hello() {
    	helloEn = new HelloEn();
    	helloEn.printHello("woony");
    }
}
package kr.co.sample;

public class HelloEn {
	public void printHello(String str) {
    	System.out.println(str + "Hello");
    }
}

HelloKr 클래스를 HelloEn클래스로 바꾸어주었습니다. HelloEn 객체를 변경할 뿐인데, Hello 객체의 코드도 수정되었습니다. 이 경우에 Hello 클래스는 HelloEn(Kr)에 의존성이 있다고 표현합니다.

 

이처럼 하나의 모듈이 바뀌면 다른 모듈의 코드까지도 변경되는 경우, 객체에 의존성이 생기는 것입니다. 위의 코드에서는 바뀌는 코드가 많지 않지만, 애플리케이션의 기능이 계속해서 변화하고 추가된다면 그 때마다 의존성이 있는 코드를 변경하고 다시 컴파일하는 과정을 반복해야합니다. 그래서 기능이 변경되거나 추가되더라도 코드의 변화량이 적도록 처음부터 코드를 작성하는 것이 좋을 것입니다. 

 

그렇다면 어떻게하면 위의 코드에서 앞으로 기능의 변화가 생기더라도 코드의 변화량이 적어질까요? 

HelloKr 클래스와 HelloEn 클래스에 대한 인터페이스를 만들어 볼까요?

 

package kr.co.sample;

public interface HelloInterface {
	public void printHello(Stirng str);
}
package kr.co.sample;

public class HelloEn implements HelloInterface {
	public void printHello(String str) {
    	System.out.println(str + "Hello");
    }
}
package kr.co.sample;

public class HelloKr implements HelloInterface {
	public void printHello(String str) {
    	System.out.println(str + "안녕하세요");
    }
}
package kr.co.sample

public class Hello {

    private HelloInterface hello;
    
    public Hello() {
    	hello = new HelloKr();
    	hello.printHello("woony");
    }
}

이렇게하면 HelloEn객체로 변경하려면 생성자에서 new HelloEn(); 으로만 변경해주면 될것입니다.

하지만 이 방법도 의존성이 존재합니다. 그래서 여전히 Hello 클래스의 변경이 필요합니다. 다른 방법은 없을까요? HelloEn 이나 HelloKr 뿐만 아니라 나중에 HelloCn,HelloJa 가 추가되더라도 Hello 클래스는 변경하지 않는 방법은 없을까요?

 

이런 고민에서 출발한 생각이 IoC/DI 입니다.

다음 예제를 보죠.

 

IoC/DI가 적용된 경우

// 컨테이너
<beans>
    <bean id="HelloBean" class="kr.co.sample.HelloKr">
    <bean id="Hello" class="kr.co.sample.Hello">
        <property name="HelloBean" ref="HelloBean"/>
    </bean>
</beans>  
//애플리케이션
package kr.co.sample

public class Hello {

    private HelloBean hello;
    
    public Hello(HelloBean hello) {
    	this.hello = hello;
    }
    
    hello.printHello("woony");
}

위의 코드처럼 Hello 클래스 내부에서 사용되는 HelloBean에 대한 설정정보를 외부 컨테이너에 등록해서 컨테이너에서 객체들을 관리하는 방법을 사용한다면 Hello 클래스에서 HelloEn 클래스가 필요하다면 설정정보의 bean class 속성만 변경해주면 됩니다. Bean이라고 하는 것은 컨테이너가 관리하는 객체들을 말합니다. class는 bean의 타입을 뜻하고, id는 이름을 뜻합니다. 나중에 @Resource나 @Autowired와 같은 어노테이션을 사용할 때, 애플리케이션에서 필요한 의존관계들을 찾는 식별자역할을 합니다.

 

위의 코드처럼 IoC/DI 가 적용되는 경우, 사용할 객체를 컨테이너에 등록한 뒤, 애플리케이션 코드에서 해당 객체를 매개변수로 받아옵니다. 그리고 필드에 할당해주면 어플리케이션이 실행될 때, 동적으로 의존관계를 설정해줍니다. 어플리케이션 코드에 HelloKr이나 HelloEn과 같은 클래스 이름을 직접 명시하지 않음으로써 의존성과 결합도를 낮추면서 유연성과 확장성을 향상시킨 것입니다. 

 

DI의 유형

DI를 구현하는 패턴에는 위의 코드처럼 생성자를 이용한 방법만 존재하는 것은 아닙니다. 마틴 파울러가 저술한 Inversion of Control Containers and the Dependency Injection pattern 에서는 세가지 패턴을 소개하고 있습니다.

 

 

① 생성자를 이용한 의존성 주입

 ' 필요한 의존성을 모두 포함하는 클래스의 생성자를 만들고 그 생성자를 통해 의존성을 주입한다. '

public class Hello {

    private final HelloBean hello;
    
    public Hello(HelloBean hello) {
    	this.hello = hello;
    }
}

생성자의 매개변수로 의존성을 가지는 객체를 만들고, DI 컨테이너로부터 객체의 래퍼런스를 받는 방법입니다. 대표적인 DI 컨테이너인 Spring에서는 Spring4버전 이후로 가장 권장하는 방식입니다.

그 이유는 madplay.github.io/post/why-constructor-injection-is-better-than-field-injection

 

생성자 주입을 @Autowired를 사용하는 필드 주입보다 권장하는 하는 이유

@Autowired를 사용하는 의존성 주입보다 생성자 주입(Constructor Injection)을 더 권장하는 이유는 무엇일까?

madplay.github.io

를 참고하시면 좋을 것 같습니다. 

 

② 수정자(setter)를 이용한 의존성 주입

 ' 의존성을 입력받는 세터(setter) 메소드를 만들고 이를 통해 의존성을 주입한다. '

컨테이너에서 객체의 레퍼런스를 제공받아 인스턴스에 저장해두었다가 내부의 메소드에서 사용하는 방식입니다. Spring3 버전까지는 수정자를 이용한 의존성 주입방법을 권장하였습니다. 

 

 

public class Hello {

    private final HelloBean hello;
    
    public void setHelloLanguage(HelloBean hello) {
    	this.hello = hello;
    }
}​

 

③ 초기화 인터페이스를 이용한 의존성 주입

 의존성을 주입하는 함수를 포함한 인터페이스를 작성하고 이 인터페이스를 구현하도록 함으로써 실행시에 이를 통하여
 의존성을 주입한다.

인터페이스를 생성하고, 인터페이스를 구현한 메소드에서 객체의 레퍼런스를 제공받아 DI가 이루어지도록 합니다. 스프링에서는 지원하지 않는 방법입니다. 아래 예제는 마틴파울러의 홈페이지에 제시된 예제를 가져왔습니다.

// Avalon
public interface InjectFinder {  
    void injectFinder(MovieFinder finder);
}

class MovieLister implements InjectFinder...  
    public void injectFinder(MovieFinder finder) {
        this.finder = finder;
    }

public interface InjectFinderFilename {  
    void injectFilename (String filename);
}

class ColonMovieFinder implements MovieFinder, InjectFinderFilename......  
    public void injectFilename(String filename) {
        this.filename = filename;
    }

 

위의 3가지 방법 이외에도 스프링에서는 필드를 통한 DI 방법또한 존재합니다. 필드에 @Autowired 어노테이션을 붙여주는 것만으로도 쉽게 주입하는 방법이지만, 첫번째 방법인 생성자 주입방식이 권장되면서 현재는 권장하지 않는 DI 방식입니다. 자세한 내용은 생성자 주입방식에서 링크한 사이트에 가시면 볼 수 있습니다.

 

 

마무리

Spring Framework를 사용한다면 IoC/DI는 아주 익숙한 개념입니다. 그러나 개념에 대한 정확한 이해없이 코드를 작성한다면 새로운 문제를 마주쳤을 때, 스스로 해결하기 무척 힘들다고 생각합니다. 확실한 개념정리는 언제나 탄탄한 기본실력이 되어 창의적인 생각의 밑거름이 된다고 생각합니다. 감사합니다.

'Spring' 카테고리의 다른 글

SpringBoot JSON 형태의 날짜타입 LocalDateTime 으로 받기  (0) 2022.11.17
@Configuration  (0) 2021.01.10
Springboot 단위 테스트  (0) 2020.12.31
ORM, 그리고 JPA ( Java Persistent API )  (0) 2020.12.27
Annotation  (0) 2020.12.26