본문 바로가기
Spring/java & Spring

[Spring] IoC, DIP 와 DI 이해하기

by domo7304 2022. 6. 25.

https://youtu.be/8lp_nHicYd4

'우아한 Tech' 채널의 [10분 테코톡] 영상을 정리합니다.

더보기

< 타임라인 >
00:34~01:35 : IoC '제어' 와 '역전' 의 의미
01:36~04:27 : IoC 는 왜 필요할까? 무엇이 달라질까?
04:28~07:25 : DIP 란?
07:26~07:58 : IoC 와 DIP 의 목적
07:59~10:13: 중간정리, IoC 와 DIP 를 적용한 코드와, 그렇지 않은 코드의 차이
10:14~11:03 : DI '의존성' 의 의미
11:04~12:25 : 의존성 주입의 3가지 방법 (생성자, setter, Interface)
12:26~13:04 : 의존성 분리 방법 : DIP
13:05~13:44 : 중간정리, principle : IoC, DIP / pattern : DI
13:45~15:15 : Spring DI, 스프링의 의존성 자동 주입 : @Autowired
15:16~19:27 : Spring 의존성 주입의 3가지 방법 (필드, 생성자, setter)

 

1. IoC
 - Inversion of Control (제어의 역전), '제어'와 '역전'의 의미
 - IoC 가 필요한 이유
2. DIP
 - Dependency Inversion Principle (의존 역전 원칙) 의 의미
3. IoC 와 DIP
 - IoC 와 DIP 의 목적
 - 중간정리, IoC 와 DIP 를 적용한 코드와, 그렇지 않은 코드의 차이
4. DI
 - Dependency Injection (의존성 주입), 의존성의 의미
 - 의존성 주입의 3가지 방법 (생성자, setter, Interface)
 - 의존성 분리 방법 : DIP
 - 중간정리, principle : IoC, DIP / pattern : DI
5. Spring DI
 - 스프링의 의존성 자동 주입 : @Autowired
 - Spring 의존성 주입의 3가지 방법 (필드, 생성자, setter)

1. IoC (Invesion of Control)

1. '제어' 와 '역전' 의 의미

제어: 어떠한 클래스 내부에서 다른 객체를 생성하고 이용할 때, 직접 코드를 생성하여 '제어' 한다고 한다.
역전: 객체를 클래스 내부에서 직접 생성하고 제어하는 것이 아니라, 외부에서부터 인자로 받아 초기화 하는 것

2. IoC (제어의 역전) 가 필요한 이유

영상에서 서브웨이를 예시로 이해하기 편하게 설명해주었다.
서브웨이에서 샌드위치를 주문한다고 하자.

  • IoC 가 적용되지 않은 경우: 클래스 내부에서 다른 객체를 제어하고, 외부에서 인자를 받지 않기 때문에, 커스텀 없는 기본 레시피의 샌드위치라고 생각할 수 있다.
  • IoC 가 적용된 경우: 외부에서 인자를 받아 객체를 생성하고 초기화하므로 샌드위치의 속 재료를 커스텀 할 수 있는 샌드위치라고 생각할 수 있다.

이처럼  IoC 는 클래스 내부가 아니라, 클래스 외부에서 제어권을 갖도록 하는 것을 말하며, 클래스 내부에서 객체를 생성하기 때문에 변경이 자유롭지 못하던 객체가, IoC 를 적용함으로써 외부에서 인자를 받아 변경이 자유로워졌음을 알 수 있다.

객체지향적 관점에서 보자면, IoC 를 통해 역할과 책임을 분리하여 결합도를 낮추고, 변경에 유연한 코드가 될 수 있도록 한 것이다.

2. DIP (Dependency Inversion Principle)

1. DIP 의 의미

'상위 레벨의 모듈은 하위 레벨의 모듈에 의존해서는 안된다. 둘 모두 추상화에 의존해야한다. 추상화는 세부 사항에 의존해서는 안된다.' 라 되어 있지만, 글 자체가 이해하기 쉽지 않아 영상에서는 다시 한 번 샌드위치를 예시로 들었다.

  • DIP 가 적용되지 않은 경우: 샌드위치를 만들기 위해서는 상위레벨(샌드위치) 이 하위레벨(어떤 특정한 빵 ex.플랫브레드) 에 의존하고 있는 상황이다. 이를 코드의 입장에서 바라본다면 샌드위치가 특정 빵을 의존하고 있기 때문에 (샌드위치 코드 안에 특정 빵에 대한 로직이 있기 때문에), 샌드위치의 빵을 바꾸기 위해서 샌드위치 자체의 코드도 바꿔야 하는 상황인 것이다.
  • DIP 가 적용된 경우 : 위와 같은 문제를 해결하기 위해 샌드위치는 '어떤 특정한 빵' 이 아니라 어떤 것일지 모르는 '빵'  interface 를 의존하도록 하고, 그 '빵' 에 해당될 플랫브레드, 파마산 오레가노, ... 등도 '빵' 을 의존하도록 하자. 이 경우 샌드위치는 무엇을 구체화될 지 모를 추상화된 빵을 의존하고 있으므로, 어떤 빵이 올지 샌드위치는 관심이 없다. 즉 빵을 바꾸고 싶을 때 샌드위치의 코드를 바꿀 필요는 없게 되는 것이다.

