의존관계 주입(DI, Dependency Injection)은 왜 사용할까? (+Spring IoC)

bae.200.ok
6 min readAug 7, 2022

안녕하세요. 배민혁 입니다.

지난글(객체 지향 프로그래밍 입문)을 통해서 객체지향을 정리해봤습니다. 오늘은 스프링의 또 다른 핵심인 의존관계 주입(이하 DI)에 대해서 이야기해보려합니다.

“의존관계 주입은 왜 사용할까?” 에 대한 답변은 제일 하단에 정리해보겠습니다.

1 배경: 객체지향 핵심 OCP, DIP

OCP, DIP를 위반한 코드

1.1 OCP(Open/Closed Principle)

  • 확장에는 열려 있으나 변경에는 닫혀 있다.
  • 풀어서 이야기하면, 이는 소프트웨어에 변경사항이 있을 때에, ‘확장할 때에 구현체가 변경될 수 있다. 그렇지만 인터페이스를 사용하므로 클라이언트 소스에는 변경이 없다.’는 뜻
  • 지난글에서 ‘클라우드 파일 시스템’ 예시 참고
  • 위의 코드는 MemberService(클라이언트)에서 다른 구현으로 바꾸면 MemberService코드를 직접 수정해야 하는 문제가 있다.

1.2 DIP(Dependency Inversion Principle)

  • 구체화에가 아닌 추상화에 의존해야한다.
  • 풀어서 이야기하면, 클라이언트가 인터페이스(=역할)를 바라봐야하고, 구현은 몰라야한다(=의존하면 안된다.)는 뜻이다.
  • 위의 코드를 보면 구현에 의존하고 있다. 즉, 직접 구현을 선택하고 있다.

2 배경: 의존관계 주입(DI, Dependency Injection)

2.1 의존한다.

의존관계를 이해하기 위해서는 ‘의존한다’라는 표현을 알아야합니다.

  • 한 객체가 다른 객체를 사용할 때 의존한다고 한다.
  • 여기서 사용이라는 표현이 말 그대로 사용을 뜻할 수 도 있지만, 코드레벨로는 ‘상속, 객체 생성, 선언’이 포함될 수 있다.

직접 코드로 예시를 들어보자면

  • OrderServiceImplOrderService상속하여 의존하고 있다.
  • OrderServiceImpl은 생성자에서 타입을 제한하면서 MemberRepository에 의존하고 있다.
  • OrderServiceImplRateDiscountPolicy를 직접 생성하여 객체에 의존하고 있다.

2.2 주입한다.

‘주입한다’는 의존성을 맺어준다고 말할 수 있는데, 코드로서 이해하면 빠르게 이해가 가능합니다. 아래는 생성자 주입에 대한 예시입니다.

  • MemberServiceImpl객체를 생성할 때에 MemmoryMemberRepository객체를 아규먼트로 전달하고 있다.
  • 여기서 MemoryMemberRepository()라는 의존성을 MemberServiceImpl을 생성할 때에 주입했다.

3 문제점: 관심사 중첩

— 해결: 관심사 분리

위의 코드를 보면 OrderServiceImplMemoryMemberRepository, RateDiscountPolicy/FixDiscountPolicy를 직접 생성하고 있습니다. 또한, 비즈니스 로직도 담고 있습니다.

즉, 비즈니스만 해결지 않고, 의존관계를 가지는 객체 생성를 하는 역할을 하고 있습니다.

  • 단일 책임 원칙(SRP, Single Responsibility Principle)을 위배하고 있다.

3.1 AppConfig 구성하기

위의 문제를 해결하기 위해서는 역할을 분리하겠습니다. 그에 따라 애플리키이션 전체 흐름을 제어하는 역할만 담당할 클래스가 필요합니다.

AppConfig의 핵심은 아래와 같습니다.

  • 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
  • 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(연결)해준다.
  • 어떤 구현 객체를 주입할지는 오직 AppConfig에서 결정하도록 한다.
  • 의존 관계에 대한 고민은 외부(=AppConfig)에 맡기고 실행에만 집중하면 된다.

⭐️ AppConfig를 통해서 MemberServiceImpl,OrderServiceImpl 의 흐름이 구현 객체가 아닌 의존성 주입을 통해서 이루어지고 있다.

3.2 제어의 역전(IoC, Inversion of Control)

우리는 AppConfig를 통해서 ‘애플리케이션 흐름 제어’라는 역할을 분리했습니다. 여기서 외부에서 애플리케이션의 흐름을 관리하는 것을 제어의 역전이라고 합니다.

  • AppConfig처럼 객체를 생성하고 관리하면서 의존 관계를 연결해주는 것을 IoC 컨테이너, DI 컨테이너, 조립기라 한다.

4 문제점: 메모리

— 해결: 싱글톤 컨테이너

서버에 요청이 들어올 때 마다 객체를 만들어낸다면, 메모리 이슈를 피할 수 없을 것입니다. 이를 해결하기 위해서 싱글톤 컨테이너라는 개념이 등장합니다.

4.1 싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴의 문제를 해결하면서, 빈 객체를 싱글톤으로 관리한다.(=싱글톤 레지스트리)

싱글톤 패턴의 문제
> 싱글톤 패턴을 적용하여 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장할 수 있다.
> 하지만, 의존관계가 싱글톤 패턴 구현에 포함되어 결국 구체 클래스에 의존하는 문제가 발생한다.
> 이로 인해 내부 속성을 변경하거나 초기화하기 어렵게 되고, 결국 테스트를 하는데에 어려움이 있다.

  • ApplicationContext(BeanFactory가 최상위 인터페이스)라는 스프링 컨테이너에 빈을 등록하고 관리하도록 한다. 스프링 컨테이너에 등록된 빈이 싱글톤으로 관리되는 빈이다.

4.2 스프링 컨테이너에 수동 빈 등록

  • AppConfig에는 @Configuration추가하기
  • 각 메서드에는 @Bean추가하기(빈을을 추가하는 것만으로는 싱글톤이 보장되지 않는다.)

4.3 스프링 컨테이너에 자동 빈 등록

  • 클래스 레벨에 @Component라는 어노테이션을 추가한다. (설정정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.)
    > 물론 @Configuration처럼 @Component애노테이션이 붙어있는 경우도 해당된다.
  • 구성 정보를 담고있는 클래스(혹은 파일)가 없으니 의존관계 주입도 @Component가 붙은 각 클래스에서 해결해야한다.
  • 의존관계 주입 방법으로 생성자 주입을 일반적으로 사용한다. 관련된 정보는 검색하면 많이 확인할 수 있다.(keyword: 의존관계 주입, Autowired, Qualifier, Primary)

⭐️ 스프링에서 제공하는 기능들을 통해서 수동/자동으로 빈을 등록할 수 있습니다.

5 의존성 주입(DI, Dependency Injection)은 왜 사용할까?

의존성 주입이란, 결국 외부에서 객체간 의존관계를 결정해주는 것을 말합니다.

이를 통해서 클래스끼리의 결합도를 낮출 수 있습니다.(=결합된 클래스들을 분리할 수 있다.) 결합도가 낮아지지면, 유연성이 확보되어 테스트가 비교적 자유로워집니다.

--

--