본문 바로가기

JAVA/Spring

스프링 3 의존관계 주입(DI)

반응형

의존관계 주입(DI)


제어의 역전(IoC)과 의존관계 주입

객체지향적인 설계나, 디자인패턴, 컨테이너에서 동작하는 서버 기술을 사용한다면 자연스럽게 IoC를 적용하거나 그 원리로 동작하는 기술을 사용하게 될 것이다.

DaoFactory처럼 객체를 생성하고 관계를 맺어주는 등의 작업을 담당하는 기능을 일반화한 것이 스프링의 IoC 컨테이너이다.

IoC가 매우 느슨하게 정의돼서 폭넓게 사용되는 용어로 스프링을 IoC 컨테이너라고만 해서는 스프링이 제공하는 기능의 특징을 멱확하게 설명하지 못한다.

스프링이 서블릿컨테이너처럼 서버에서 동작하는 서비스 컨테이너라는 뜻인지,

단순히 IoC  개념이 적용된 템플릿 메서드 패턴을 이용해 만들어진 프레임워크인지,

또 다른 IoC특징을 지닌 기술이라는 것인지 등 파악하기 힘들다.

스프링이 제공하는 IoC 방식을 핵심을 짚어주는 의존관계주입(Dependency Injection)이라는 좀 더 의도가 명확히 드러나는 이름을 사용하기 시작했다.

지금은 의존관계 주입 컨테이너로 DI 컨테이너라고 많이 불린다.


의존관계 주입, 의존성 주입, 의존 오브젝트 주입?


런타임 의존관계 설정

의존관계

두 개의 클래스 또는 모듈이 의존관계에 잇다고 말할때는 항상 방향성을 부여해줘야 한다.

즉 누가 누구에게 의존하는 관계에 있다는 식이어야 한다.

UML 모델에서는 두 클래스의 의존관계(dependency relationship)를 점선으로 된 화살표로 표현한다.



의존하고 있다는 것은 무슨 의미?

의존한다는 건 의존대상, 여거서는 B가 변하면 그것이 A에 영향을 미친다는 뜻이다.

B의 기능이 추가되거나 변경되거나, 형식이 바뀌거나 하면 그 영향이 A로 전달된다는 것이다.

대표적인 예가 A가 B를 사용하는 경우, 예를들어 A에서 B에 정의된 메서드를 호출해서 사용하는 경우다.

이럴 땐 '사용에 대한 의존관계'있다고 말할 수 있다.

만약 B에 새로운 메서드가 추가되거나 기존 메서드의 형식이 바뀌면 A도 그에 따라 수정되거나 추가되어야 한다. 또는 B의 형식은 그대로지만 기능이 내부적으로 변경되면, 결과적으로 A의 기능이 수행되는 데도 영향을 미칠 수 있다. 이렇게 사용의 관계에 있는 경우에 A와 B는 의존관계가 있다고 말할 수 있다.

다시 말하면 의존관계에는 방향성이 있다.

A가B에 의존하고 있지만, 반대로 B는 A에 의존하지 않는다.

의존하지 않는다는 말은 B는 A의 변화에 영향을 받지 않는다는 뜻이다.


UserDao의 의존관계

UserDao가 ConnectionMaker에 의존하고있는 형태다.

UserDao는 ConnectionMaker 인터페이스에만 의존하고 있다.

따라서 ConnectionMaker 인터페이스가 변한다면 그 영향을 UserDao가 직접적으로 받게 된다.

하지만 ConnectionMaker 인터페이스를 구현한 클래스, 즉 DConnectionMaker 등이 다른 것으로 바뀌거나 내부에서 사용하는 메서드에 변화가 생겨도 UserDao에 영향을 주지 않는다.

이렇게 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 된다.

결합도가 낮다고 설명할 수 있다.

의존관계란 한쪽의 변화가 다른 쪽에 영향을 주는 것이라고 했으니, 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에서 자유로워진다.


UserDao 클래스는 ConnectionMaker 인터페이스에게만 직접 의존한다.

UserDao는 DConnectionMaker 클래스의 존재도 알지 못한다.

모델의 관점에서 보면 UserDao는 DConnectionMaker 클래스에 의존하지 않기 때문이다.

UML에서 말하는 의존관계란 이렇게 설계 모델의 관점에서 이야기하는 것이다.

