개념
- 외부에서 두 객체간의 관계를 결정해주는 디자인 패턴
- 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임시 관계를 동적으로 주입
- 유연성을 확보하고 결합도를 낮춤
의존성 주입이 필요한 이유(강한 결합의 단점)
public class Pencil {
}
public class Store {
private Pencil pencil;
public Store() {
this.pencil = new Pencil();
}
}
- 두 클래스가 강하게 결합되어 있음
- Store에서 Pencil이 아닌 Book을 팔고자 하면 Store 클래스의 코드 변경이 필요
- 즉, 의존대상이 변화하면 이에 맞게 수정해야함
- 유연성이 떨어짐
- 객체들 간의 관계가 아니라 클래스간의 관계가 맺어지고 있음
- 객체들간에 관계가 맺어졌더라면 다른 객체의 구체화된 클래스(Pencil인지 Book인지) 몰라도 인터페이스 타입으로 사용할 수 있다.
의존성 주입을 통한 문제 해결
- 의존성 주입 방법 3가지
- 생성자 주입
- 필드 주입 → @Autowired 어노테이션으로 간단하게 의존성 주입
- Setter 주입
- Spring 4부터는 생성자를 통한 주입을 권장하고 있음. (이유는 아래에 나옴)
생성자 주입 방법
public interface Product {
}
public class Pencil implements Product {
}
public class Store {
private Product product;
public Store(Product product) {
this.product = product;
}
}
- Pencil, Book, Food 등 여러 제품을 하나로 표현하기 위해 Product라는 인터페이스가 있어야한다
- Pencil이 Product 인터페이스를 구현
- Store는 생성자를 통해 외부에서 Product를 주입받아 생성
public class BeanFactory {
public void store() {
Product pencil = new Pencil();
Store store = new Store(pencil);
}
}
- Store에서 Product 객체를 주입하기 위해서는 애플리케이션 실행 시점에 필요한 객체(Bean)을 생성해야하며
- 의존성 있는 두 객체를 생성하기 위해 Pencil 이라는 객체를 만들고 그 객체를 Store에 주입시켜주는 역할을 하는 DI 컨테이너가 필요
- 이 역할을 Spring 에서 Bean Factory 가 해줌
- 제어의 역전(Inversion of Control)
-
어떠한 객체를 사용할지에 대한 책임이 Bean Factory와 같은 DI 컨테이너에게 넘어갔고, 개발자는 수동적으로 주입받는 객체를 사용
-
DI는 IoC와 같은 의미로 사용되기도 하는데, DI 가 되기 위해서는 IoC가 되어야, IoC가 되기 위해서는 DI가 되야되기 때문에 뗄레야 뗄 수 없는 관계.
-
Spring IoC컨테이너에 의한 의존성 주입은 Bean 끼리만 가능
IoC(제어의 역전)
- spring boot에서는 일일이 BeanFactory에 코딩으로 등록하는 것이 아니라 Annotation을 이용해 스프링 빈 객체로 등록
생성자 주입 방법을 권장하는 이유
- 필수적으로 사용해야하는 의존성 없이는 인스턴스 만들지 못하게 강제할 수 있기 때문
- 순환 참조를 방지할 수 있음
- 생성자 인자에 사용되는 빈을 찾음 → 빈생성자 호출
- 객체 생성 시점에 빈을 주입하는데, 서로 참조하면 객체가 생성되지 않은 상태에서 빈을 찹조하기 때문에 순환 참조 에러 발생
- 컴파일 시점에 순환참조되는 구조를 에러로 알아낼 수 있음
- 필드에 final을 선언할 수 있음(불변성)
- 필드, setter 주입은 final을 선언할 수 없음
- final을 붙이려면 클래스의 인스턴스 생성될 때 final이 붙은 필드를 반드시 초기화 해야하는데
- 필드, setter주입은 인스턴스 생성된 후에 의존성 주입하기 때문
- 유닛 테스트 가능
- 일반적으로 이렇게 테스트 불가능하다. 필드 접근제한자를 public 으로 해두면 외부에서 값을 변경할 수 있기 때문에 private으로 선언하기 때문
public class TestWithoutDIContainer {
@Test
public void test() {
Pencil pencil = new Pencil();
Store store = new Store();
}
}
- 필드 주입 방식의 테스트 코드
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RootConfig.class)
public class TestWithoutDIContainer {
@Autowired
Store store;
@Test
public void test() {
System.out.println(store);
}
}
- 생성자 주입 방식의 테스트 코드
public class TestWithoutDIContainer {
@Test
public void test() {
Product pencil = new Pencil();
Store store = new Store(pencil);
}
}
의존성 주입 장점
- 의존성이 줄어든다(유연성 강화, 코드 변경에 민감하지 않음)
- 재사용성이 높은 코드가 된다
- 분리된 클래스이기 때문에 별도로 구현하면 다른 클래스에서 재사용 가능
- 테스트하기 좋은 코드
- 분리된 클래스이기 때문에 테스트를 각각 분리해서 진행 가능
- 가독성이 높아진다
- components의 종속성을 보다 쉽게 파악할 수 있으므로 코드를 쉽게 읽을 수 있다
참고자료