이 때 '빵' interface 는 저수준 모듈인 '어떤 빵' 보다는 고수준 모듈인 '샌드위치' 에 의해 만들어지는데, 처음에 샌드위치가 어떤 빵에 의존했던 상황이 역전되어 어떤 빵이 샌드위치에 의존하게 된다고 하여 의존 역전 원칙 이라고 하는 것이다.

DIP 의 경우 객체지향원칙 SOLID 와 함께 묶어 공부하는 것이 맞을 것 같지만 영상에 나와서 함께 정리하였다. DIP 의 목적이 한 클래스의 변경이 다른 클래스에 대해 미치는 영향을 최소화 하기 위함임은 이해가 가지만, '의존 역전 원칙' 이라는 단어 자체는 뭔가 구체적으로 와닿지 않는 느낌이다....

3. IoC 와 DIP

1. IoC 와 DIP 의 목적

IoC 와 DIP 모두 객체지향적 관점에서 한 클래스의 변경이 다른 클래스에 대해 미치는 영향이 최소가 되도록, 변경에 유연한 코드가 되도록 하는 것에 같은 목적을 둔다.

2. 중간정리, IoC 와 DIP 를 적용한 코드와, 그렇지 않은 코드의 차이

위에서도 다루었지만, IoC, DIP 적용에 따른 차이를 샌드위치로 간단히 짚어보자면

  • IoC 가 적용되지 않은 샌드위치: 손님이 샌드위치의 재료를 커스텀할 수 없다. (클래스 내에서 객체를 제어하기 때문)
  • IoC 가 적용된 샌드위치: 손님이 재료를 요청하여 샌드위치의 재료를 커스텀할 수 있다. (외부에서 인자를 받아 객체를 제어하기 때문)
  • DIP 가 적용되지 않은 샌드위치: 샌드위치는 '어떤 특정한 빵'을 의존하며, 다른 빵으로 바꾸고 싶다면 샌드위치의 코드도 바꿔야 한다.
  • DIP 가 적용된 샌드위치: 샌드위치는 무엇인지 모를 그냥 '빵' 을 의존하며, 어떤 빵이 올지 샌드위치는 관심이 없다. 때문에 다른 빵으로 바꾸고 싶을 때 샌드위치의 코드는 신경쓰지 않아도 된다.

4. DI (Dependency Injection)

1. DI (Dependency Injection) 의 의미

DI 는 'design pattern' 으로, IoC 를 구현하는 다양한 디자인 패턴 중 하나이다. 클래스 간에 의존성이 있다는 것은 한 클래스의 변경이 다른 클래스에 영향을 미침을 의미하며, 의존성 주입이란 말그대로 의존성을 다른 곳에서부터 주입해주는 것을 의미한다.

'제어의 역전' 이 클래스 내부가 아니라 외부로부터 인자를 받아 객체를 제어 함으로써 변경에 유연하도록 하는 원칙이고, 이 외부에서 제어를 받는 방법 중 한 가지로써 '의존성 주입' 을 받는 방법이 있다고 생각하면 될 것 같다.

2. 의존성 주입의 3가지 방법

  • 생성자 주입
  • Setter 주입
  • Interface 주입

Spring 에서 권장하는 생성자 주입에 대해서만 정리하자면, 필요한 의존성을 모두 포함하는 생성자를 만들고, 해당 생성자의 인자를 외부로부터 받는 방법이다.

3. 의존성 분리 방법 : DIP

현재 IoC 를 구현하는 디자인패턴 중 하나인 DI 에 대해 이야기하고 있으므로, 의존성 주입을 통해 IoC 를 구현하여 이제서야 커스텀 가능한 샌드위치가 되었다고 하자(DIP 는 적용 전). 이 상태의 샌드위치는 빵 종류, 치즈 종류, 소스 종류 등을 외부에서부터 인자로 받을 수 있다. 

IoC 가 적용되어 외부로부터 인자를 받아 클래스의 변경은 자유로워졌지만, DIP 가 적용되지 않은 상황에서는 다음과 같은 문제가 발생한다.

  • 샌드위치의 빵/치즈/소스 를 바꾸고 싶다면, 샌드위치 자체의 코드를 수정해야한다. (앞의 DIP 에서 말한 문제)

빵/치즈/소스를 바꾸고 싶은데 샌드위치 코드도 바꿔야 하는 상황은 객체지향적이지 못하다. 때문에 샌드위치 코드는 수정하지 않고 바꾸고 싶은 빵/치즈/소스 만을 바꿀 수 있도록 해야하는데, 이를 '의존성을 분리한다' 고 말을 하며, 이 때 사용되는 원칙이 DIP 이다. 즉 샌드위치에 대한 코드는 '추상화된 빵, 치즈, 소스' 를 의존하도록 하여, 구체적인 빵, 치즈, 소스는 무엇인지 샌드위치 입장에서 관심이 없도록 하는 것이다.