그런데 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계말고, 런타임시에 오브젝트 사이에서 만들어지는 의존관계도 있다.

런타임 의존관계 또는 오브젝트 의존관계인데, 설계 시점의 의존관계가 실체화된 것이라고 볼수 있다. 런타임 의존관계는 모델링 시점의 의존관계와는 성격이 다르다.


인터페이스를 통해 설계 시점에 느슨한 의존관계를 갖는 경우에는 UserDao의 오브젝트가 런타임시에 사용할 오브젝트가 어떤 클래스로 만든 것인지 미리 알수가 없다.

개발자나 운영자가 사전에 어떤 클래스의 오브젝트를 쓸지 미리 정해놓을 수는 있지만 그것이 UserDao나 ConnectionMaker 등의 설계와 코드 속에서는 드러나지 않는 다는 말이다.

프로그램이 시작되고 UserDao 오브젝트가 만들어지고 나서 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트(dependency object)라고 말한다.


의존관계 주입은 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

UserDao는 ConnectionMaker 인터페이스라는 매우 단순한 조건만 만족하면 어떤 클래스로부터 만들어졌든 상관없이 오브젝트를 받아들이고 사용한다.


세가지 조건

1. 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.

2. 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.

3. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.


의존관계 주입의 핵심은 설계시점에는 알지 못햇던 두 오브젝트의 관계를 맺도록 도와주는 제 3의 존재가 있다는 것이다.


이 개념은 관계설정 책임을 분리하는 작업을 할때 이미 설명했다.

DI에서 말하는 제3의 존재는 바로 관계설정 책임을 가진 코드를 분리해서 만들어진 오브젝트라고 볼 수 있다.

전략패턴에 등장하는 클라이언트나 앞에서 만들었던 DaoFactory, 또 DaoFactory와 같은 작업을 일반화해서 만들어졌다는 스프링의 애플리케이션 컨텍스트, 빈팩토리, IoC컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 지닌 제 3의 존재라고 볼 수 있다.


UserDao의 의존관계

인터페이스를 사이에 두고 UserDao와 ConnectionMaker 구현 클래스 간에 의존관계를 느슨하게 만들긴 했지만, 마지막으로 남은 문제가 있다.

그것은 UserDao가 사용할 구체적인 클래스를 알고 있어야 한다는 점이다.

관계설정의 책임을 분리하기전에 UserDao 클래스의 생성자이다.

public UserDao() {

connectionMaker = new DConnectionMaker();

}


UserDao는 이미 설계 시점에서 DConnectionMaker라는 구체적인 클래스의 존재를 알고 있다.

따라서 모델링 때의 의존관계, 즉 ConnectionMaker 인터페이스의 관계뿐 아니라 런타임 의존관계, 즉 DConnectionMaker 오브젝틀 사용하겠다는 것 까지 UserDao가 결정하고 관리하고 있는 셈이다.

이 코드의 문제는 이미 런타임 시의 의존관계가 코드 속에 다 미리 결정되어 있다는 점이다.

그래서 IoC방식을 써서 UserDao로부터 런타임 의존관계를 드러내는 코드를 제거하고, 제 3의 존재에 런타임 의존관계 결정 권한을 위임한다.

그래서 최종적으로 만들어진것이 DaoFactory다.

DaoFactory는 런타임 시점에 UserDao가 사용할 ConnectionMaker 타입의 오브젝트를 결정하고 이를 생성한 후에 UserDao의 생성자 파라미터로 주입해서 UserDao가 DConnectionMaker의 오브젝트와 런타임 의존관계를 맺게해준다.

따라서 의존관계 주입의 세가지 조건을 모두 충족한다고 볼 수 있다.

이미 DaoFactory를 만든 시점에 의존관계 주입(DI)를 이용한 셈이다.

UserDao의 의존관계는 ConnectionMaker 인터페이스 뿐이다.

이것은 클래스 모델의 의존관계이므로 코드에 반영하고, 런타임 시점에서도 변경되지 않는다.

런타임 시점의 의존관계를 결정하고 만들려면 제 3의 존재가 필요하다고 했다.

DaoFactory가 그 역할을 담당한다고 해보자.

DaoFactory는 여기서 두 오브젝트 사이의 런타임 의존관계를 설정해주는 의존관계 주입 작업을 주도하는 존재이며, 동시에 IoC방식으로 오브젝트의 생성과 초기화, 제공 등의 작업을 수행하는 컨테이너다.

