본문 바로가기

Spring

의존관계 주입(Dependency Injection)

  의존관계 주입은 영어로 Dependency Injection으로 번역되는데,

  스프링은 DI 컨테이너라고 불릴 정도로,

  스프링을 이해하는데 의존 관계 주입은 중요한 개념입니다. 

 

  이 글에서는 

  1) 의존 관계란?

  2) 의존 관계 주입이란? (feat. OCP, DIP) 

  3) 여러 가지 의존 관계 주입 방식 

  에 대해서 살펴보겠습니다. 

 

1) 의존 관계란?

- 의존 관계란 A, B라는 모듈이 있을 때, B의 변화가 A에 영향을 미치면 'A가 B에 의존한다'고 합니다. 

   예시 코드를 통해서 살펴보겠습니다. 

@Component
public class OrderServiceImpl implements OrderService {

      private final MemberRepository memberRepository;
      private final DiscountPolicy discountPolicy;
      
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
          this.memberRepository = memberRepository;
          this.discountPolicy = discountPolicy;
      }
}

 - 위의 코드를 보면 OrderServiceImpl 클래스의 생성자의 인자로

   MemberRepository 인터페이스, DiscountPolicy 인터페이스가 전달되었습니다.

   이는 OrderServiceImpl 클래스가 MemberRepository 인터페이스, DiscountPolicy 인터페이스에

   의존한다는 것을 의미합니다.

 -> 즉, MemberRepository 인터페이스와 DiscountPolicy 인터페이스에 변화가 생기면,

     OrderServiceImpl 클래스에 영향을 미칩니다. 

 

 

2) 의존 관계 주입이란?

- 위의 코드에서 OrderServiceImpl 클래스의 생성자의 인자로 MemberRepository 인터페이스와,

  DiscountPolicy 인터페이스가 전달된 것을 곧 '의존 관계 주입'이라고 합니다.

  그렇다면 의존 관계 주입을 하는 이유가 무엇일까요?

 

 흔히들 MVC 프레임워크라는 개념을 들어보셨을 것입니다.

 MVC 프레임워크란 Model-View-Controller로 모듈을 분리하여 개발하는 것을 의미합니다.

 MVC 프레임워크의 장점은 각각의 모듈을 독립적으로 관리함에 따라 유지보수가 편리해진다는 점입니다. 

  이 때, 각 모듈들은 독립적으로 존재함과 동시에 의존 관계를 맺어야 합니다.

  예를 들어, 스프링 MVC 프레임워크는 사용자 요청을 다음과 같은 흐름으로 처리합니다. 

 

사용자 요청이 Controller 클래스를 통해서 전달이 되면,

Controller 클래스는 비즈니스 로직을 담당하고 있는 Service 클래스를 호출해야 합니다.

그렇게 하려면, Controller 클래스에 Service 클래스의 객체를 지녀야 합니다.

-> 이와 같이 독립적인 모듈들 간의 의존 관계를 설정하는 작업을 의존 관계 주입이라고 합니다. 

  

 

2-1) 의존 관계 주입과 OCP, DIP 

- 의존 관계 주입은 객체 지향 SOLID 원칙의 OCP, DIP와도 연관이 있습니다. 

 

  OCP는 소프트웨어 개체는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다는 원칙입니다. 

  DIP는 의존 관계 설정 시, 추상적인 것에 의존하고, 구체적인 것에 의존하지 않아야 한다는 원칙입니다. 

 

  그럼 의존 관계 주입과 OCP, DIP가 어떻게 연관이 있는지를 설명드리겠습니다.

  위의 예시 코드를 다시 한 번 참고하겠습니다. 

@Component
public class OrderServiceImpl implements OrderService {

      private final MemberRepository memberRepository;
      private final DiscountPolicy discountPolicy;
      
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
          this.memberRepository = memberRepository;
          this.discountPolicy = discountPolicy;
      }
}

- 제가 위에서 설명드릴 때, MemberRepository '인터페이스', DiscountPolicy '인터페이스'라고 설명드렸습니다.

  즉, OrderServiceImpl 클래스는 2개의 인터페이스에 의존하고 있습니다.  

 

  그런데 인터페이스는 객체를 생성할 수 없습니다.

  따라서 OrderServiceImpl 클래스에서 MemberRepository를 호출하려면,

  생성자가 호출되는 시점에 구체적인 객체가 할당되어야 합니다.

  여기서는 우선 구체적인 객체를 MemoryMemberRepository 클래스의 객체라고 하겠습니다.

  그리고 이와 관련된 설정 정보를 독립적인 AppConfig 클래스에서 관리합니다. 

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    private DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }


}

 - 그런데 개발을 하다가, DB를 MemoryMemberRepository에서

    JpaMemberRepository로 변경이 필요해졌다고 가정해봅시다.

