DI (Dependency Injection) : 의존성 주입

Moondy·2022년 5월 13일
0

개념

  • 외부에서 두 객체간의 관계를 결정해주는 디자인 패턴
  • 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임시 관계를 동적으로 주입
  • 유연성을 확보하고 결합도를 낮춤

의존성 주입이 필요한 이유(강한 결합의 단점)

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();
          //접근제한자가 public일 때만 이렇게 가능
          }
      }
    • 필드 주입 방식의 테스트 코드
      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration(classes = RootConfig.class)
      public class TestWithoutDIContainer {
      		@Autowired
      		Store store;
      		// DI 컨테이너에 의해 내부 Product 필드에 의존성 주입가능
      
          @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의 종속성을 보다 쉽게 파악할 수 있으므로 코드를 쉽게 읽을 수 있다

참고자료

profile
DevOps를 살짝 찍먹하는 BackEnd 개발자

0개의 댓글