4. 중간정리 principle : IoC / pattern : DI

용어가 헷갈리지 않도록 다시 한 번 정리하자면 IoC (Inversion of Control) 는 '원칙' 이며, DI (Dependency Injection) 는 IoC 를 달성하기 위한 '디자인 패턴 중 하나' 이다.

IoC : 샌드위치가 스스로의 재료를 결정할 수 없다는 (외부로부터 제어를 받는다는) 추상적인 개념.
DI : 샌드위치의 재료를 정해준다는 IoC 를 달성하기 위한 구체적인 행위.

5. Spring DI

1. 스프링 의존성 자동 주입 : @Autowired

스프링을 사용하지 않고도 의존성 주입은 위에서 본 것과 같이 단순 생성자 주입으로 가능하다. 하지만 스프링의 도움을 통해 조금 더 편하게 의존성을 주입받을 수 있다. @Autowired 에 의한 의존성 주입을 이야기하기 전에, 스프링에서 클래스가 어떻게 스프링 빈으로 등록되고, 어떻게 자동으로 의존성이 주입되는지 과정을 짚고 넘어가야 할 것 같아 최대한 간단하게 남겨본다.

@SpringBootApplication 어노테이션에서 ctrl 을 눌러 들어가보면 @ComponentScan 이 있는 것을 볼 수 있다. @ComponentScan 의 역할은 @Component 어노테이션이 부여된 클래스들을 스캔하며 스프링 빈으로 등록해주는 것이다.

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-stereotype-annotations

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

위 문서를 통해 @Controller, @Service, @Repository 는 @Component 를 목적에 맞도록 명시해주기 위한 구체적인 어노테이션임을 알 수 있다.

즉, 정리하자면 @ComponentScan 이 @Component 를 가지는 클래스를 스캔하여 스프링 빈으로 등록하게 되며, 이렇게 등록된 스프링 빈 객체는 스프링 IoC 컨테이너가 관리하게 된다. 이렇게 빈으로 등록된 객체들에 대해

위 이미지와 같이 @Autowired 어노테이션을 사용한다면, 스프링이 자동으로 적절한 의존성을 주입해주게 된다.

2. Spring 의존성 주입의 3가지 방법

  • 필드 주입
  • Setter 주입
  • 생성자 주입

위와 같이 3가지가 있다. 바로 위에 있는 이미지가 '필드 주입' 의 예시인데, 필드 주입의 경우는 Spring 에서도 권장하지 않으며, 밑줄에 커서를 가져가면 아래 이미지와 같이 IntelliJ 에서도 경고를 띄워주는 것을 볼 수 있다.

권장되지 않는 이유는 스프링이라는 프레임워크의 힘을 빌려 자동으로 의존성이 주입되도록 한 것이기 때문에, '수동으로' 의존성을 주입하고 싶은 경우가 있더라도 그럴 수 없는 상황이 발생하기 때문이다. 

Setter 주입의 경우 일단 생략하고...그렇다면 왜 생성자 주입이 권장되는지 알아보자. 첫 번째로 '필드 주입' 의 문제였던 의존성 직접 주입을 해결할 수 있다. 상황에 따른 생성자를 내가 작성하여 의존성을 주입해주도록 하면 되기 때문이다.

그 외에도 객체의 불변성 확보, 순환 참조 에러 방지 등 다양한 이점으로 인해 생성자 주입을 통해 의존성을 주입하는 것이 좋다.

Spring 4.3 이후부터는 생성자가 한 개만 있을 경우 해당 생성자에 스프링이 자동으로 @Autowired 를 붙여주므로, Lombok 의 @RequiredArgsConstructor 어노테이션까지 활용한다면 위 이미지와 같이 생성자 주입을 활용하여 의존성 주입을 할 수 있다.

 

IoC, DIP, DI 에 대해서만 정리할 예정이었는데, 정리하다보니 

  • 객체지향설계 (SOLID)
  • 빈 스코프 : 싱글톤 빈, 프로토타입 빈
  • 필드주입, 생성자주입 등에 대한 자세한 비교

조금 더 자세히 포스팅하면 좋을 것 같은 주제들이 많이 숨어 있었다. 천천히 더 공부해봐야겠다.

생성자 주입을 사용해야 하는 이유로는 아래 블로그에 너무나 잘 정리되어 있어 링크로 남긴다.

https://mangkyu.tistory.com/125

 

[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)

Spring 프레임워크의 핵심 기술 중 하나가 바로 DI(Dependency Injection, 의존성 주입)이다. Spring 프레임워크와 같은 DI 프레임워크를 이용하면 다양한 의존성 주입을 이용하는 방법이 ..

mangkyu.tistory.com

댓글