IoC(Inversion of Control, 제어의 역전)란?
흔히 IoC는 제어의 역전이라는 뜻으로 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 말한다.
맞는 말이긴하지만 좀 더? 직관적으로 풀어서 작성해보면 아래와 같을 것이다.
don’t call me, I’ll call you. - Hollywood principle
개인적으로 IoC 를 가장 직관적으로 잘 설명한 문장이라고 생각한다.
좀 더 개발 친화적인 용어로 풀어서 설명하면 다음과 같이 표현할 수 있다.
IoC 란 코드의 흐름을 제어하는 주체가 바뀌는 것이다.
코드의 흐름을 제어한다는 것은 여러 행위를 포함한다. 오브젝트를 생성하는 것, 오브젝트의 생명주기를 관리하는 것, 메소드를 수행하는 것 등. 그리고 일반적인 프로그램은 이러한 행위를 하나부터 열까지 모두 스스로 수행한다.
IoC 를 적용한다는 것은 이러한 흐름 제어를 또 다른 제 3자가 수행한다는 것을 의미한다.
스프링에서의 Bean 초기화, 소멸 콜백 메소드를 살펴보자.
public class NetworkClient{
...
@PostConstruct
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메세지");
}
@PreDestroy
public void close() {
System.out.println("NetworkClient.close");
disconnect();
}
}
우리가 Bean을 등록하고 생명주기 메소드가 호출되었을 때의 동작만 정의하고, 언제 생명주기 메소드를 호출 할지는 신경쓰지 않는다. 즉, Bean 의 메인 흐름 제어권은 나의 코드가 아니라 스프링 플랫폼에서 쥐고 있다.
누가 물어봤을 때 명확한 답변을 못했던 ‘프레임워크와 라이브러리의 차이는 무엇인가?’ 에 대해 IoC 관점으로 설명이 가능하다. 라이브러리는 내 코드가 라이브러리를 이용한다. 즉, 제어권이 내 코드에 있다. 반면 프레임 워크는 프레임 워크가 나의 코드를 실행한다. 즉, 제어권은 프레임워크에게 있다.
Software frameworks, callbacks, schedulers, event loops, dependency injection, and the template method are examples of design patterns that follow the inversion of control principle
IoC 를 따르는 개념이 생각보다 많다.
더 자세한 예시는 아래 포스팅 참고.
Dependency Inversion principle (DIP, 의존관계 역전 법칙)
a. High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
b. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
SOLID 원칙 중 하나이다. 의존관계에 대해 다루고 있는데 한번에 바로 이해될 수 있는 설명이 아니었다. 관련된 내용을 찾아보다가 좀 더 직관적으로 표현된 문장을 발견했다.
DIP is about the level of the abstraction in the messages sent from your code to the thing it is calling
DIP 가 주장하는 바의 핵심은 추상화에 의존하라는 것이다.
추상화가 아닌 구체클래스에 의존한 경우를 살펴보자.
public class FileLoader {
private TextFileParser textFileParser;
public FileLoader() {
// TextFile 이 아닌 csv 파일을 파싱해야할 경우 필연적으로 코드의 변경이 발생
this.textFileParser = new TextFileParser();
}
public File parseFile(String serializedFile) {
return textFileParser.parse(serializedFile);
}
}
위와 같이 TextFileParser 에 변경이 발생했을 때 이를 의존하는 FileLoader 역시 변경이 발생하게 된다. 또한 FileLoader 는 TextFile 외에 다른 File 을 파싱하기 위해서는 아예 Parser 클래스를 변경해야한다. 변경에 유연하지 않은 구조이다.
추상화에 의존할 경우를 살펴보자
public interface FileParser {
File parse(String serializedFile);
}
public class TextFileParser implements FileParser {
@override
public File parse(String serializedFile) {
// Do something
return File();
}
}
public class FileLoader {
private FileParser fileParser;
public FileLoader(FileParser fileParser) {
this.fileParser = fileParser;
}
public File parseFile(String serializedFile) {
return fileParser.parse(serializedFile);
}
}
FileLoader 는 FileParser 인터페이스에 의존하기에 FileParser 의 구현체인 TextFileParser 변경이 발생하더라도 영향을 받지 않는다. 또한 FileParser 인터페이스를 구현한 구현체라면 무엇이든 FileLoader 에서 이용이 가능하다. 즉, 다형성을 활용하여 변경에 유연한 구조가 된다.
DI(Dependency Injection)란?
DI is about how one object acquires a dependency
DI는 의존관계 주입이라는 뜻으로 여기서 의존관계(Dependency)는 어떠한 객체와 같이 수행(work with)되는 다른 객체와의 관계를 뜻한다.
즉, DI 는 필요로 하는 오브젝트를 스스로 생성하는 것이 아닌 외부로 부터 주입받아 다른 객체와의 관계를 맺는 기법을 의미한다.
Spring 프레임워크에서 지원하는 IoC의 형태로 클래스 사이의 의존관계를 빈 설정 정보를 바탕으로 컨테이너가 자동으로 연결해준다.
더 자세한 내용은 다음을 참고 하자.
각 개념 간의 관계
IoC 와 DI
종종 IoC 와 Dependency Injection 은 서로 interchangeably 한 것, 더 나아가 아예 같은 것처럼 표현하는 글이 보이곤 하는데 이는 잘못된 해석이라고 생각한다. Dependency Injection 은 IoC 개념이 적용된 과정 중의 결과물 중 하나일 뿐이다. 의존성을 주입한다는 것을 IoC 적인 행위로 바라볼 수 는 있지만 IoC 가 곧 의존성 주입이라고 보기 는 어렵기 때문이다.
DIP 와 DI
단어가 비슷해보이는 DIP 와 DI 역시 같은 개념으로 오해하기 쉽다. 하지만 마찬가지로 DI 는 DIP 를 구현하는 기법중 하나일 뿐 서로 같은 개념이 아니다. 위 DIP 예제 코드에서도 DI 가 이용되었다.
DIP 에 대한 이해가 부족했을 때, 아래와 같은 코드도 DIP 를 만족하는 것이라고 생각할 수 있다.
public interface FileParser {
File parse(String serializedFile);
}
public class TextFileParser implements FileParser {
@override
public File parse(String serializedFile) {
// Do something
return File();
}
}
public class FileLoader {
private FileParser fileParser;
public FileLoader() {
this.fileParser = new TextFileParser();
}
public File parseFile(String serializedFile) {
return fileParser.parse(serializedFile);
}
}
하지만 해당 코드에서는 FileParser 를 다른 구현체로 바꿀 수 없다. 사실상 타입만 인터페이스로 했을 뿐 다형성의 이점을 전혀 살리지 못하는 코드이며 DIP 를 만족한다고 보기 어렵다.
또 다른 예시를 살펴보자. 이 코드도 조금 아쉬운 점이 있다.
public class FileLoader {
private TextFileParser textFileParser;
public FileLoader(TextFileParser textFileParser) {
this.textFileParser = textFileParser;
}
public File parseFile(String serializedFile) {
return textFileParser.parse(serializedFile);
}
}
해당 코드는 TextFileParser 를 주입 받으므로 DI 가 이루어졌다고 볼 수 있다. 따라서 TextFileParser 의 생성자에 변경이 생기더라도 FileLoader 에 전파되지 않는 것은 긍정적인 부분이다. 하지만 DIP 는 지켜지지 않았다. 구체 클래스에 의존하고 있으므로 다른 FileParser 로 교체하는 것은 불가능하며 TextFileParser 의 변경에 FileLoader 가 영향을 받게 된다.
위 예시들이 시사하는 바는 DIP 와 DI 는 서로 조합되었을 때 시너지를 발휘한다는 것이다. 그래서 보통 한쪽 개념의 예시를 들 때 다른 쪽 개념이 같이 적용되어 있기 때문에 두 개념을 같다고 이해 할 법도 하다.
스프링 컨테이너(Spring Container)란?
스프링 컨테이너는 스프링의 빈(Bean)을 생성하고 관리한다. 스프링 컨테이너는 IoC Container 혹은 DI Container 라고 불리는데, 이는 스프링 컨테이너가 IoC 혹은 DI를 도맡아 진행하기 때문이다. 즉, 스프링 컨테이너는 스프링 Bean들을 생성하고, 이들의 의존 관계를 연결해주는 역할을 한다.
이러한 스프링 컨테이너는 BeanFactory와 ApplicationContext로 나뉘는데 둘의 내용은 다음과 같다.
BeanFactory
- 스프링 컨테이너의 최상위 인터페이스이다.
- 스프링 빈을 관리하고 조회하는 역할을 담당한다.
ApplicationContext
- BeanFactory 기능을 모두 상속받아서 제공한다.
- 다음과 같은 부가기능들을 제공한다.
- 메시지 소스를 활용한 국제화 기능
- 환경변수 - 로컬, 개발, 운영 등을 구분해서 처리
- 애플리케이션 이벤트 관리
- 편리한 리소스 조회
보통 스프링 컨테이너라고 하면 ApplicationContext를 뜻한다. BeanFactory의 모든 기능을 상속받는데다가 편리한 부가기능을 제공하기 때문에 BeanFactory보다는 ApplicationContext를 사용하게 된다.
스프링 빈(Bean)이란?
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container.
해당 내용은 스프링 공식문서에서 발췌해왔다. 이 내용을 번역하자면 다음과 같다.
스프링에서는, 스프링 IoC 컨테이너에 의해 관리되고 애플리케이션의 핵심을 이루는 객체들을 Bean이라고 부른다. Bean은 스프링 IoC 컨테이너에 의해 인스턴스화되어 조립되거나 관리되는 객체를 말한다.
즉, 스프링 빈은 스프링 컨테이너에 의해서 만들어지고 관리되는 객체라는 뜻이다.
더 자세한 내용은 해당 내용 참고
예상 면접 질문 및 답변
Q. Spring DI/IoC는 어떻게 동작하나요?
IoC(제어의 역전)은 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것으로 코드의 최종호출은 개발자가 제어하는 것이 아닌 프레임워크의 내부에서 결정된 대로 이루어집니다.
DI(의존관계 주입)은 Spring 프레임워크에서 지원하는 IoC의 형태로 클래스 사이의 의존관계를 빈 설정 정보를 바탕으로 컨테이너가 자동으로 연결해줍니다.
스프링에서는 스프링 컨테이너 ApplicationContext를 이용하여 설정 정보를 생성, 등록하고 필요한 객체를 생성자 혹은 setter를 통해 주입합니다.
Q. IoC 컨테이너의 역할은 무엇이 있을까요?
애플리케이션 실행시점에 빈 오브젝트를 인스턴스화하고 DI 한 후에 최초로 애플리케이션을 기동할 빈 하나를 제공해준다
Q. Spring Bean이란 무엇인가요?
IoC 컨테이너 안에 들어있는 객체로 필요할 때 IoC컨테이너에서 가져와서 사용합니다. @Bean 을 사용하거나 xml설정을 통해 일반 객체를 Bean으로 등록할 수 있습니다.
참고
- https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard
- https://justhackem.wordpress.com/2016/05/14/inversion-of-control/
- https://www.martinfowler.com/articles/injection.html
- https://martinfowler.com/articles/dipInTheWild.html
- https://dzone.com/articles/ioc-vs-di
- https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans
- https://www.codeproject.com/Articles/592372/Dependency-Injection-DI-vs-Inversion-of-Control-IO
- https://medium.com/@ivorobioff/dependency-injection-vs-service-locator-2bb8484c2e20