따라서 의존관계 주입을 담당하는 컨테이너라고 볼수 있고, 줄여서 DI 컨테이너라고 불러도 된다.

보통 DI는 그 근간이 되는 개념인 IoC와 함께 사용해서 IoC/DI 컨테이너라는 식으로 함께 사용하기도 한다.

DaoFactory는 그래서 DI 컨테이너이다.

DI 컨테이너는 UserDao를 만드는 시점에서 생성자의 파라미터로 이미 만들어진 DConnectionMaker의 오브젝트를 전달한다.

정확히는 DConnectionMaker 오브젝트의 레퍼런스가 전달되는 것이다.

주입이라는 건 외부에서 내부로 무엇인가를 넘겨줘야 하는 것인데, 자바에서 오브젝트에 무엇인가를 넣어준다는 개념은 메서드를 실행하면서 파라미터로 오브젝트의 레퍼런스를 전달해주는 방법 뿐이다.

가장 쉽게 사용할 수 있는 파라미터 전달이 가능한 메서드는 바로 생성자다.

DI 컨테이너는 자신이 결정한 의존관계를 맺어줄 클래스의 오브젝트를 만들고 이 생성자의 파라미터로 오브젝트의 레퍼런스를 전달해준다.

public class UserDao {

private ConnectionMaker connectionMaker;


public UserDao(ConnectionMaker connectionMaker) {

this.connectionMaker = connectionMaker;

}

}


위코드가 이 과정의 작업을 위한 필요한 전형적인 코드다.

이렇게 생성자 파라미터를 통해 전달받은 런타임 의존관계를 갖는 오브젝트는 인스턴스 변수에 저장해둔다.

이렇게 해서 두 개의 오브젝트 간에 런타임 의존관계가 만들어졌다.

UserDao 오브젝트는 이제 생성자를 통해 주입받는 DConnectionMaker 오브젝를 언제든지 사용하면된다.

이렇게 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 레퍼런스를 전달받는 과정이 마치 메서드(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다고 해서 이를 의존관계 주입이라고 부른다.

이런 객체에 대한 런타임 의존관계 주입과 그것을 발생하는 런타임 사용 의존관계의 모습을 보여준다.


DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다.

스프링 컨테이너의 IoC는 주로 의존관계 주입 또는 DI라는 데 초점이 맞춰져있다.

그래서 스프링을 Ioc컨테이너 외에도 DI컨테이너 또는 DI 프레임워크라고 부르는 것이다.


의존관계 검색과 주입

스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아니다.

코드에서 구체적인 클래스에 의존하지 않고, 런타임시에 의존관계를 결정한다는 점에서 의존관계 주입과 비슷하지만, 의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용하기 때문에 의존관계 검색(dependency lookup)이라고 불리는 것도 있다.

의존관계 검색은 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다.

물론 자신이 어떤 클래스의 오브젝트를 이용할지 결정하지는 않는다.

그러면 IoC라고 할수는 없을 것이다.

의존관계 검색은 런타임 시 의존관계를 맺는 오브젝트를 결정하는 것과 오브젝트의 생성 작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메서드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.


public UserDao() {

DaoFactory daoFactory = new DaoFactory();

this.connectionMaker = daoFactory.connectionMaker();

}


이렇게 해도 UserDao는 여전히 자신이 어떤 ConnectionMaker 오브젝트를 사용할지 미리 알지 못한다. 여전히 코드의 의존대상은 ConnectionMaker 인터페이스뿐이다.

런타임 시에 DaoFactory가 만들어서 돌려주는 오브젝트와 다이나믹하게 런타임 의존관계를 맺는다. 따라서 IoC 개념을 잘 따르고 있으며, 그 혜택을 받고 있는 코드다.

하지만 적용하는 방법은 외부로부터의 주입이 아니라 스스로 IoC 컨테이너인 DaoFactory에게 요청하는 것이다.

DaoFactory의 경우라면 미리 준비된 메서드를 호출하면 되니까 단순히 요청으로 보이겠지만, 이런 작업을 일반화한 스프링의 애플리케이션 컨텍스트라면 미리 정해놓은 이름을 전달해서 그 이름에 해당하는 오브젝트를 찾게 된다.

따라서 이를 일종의 검색이라고 볼 수 있다.

또한 그 대상이 런타임 의존관계를 가질 오브젝트이므로 의존관계 검색이라고 부르는 것이다.

스프링의 IoC 컨테이너인 애플리케이션 컨텍스트는 getBea()이라는 메서드를 제공한다.

바로 이 메서드가 의존관계 검색에 사용되는 것이다.

UserDao는 애플리케이션 컨텍스트를 사용해서 의존관계 검색 방식으로 ConnectionMaker오브젝트를 가져오게 만들수도 있다.


public UserDao() {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);

}