-> 이 때, OrderServiceImpl 클래스를 변경할 필요 없이,

     AppConfig 클래스의 memberRepository 메소드의 리턴 값만 수정하면 됩니다. 

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    private MemberRepository memberRepository() {
        return new JpaMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    private DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }


}

  - 이렇게 하면 OrderServiceImpl 클래스의 생성자가 호출될 때, JpaMemberRepository 클래스의 객체가 할당됩니다. 

    위의 코드에서 OrderServiceImpl 클래스는 OCP와 DIP를 만족하는데 각각을 설명하면 다음과 같습니다.

    

   (1) OCP

  - OrderServiceImpl 클래스는 확장에 대해 열려 있고, 변경에 대해 닫혀 있어야 합니다.

    OrderServiceImpl 클래스는 MemberRepository 인터페이스를 의존 관계 주입을 받는데,

    이를 통해 AppConfig 클래스에서 설정 정보 변경만으로 새로운 Repository에 의존할 수 있습니다.

    -> 즉, OrderServiceImpl 클래스는 확장에 열려 있습니다.

 

    반면, 코드의 변경은 AppConfig 클래스에서만 일어나고, OrderServiceImpl 클래스의 코드에는

    아무런 변경이 일어나지 않습니다.

    -> 즉, OrderServiceImpl 클래스는 변경에 닫혀 있습니다. 

  

 (2) DIP

- OrderServiceImpl 클래스는 추상적인 것에 의존해야 합니다. 

-> OrderServiceImpl 클래스는 MemberRepository 인터페이스라는 추상적인 것에 의존하고 있습니다.  

    이렇게 해서 얻을 수 있는 장점은 위의 OCP에서 언급했듯이,

    OrderServiceImpl 클래스의 코드를 변경하지 않고도 확장이 가능하다는 것입니다.

-> 즉, DIP와 OCP는 서로 맞물려서 동작합니다. 

 

- 위와 같이 의존 관계 주입과 OCP, DIP를 적용하면 객체지향적으로 유지보수하기 편한 코드를 작성할 수 있습니다. 

 

 

3) 여러 가지 의존 관계 주입 방식

- 의존 관계 주입 방식에는 여러 가지 방식이 있습니다.

  대표적으로는 위에서 설명 드린 생성자 주입 방식, setter 주입 방식, 필드 주입 방식이 있습니다. 

 

  3-1) 생성자 주입 방식

  - 생성자 주입 방식은 생성자를 통해 의존 관계를 주입하는 방식입니다.

    즉, 클래스의 생성자에 의존 관계에 있는 인터페이스(or 클래스)를 인자로 전달합니다.

 

    생성자 주입 방식의 장점은 크게 2가지입니다. 

    (1) 객체의 생성 시점에 자동으로 의존 관계가 주입된다. 

   -> 의존 관계 주입을 자동으로 해주기 때문에, 의존 관계 주입을 빠뜨릴 위험이 없습니다. 

    (2) 변경에 닫혀 있다.   

   -> 일반적으로 한 번 설정된 의존 관계는 거의 변경되지 않습니다.    

       생성자 주입 방식은 의존 관계의 변경이 닫혀 있으므로 안전합니다.       

 

 3-2) setter 주입 방식

 - setter 주입 방식은 setter 메소드를 통해서 의존 관계를 주입하는 방식입니다. 

   setter 주입 방식은 생성자 주입 방식과는 다르게 의존 관계를 변경할 필요가 있을 때 사용합니다. 

 

 3-3) 필드 주입 방식 

 - 필드 주입 방식은 @Autowired 어노테이션을 통해 필드에 직접 주입하는 방식입니다. 

   필드 주입 방식은 간결하다는 장점이 있지만,

   외부에서 수정이 불가능하므로, 테스트 코드를 작성할 때 객체를 수정할 수 없다는 단점이 있습니다. 

 

  

참고

김영한 스프링 핵심 원리 

https://mangkyu.tistory.com/125

 

'Spring' 카테고리의 다른 글

스레드 풀  (0) 2022.08.14
싱글톤 컨테이너  (0) 2022.08.13
Spring 로깅  (0) 2022.08.08
서블릿  (0) 2022.07.27
IoC, DI  (0) 2022.06.18