Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags more
Archives
Today
Total
관리 메뉴

ultra_dev

DI(Dependency Injection) 의존 관계 주입 본문

Computer Science

DI(Dependency Injection) 의존 관계 주입

ultra_dev 2023. 3. 28. 11:35

DI란 스프링이 다른 프레임워크와 차별화되어 제공하는 의존 관계 주입 기능으로, 객체를 직접 생성하는 게 아니라 외부에서 생성한 후 주입 시켜주는 방식이다.

 

DI(의존성 주입)를 통해서 모듈 간의 결합도가 낮아지고 유연성이 높아진다. DI의 방법으로는 필드 주입, Setter 주입, 생성자 주입이 있다. 이 중 생성자 주입 방식이 추천된다.

 

final 키워드를 사용할 수 있어서 컴파일 시점에 누락된 의존성을 확인 가능하고, 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하며, 객체의 생성과 조립(의존관계 주입)이 동시에 실행되다 보니 순환참조에러를 사전에 잡을 수 있다는 장점이 있기 때문이다.

 

유사한 개념으로 IOC(INVERSION OF CONTROL)가 있다.

 

IoC란 "제어의 역전" 이라는 의미로, 메소드나 객체의 호출 작업을 개발자가 결정하는 것이 아니라, 외부에서 결정되는 것을 의미한다.

 

객체의 의존성을 역전 시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있게 한다.

 

즉, IoC는 프로그램 제어권을 역전시키는 개념이고, DI는 해당 개념을 구현하기 위해 사용하는 디자인 패턴 중 하나로, 이름 그대로 객체의 의존관계를 외부에서 주입시키는 패턴이라고 할 수 있다.



DI안하는 경우와 비교

📌 외부에서 의존 관계 주입을 안하는 경우의 의존관계(Not DI)


class CafeManager {
    private CoffeeRecipe coffeeRecipe;

    public CafeManager() {
        coffeeRecipe = new CoffeeRecipe();        
    }
}

이렇게 매니저가 커피 레시피에 의존하는 걸 의존관계라고 한다.

커피 레시피가 변하면 이에 맞춰서 매니저도 커피 만드는 방법을 수정해야 하고,

레시피 변화가 매니저의 행위에 영향을 미치니 매니저는 레시피에 의존한다고 하는 것이다.

 

위처럼 coffeeRecipe에만 의존하는 거는 너무 확장성이 없다.

다양한 커피 레시피를 의존하고 싶은 경우는 의존관계를 인터페이스로 추상화해야 한다.

 

class CafeManager {
    private CoffeeRecipe coffeeRecipe;

    public CafeManager() {
        coffeeRecipe = new LatteRecipe();
        //coffeeRecipe = new IceCoffeeRecipe();
        //coffeeRecipe = new HotCoffeegerRecipe();
    }
}

interface CoffeeRecipe {
    newCoffee();
    // 이외의 다양한 메소드
} 

class IceCoffeeRecipe implements CoffeeRecipe {
    public Coffee newCoffee() {
        return new IceCoffee();
    }
    // ...
}

의존관계를 인터페이스로 추상화하게 되면,

더 다양한 의존 관계를 맺을 수가 있고,

실제 구현 클래스와의 관계가 느슨해지고, 결합도가 낮아진다.


📌 그렇다면 Dependency Injection은 무엇인가?

 

위에서는 CafeManager 내부적으로 의존관계인 CoffeeRecipe가 어떤 값을 가질지 직접 정하고 있다.

 

만약 어떤 CoffeeRecipe를 만들지를 매니저가 정하는 상황을 상상해본다면,

CafeManager가 의존하고 있는 CoffeeRecipe를 외부(CafeOwner)에서 결정하고 주입하는 것이다.

이처럼 그 의존관계를 외부에서 결정하고 주입하는 것이 DI(의존관계 주입)이다.

DI 장점

1. 의존성이 줄어든다.

의존한다는 것은 그 의존대상의 변화에 취약하다는 것이다.(대상이 변화하였을 때, 이에 맞게 수정해야함)

DI로 구현하게 되었을 때, 주입받는 대상이 변하더라도 그 구현 자체를 수정할 일이 없거나 줄어들게됨.

2. 재사용성이 높은 코드가 된다.

기존처럼 CafeManger내부에서만 사용하던 CoffeeRecipe를 따로 분리하면 다른 클래스에서 재사용할 수가 있다.

3. 테스트하기 좋은 코드가 된다.

CoffeeRecipe의 테스트를 CafeManger테스트와 분리하여 진행할 수 있다.

4. 가독성이 높아진다.

CoffeeRecipe의 기능들을 별도로 분리하게 되어 자연스레 가동성이 높아진다.

DI 구현 방법

DI는 의존관계를 외부에서 결정하는 것이기 때문에, 클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법이다. 런타임 시점의 의존관계를 외부에서 주입하여 DI 구현이 완성된다.

CafeOwner가 어떤 레시피를 주입하는지 결정하는 예시로 설명

  • 생성자를 이용
class CafeManger {
    private CoffeRecipe coffeRecipe;

    public CafeManger(CoffeRecipe coffeRecipe) {
        this.coffeRecipe = coffeRecipe;
    }
}

