DI가 뭘까?

홍당무·2023년 2월 22일
0
post-thumbnail

DI (Dependency Injection)

정의

소프트웨어 엔지니어링에서 의존성 주입(dependency injection)은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다. "의존성"은 예를 들어 서비스로 사용할 수 있는 객체이다. 클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 말해주는 것이다. "주입"은 의존성(서비스)을 사용하려는 객체(클라이언트)로 전달하는 것을 의미한다. 서비스는 클라이언트 상태의 일부이다. 클라이언트가 서비스를 구축하거나 찾는 것을 허용하는 대신 클라이언트에게 서비스를 전달하는 것이 패턴의 기본 요건이다.

의존성 주입의 의도는 객체의 생성과 사용의 관심을 분리하는 것이다. 이는 가독성과 코드 재사용을 높혀준다.
의존성 주입은 광범위한 역제어 테크닉의 한 형태이다. 어떤 서비스를 호출하려는 클라이언트는 그 서비스가 어떻게 구성되었는지 알지 못해야 한다. 클라이언트는 대신 서비스 제공에 대한 책임을 외부 코드(주입자)로 위임한다. 클라이언트는 주입자 코드를 호출할 수 없다. 그 다음, 주입자는 이미 존재하거나 주입자에 의해 구성되었을 서비스를 클라이언트로 주입(전달)한다. 그리고 나서 클라이언트는 서비스를 사용한다. 이는 클라이언트가 주입자와 서비스 구성 방식 또는 사용중인 실제 서비스에 대해 알 필요가 없음을 의미한다. 클라이언트는 서비스의 사용 방식을 정의하고 있는 서비스의 고유한 인터페이스에 대해서만 알면 된다. 이것은 "구성"의 책임으로부터 "사용"의 책임을 구분한다.

메인 모듈이 '직접' 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자(dependency injector)가 이 부분을 가로채 메인 모듈이 '간접'적으로 의존성을 주입하는 방식

예시

A가 B에 의존한다. = B가 변하면 A에 영향을 미치는 관계 = A -> B를 의미하며 코드로는 이러한 것을 A가 B에 의존한다고 한다.

class Fruit {
    public void fruitSection() {
        System.out.println("과일 판매 코너입니다.");
    }
}

class Snack {
    public void snackSection() {
        System.out.println("간식 판매 코너입니다.");
    }
}
public class Store {
    private final Fruit fruit;
    private final Snack snack;


    public Store(Fruit fruit, Snack snack) {
        this.fruit = fruit;
        this.snack = snack;
    }

    public void StoreGuide() {
        fruit.fruitSection();
        snack.snackSection();
    }

    public static void main(String[] args) {
        Store a = new Store(new Fruit(), new Snack());
        a.StoreGuide();
    }
}

간단한 예시로 가게안의 구역을 소개하는 코드를 만들어 보았다. 예시처럼 Store 는 Fruit, Snack에 의존한다. Fruit, Snack의 값이 바뀌면 Store의 값도 변경되는 것이다.

이를 DI를 적용시키게 되면 이렇다.

의존관계역전원칙 : 의존성 주입을 할 때는 의존관계원칙(Dependency Inversion Principle)이 적용된다. 이는 2가지 규칙을 지키는 상태를 일컫는다.
1.상위 모듈은 하위모듈에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다.
2.추상화는 세부사항에 의존해서는 안 된다. 세부 사항은 추상화에 따라 달라져야 한다.

import java.util.ArrayList;
import java.util.List;

interface Product {
    void goods();
}

class Fruit implements Product {

    @Override
    public void goods() {
        fruitSection();
    }

    public void fruitSection() {
        System.out.println("과일 판매 코너입니다.");
    }
}
class Snack implements Product {

    @Override
    public void goods() {
        snackSection();
    }

    public void snackSection() {
        System.out.println("과일 판매 코너입니다.");
    }
}
public class Store {
    private final List<Product> products;

    public Store(List<Product> products) {
        this.products = products;
    }

    public void storeGuide() {
        products.forEach(Product::goods);
    }

    public static void main(String[] args) {
        List<Product> productList = new ArrayList<>();
        productList.add(new Fruit());
        productList.add(new Snack());
        Store a = new Store(productList);
        a.storeGuide();
    }
}

이 때, Store는 Fruit, Snack에 의존하는 것이 아닌 Product라는 인터페이스를 둬서 Fruit, Snack 각각이 Product에 의존하게끔 만든 것이다. 즉, 의존관계역전원칙이 적용되었다.

DI 구현 3가지 방법

1. 필드 주입

클래스 내부 필드에 직접 의존성을 주입하는 방식이며 변수 선언부에 @Autowired 어노테이션을 사용해 의존성을 주입받는다.

public class Store {
	@Autowired
    private List<Product> products;

다만 코드의 간결성을 얻는 대신 외부에서 변경이 힘들다는 단점을 가지고 있다.

또한 프레임워크에 의존적으로 객체지향적으로 좋지 않다.

2. 생성자 주입

클래스의 생성자가 하나이고, 그 생성자로 주입받을 객체가 빈으로 등록되어 있다면 @Autowired를 생략 할 수 있다.

public class Store {
	// final을 붙일 수 있다.
    private final List<Product> products;
    
    //@Autowired
    public Store(List<Product> products) {
        this.products = products;
    }

3. 수정자 주입(세터 주입)

Setter 메소드에 @Autowired 어노테이션을 붙이는 방법이다.

public class Store {

    private List<Product> products;
    
    @Autowired
    public void storeGuide() {
        products.forEach(Product::goods);

단점으로는 세터 주입을 사용하면 set 메서드를 public으로 열어두어야 하기 때문에 언제 어디서든 변경이 가능하다.

어떤 주입 방식이 좋을까 ?

Spring Framwork reference에서 권장하는 방법은 생성자를 통한 주입이다. 그 이유로는

1. 순환 참조를 방지할 수 있다.

  • 생성자를 통해 주입하고 실행하면 BeanCurrentlyInCreationException이 발생하게 된다.
  • 순환 참조 뿐만아니라 더 나아가서 의존 관계에 내용을 외부로 노출 시킴으로써 어플리케이션을 실행하는 시점에서 오류를 체크할 수 있다.

2. 불변성(Immutability)

  • 생성자로 의존성을 주입할 때 final로 선언할 수 있고, 이로인해 런타임에서 의존성을 주입받는 객체가 변할 일이 없어지게 된다.
    하지만 수정자 주입이나 일반 메소드 주입을 이용하게되면 불필요하게 수정의 가능성을 열어두게 되고, 이는 OOP의 5가지 원칙 중 OCP(Open-Closed Principal, 개방-폐쇄의 원칙)를 위반하게 됩니다. 그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋습니다. 또한, 필드 주입 방식은 null이 만들어질 가능성이 있는데, final로 선언한 생성자 주입 방식은 null이 불가능합니다.

3. 테스트 용이

  • DI의 핵심은 관리되는 클래스가 DI 컨테이너에 의존성이 없어야 한다는 것이다.
  • 즉, 독립적으로 인스턴스화가 가능한 POJO(Plain Old Java Object)여야 한다는 것이다.
profile
공부하는 백엔드 개발자

0개의 댓글