의존관계 검색은 기존 의존관계 주입의 거의 모든 장점을 갖고 있다.

Ioc 원칙에도 잘 들어맞는다. 단, 방법만 조금 다를 뿐이다.


그렇다면 의존관계 검색과 앞에서 살펴봣던 의존관계 주입방법 중 어떤 것이 더 나을까?

의존관계 주입쪽이 훨신 단순하고 깔끔하다.

의존관계 검색방법은 코드 안에 오브젝트 팩토리 클래스나 스프링 API가 나타난다.

애플리케이션 컴포넌트가 컨테이너와 같이 성격이 다른 오브젝트에 의존하게 되는 것이므로 그다지 바람직하지 않다.

사용자에 대한 DB 정보를 어떻게 가져올 것인가에 집중해야 하는 UserDao에서 스프링이나 오브젝트 팩토리를 만들고 API를 이용하는 코드가 섞여 있는게 어색하다.

따라서 대개는 의존관계 주입 방식을 사용하는 편이 낫다.


그런데 의존관계 검색방식을 사용해야 할때가 있다.

앞서 만들었던 테스트 코드인 UserDaoTest를 보자.

테스트 코드에서는 이미 의존관계 검색 방식인 getBean()을 사용했다.

스프링의 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션의 기동시점에서 적어도 한번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다.

스태틱 메서드인 main()에서 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문이다.

서버에서도 마찬가지다.

서버에는 main()과 같은 기동 메서드는 없지만, 사용자의 요청을 받을때마다 main() 메서드와 비슷한 역할을 하는 서블릿에서 스프링 컨테이너에 담긴 오브젝트를 사용하려면 한 번의 의존관계 검색방식을 사용해 오브젝트를 가져와야한다.

다행이 이런 서블릿은 스프링이 미리 만들어서 제공하기 때문에 직접 구현할 필요는 없다.

의존관계 검색과 의존관계 주입을 적용할 때 발견할 수 있는 중요한 차이점이 하나있다.

의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다는 점이다.

UserDao에 스프링의 getBane()을 사용한 의존관계 검색방법을 적용했다고 해보자.

이 경우 UserDao는 굳이 스프링이 만들고 관리하는 빈일 필요가 없다. 그냥 어딘가에서 직접 new UserDao() 해서 만들어서 사용해도 된다.

이때는 ConnectionMaker만 스프링의 빈이기만 하면된다.

반면에 의존관계 주입에서는 UserDao와 ConnectionMaker 사이에 DI가 적용되려면 UserDao도 반드시 컨테이너가 만드는 빈 오브젝트여야 한다.

컨테이너가 UserDao에 ConnectionMaker 오브젝트를 주입해주려면 UserDao에 대한 생성과 초기화 권한을 갖고 있어야 하고, 그러려면 UserDao는 IoC방식으로 컨테이너에서 생성되는 오브젝트, 즉 빈이어야 하기 때문이다. 이런점에서 DI와 DL(의존관계 검색)은 적용 방법에 차이가 있다.

DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 되어야 한다는 사실을 잊지말자.


DI받는다

DI의 동작방식은 이름 그대로 외부로부터의 주입이다.

하지만 단지 외부에서 파라미터로 오브젝트를 넘겨줬다고 해서, 즉 주입해줬다고 해서 다 DI가 아니라는 점을 주의해야 한다.

주입받는 메서드 파라미터가 이미 특정 클래스 타입으로 고정되어있다면 DI가 일어날 수 없다.

DI에서 말하는 주입은 다이나믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이뤄져야한다.

그래서 DI원리를 지키며 외부에서 오브젝트를 제공받는 방법을 단순히'주입받는다'라고 하는 대신 'DI 받는다'라고 표현한다.

단순히 오브젝트 주입이 아니라 DI 개념을 따르는 주입임을 강조하는 것이라고 생각하면 좋다

반응형