class CafeOwner {
    private CafeManger cafeManger = new CafeManger(new IceCoffeeRecipe());

    public void changeMenu() {
        cafeManger = new CafeManger(new hotCoffeeRecipe());
    }
}
  • 메소드를 이용 (대표적으로 Setter 메소드)
class CafeManager {
    private CoffeeRecipe coffeeRecipe = new HotCoffeeRecipe();

    public void setCoffeeRecipe(CoffeeRecipe coffeeRecipe) {
        this.coffeeRecipe = coffeeRecipe;
    }
}

class CafeOwner {
    private BurgerChef burgerChef = new BurgerChef();

    public void changeMenu() {
        cafeManager.setCoffeeRecipe(new IceCoffeeRecipe());
    }
}


1. 생성자 주입(Constructor Injection)

 
@Controller
 
public class CocoController {
 
  //final을 붙일 수 있음
 
    private final CocoService cocoService;
 
  //---------------------------------------------------------
 
  //@Autowired
 
    public CocoController(CocoService cocoService) {
 
        this.cocoService = cocoService;
 
    }
 
}

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


2. 필드 주입(Field Injection)

 
@Controller
 
public class CocoController {
 
 
 
    @Autowired
 
    private CocoService cocoService;
 
}

필드에 @Autowired 어노테이션만 붙여주면 자동으로 의존성 주입된다.
사용법이 매우 간단하기 때문에 가장 많이 접할 수 있는 방법이다.
 
단점

  • 코드가 간결하지만, 외부에서 변경하기 힘들다.
  • 프레임워크에 의존적이고 객체지향적으로 좋지 않다.

3. 수정자 주입(Setter Injection)

 
@Controller
 
public class CocoController {
 
    private CocoService cocoService;
 
    
 
    @Autowired
 
    public void setCocoService(CocoService cocoService) {
 
     this.cocoService = cocoService;
 
    }
 
}

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

  • 수정자 주입을 사용하면 setXXX 메서드를 public으로 열어두어야 하기 때문에 언제 어디서든 변경이 가능하다.

어떤 주입 방식을 사용하는게 좋을까?


Spring Framwork reference에서 권장하는 방법은 생성자를 통한 주입이다.
@Autowired 어노테이션만으로 간단하게 의존성을 주입할 수 있는데 왜 생성자 주입 방법을 권장하는걸까?
 

생성자 주입을 권장하는 이유

 

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

개발을 하다 보면 여러 컴포넌트 간에 의존성이 생긴다.
예를 들어, A가 B를 참조하고, B가 다시 A를 참조하는 순환 참조되는 코드가 있다고 가정

 
@Service
 
public class AService {
 
 
 
    // 순환 참조
 
    @Autowired
 
    private BService bService;
 
 
 
    public void HelloA() {
 
        bService.HelloB();
 
    }
 
}
 
@Service
 
public class BService {
 
    
 
    // 순환 참조
 
    @Autowired
 
    private AService aService;
 
 
 
    public void HelloB() {
 
        aService.HelloA();
 
    }
 
}

필드 주입과 수정자 주입은 빈이 생성된 후에 참조를 하기 때문에 어플리케이션이 아무런 오류 그리고 경고 없이 구동된다.
그리고 그것은 실제 코드가 호출될 때까지 문제를 알 수 없다
반면, 생성자를 통해 주입하고 실행하면 BeanCurrentlyInCreationException이 발생하게 된다.
순환 참조 뿐만아니라 더 나아가서 의존 관계에 내용을 외부로 노출 시킴으로써 어플리케이션을 실행하는 시점에서 오류를 체크할 수 있다
 

2. 불변성(Immutability)

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

 
@Controller
 
public class CocoController {
 
 
 
    private final CocoService cocoService;
 
 
 
    public CocoController(CocoService cocoService) {
 
        this.cocoService = cocoService;
 
    }
 
}

 

3. 테스트에 용이하다.

생성자 주입을 사용하게 되면 테스트 코드를 좀 더 편리하게 작성할 수 있습니다.

 
public class CocoService {
 
    private final CocoRepository cocoRepository;
 
    
 
    public CocoService(CocoRepository cocoRepository) {
 
     this.cocoRepository = cocoRepository;
 
    }
 
    
 
    public void doSomething() {
 
        // do Something with cocoRepository
 
    }
 
}
 
 
 
public class CocoServiceTest {
 
    CocoRepository cocoRepository = new CocoRepository();
 
    CocoService cocoService = new CocoService(cocoRepository);
 
    
 
    @Test
 
    public void testDoSomething() {
 
        cocoService.doSomething();
 
    }
 
}


위와 같은 이유로 필드 주입이나 수정자 주입 보다는 생성자 주입의 사용을 권장한다.

 

출처 : https://dev-coco.tistory.com/70

 

 

 

'Computer Science' 카테고리의 다른 글

제네릭이란  (0) 2023.03.29
MSA(Micro Service Architecture)  (0) 2023.03.29
인덱스(INDEX)에 대해  (0) 2023.03.28
REST API  (0) 2023.03.27
객체지향프로그래밍(Object-Oriented-Programming)  (0) 2023.03.27
Comments