의존성(관계) 주입(DI)
DI
오브젝트 레퍼런스를 외부로부터 제공(주입)받고 이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심이다. 의도를 강하게 드러내면 ‘의존관계 설정’이라고 표현할 수 있다.
의존관계
- 두 개의 클래스 또는 모듈이 의존관계에 있다고 말할 때는 항상 방향성을 부여한다. 의존한다는 것은 의존대상이 변하면 그것이 의존하고 있는 클래스 또는 모듈에 영향을 미친다는 뜻이다. 따라서 의존하는 대상은 변화에 따라 수정혹은 추가해야 한다.
- 반대로 의존하지 않는다는 말은 변화에 영향을 받지 않는다는 뜻이다.
- 인터페이스를 통해 의존관계를 제한하면 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받을 수 있다.
- 의존 오브젝트
런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트라고 말한다. - 의존관계 주입은 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체(클라이언트 오브젝트)를 런타임 시에 연결해주는 작업을 말한다.
- 의존관계 주입 정리
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
-
의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
핵심은 설계 시점에 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것
의존관계주입 동작 과정
의존관계가 필요한 오브젝트를 만드는 시점에서 생성자의 파라미터로 이미 만들어진 요구된 오브젝트를 전달한다. 정확히 말하면 요구된 오브젝트의 레퍼런스를 전달되는 것이다.
주입이라는 건 외부에서 내부로 무엇인가를 넘겨줘야 하는 것인데, 자바에서 오브젝트에 무엇인가를 넣어준다는 개념은 메소드를 실행하면서 파라미터로 오브젝트의 레퍼런스를 전달해주는 방법뿐이다. 가장 손쉽게 사용할 수 있는 파라미터 전달이 가능한 메소드는 바로 생성자다.
DI 컨테이너는 자신이 결정한 의존관계를 맺어줄 클래스의 오브젝트를 만들고 이 생성자의 파라미터로 오브젝트의 레퍼런스를 전달해준다.
DI컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 레퍼런스를 전달받는 과정이 마치 메소드를 통해 DI 컨테이너가 주입해주는 것과 같다고 해서 이를 의존관계 주입이라고 부른다.
의존관계검색
// UserDao 생성자
public UserDao() {
AnnotaionConfigApplicationContext context =
new AnnotationConfigApplicationContext(DaoFactory.class);
this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}
의존관계검색은 애플리케이션 컨텍스트의 getBean()이라는 메소드를 이용해 의존하는 오브젝트를 검색하여 직접 가져오는 방식이다.
의존관계 주입 방법이 코드가 더 깔끔하고 단순하다. 의존관계 검색은 애플리케이션 컴포넌트가 컨테이너와 같이 성격이 다른 오브젝트에 의존하게 되는 것이므로 그다지 바람직하지 않다.
의존관계 검색과 의존관계 주입의 가장 큰 차이점은 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다는 점이다.
DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다.
마이크로 DI (수동 DI)
의존관계 주입은 다양한 형태로 적용될 수 있다. DI의 가장 중요한 개념은 제3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다.
일반적인 DI는 의존관계에 있는 두 개의 오브젝트와 이 관계를 다이내믹하게 설정해주는 오브젝트 팩토리 그리고 이를 사용하는 클라이언트라는 4개의 오브젝트 사이에서 일어난다.
하지만 원시적인 전략 패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 함께 지고 있을 수도 있다. 또는 클라이언트와 전략(의존 오브젝트)이 결합될 수도 있다. 심지어는 클라이언트와 DI관계에 있는 두 개의 오브젝트가 모두 하나의 클래스 안에 담길 수도 있다.
이런 경우에는 DI가 매우 작은 단위의 코드와 메소드 사이에서 일어나기도 한다. DI의 장점을 단순화해서 IoC컨테이너 없이 코드 내에서 적용한 경우를 마이크로 DI라고도 한다. 또는 코드에 의한 DI라는 의미로 수동 DI라고 부를 수 있다.
JdbcContext의 특별한 DI
JdbcContext와 UserDao 사이에는 인터페이스를 적용하지 않고 DI를 적용했다(직접 의존하는 관계). 스프링의 DI의 기본 의도에 맞게 JdbcContext의 메소도를 인터페이스로 뽑아내어 정의해두고, 이를 UserDao에서 사용하게 해야 하지 않을까? 그렇게 해도 상관은 없지만 꼭 그럴 필요는 없다.
스프링의 DI를 넓게 보면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄한다. 이런 의미에서 JdbcContext가 UserDao 객체에서 사용하게 주입했다는 사실은 DI의 기본을 따른다고 볼 수 있다.
인터페이스의 미사용으로 클래스의 변경에서 자유롭지 않지만 JdbcContext를 DI하는 이유는 첫째는 JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤빈이 되기 때문이다. 상태정보가 없으며 읽기전용의 오브젝트만 가진 JdbcContext는 싱글톤이 되기에 문제가 없다. JdbcContext는 컨텍스트 메소드를 제공해주는 서비스 오브젝트로서 의미가 있기에 여러 오브젝트에서 싱글톤으로 공유하여 사용하는 것이 이상적이다. 둘째로 JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다. 이 이유가 가장 중요하다. DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링에 등록돼야 한다. 스프링이 생성하고 관리하는 IoC대상이어야 DI에 참여할 수 있다. 따라서 JdbcContext는 다른 빈을 DI받기 위해서라도 스프링 빈으로 등록돼야 한다.
단, 이런 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항임을 잊지 말자. 그저 인터페이스를 만들기 귀찮아서 그냥 클래스를 사용하면 안된다. 굳이 원한다면 JdbcContext와 UserDao 사이에 인터페이스를 만들어도 문제 될 것은 없다.
코드를 이용한 수동 DI
UserDao가 JdbcContext의 생성과 초기화를 책임지는 방식이다. 하지만 JdbcContext는 다른 스프링 빈에 의존하고 있다. 스프링의 빈이 아닌 JdbcContext는 DI까지 UserDao에게 위임함으로써 해결할 수 있다. 오브젝트를 생성하고 그 의존 오브젝트를 수정자 메소드를 통해 주입하는 DI의 동작원리를 그대로 모방하면 된다. UserDao는 DataSource 반을 필요로 하지 않지만 JdbcContext를 위해 주입받게 된다.
결론
두 방식 모드 장단점을 갖는다. 특별한 DI는 오브젝트 사이의 실제 의존관계가 설정파일에 명확히 드러난다는 장점이 있지만 DI의 근본적인 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출된다는 단점이 있다.
수동으로 DI하는 방법은 내부에서 만들어지고 사용되면서 그 관계를 외부에 드러내지 않는다는 장점이 있다. 하지만 여러 오브젝트가 사용하더라도 싱글톤으로 만들 수 없고, DI 작업을 위한 부가적인 코드가 필요하다는 단점이 있다.
출처
토비의 스프링(책)
댓글남기기