개요
IoC is also known as dependency injection (DI)
스프링 공식 문서에서는 IoC 개념과 DI개념의 설명이 일치하는 부분이 많습니다. 2개는 사실상 같은 원리로 작동하기 때문이며 다른 분들이 동의어로 사용하는 경우도 많습니다. 구체적으로 비교하면 차이가 있는데, 스프링 공식 문서를 중심으로 DI를 알아보려고 합니다.
DI란?
DI는 의존성 주입이라는 뜻으로, 개념적 정의 설명 이전에 먼저 코드로 "의존성이 있다"는 것이 어떤 의미인지 확인해보겠습니다.
사전을 찾아보니 다음과 같습니다.
영어사전을 보면, 불필요할 정도의 종속을 말하기도 합니다. 한국어를 보면, 다른 것에 의지하여 존재하는 것입니다. 즉, 혼자서는 독립적으로 존재하지 못하고 다른 요소에 도움을 받아야만 존재할 수 있는 성질로 추측해볼 수 있습니다. 우리나라의 경제가 석유가 없으면 안 되기에 석유에 의존하여 많은 산업을 가동하는 것처럼 스프링에서도 객체들은 서로 의지하여 존재하는 관계입니다.
또한, 주사기를 통해서 의존성이라는 성질을 주입(Injection) 한다고도 볼 수 있습니다. 아픈 사람이 영양 주사기로 영양소를 주입받아 에너지를 채워 살아가듯(?), 객체들은 의존성을 주입받아 자신들의 온전한 역할을 할 수 있습니다.
스프링 코드로 먼저 맛보기
SimpleMovieListener 클래스의 변수 movieFinder와 생성자를 생성합니다.
public class SimpleMovieListener {
// SimpleMovieListener MovieFinder에 의존성이 있다
private final MovieFinder movieFinder;
// 스프링 컨테이너가 MovieFinder를 주입하기 위한 생성자
public SimpleMovieListener(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// moveFinder를 활용한 비지니스 로직 처리...
}
"SimpleMovieListener 클래스는 MovieFinder에 의존성을 가지고" 있습니다. SimpleMovieListener는 생성자 주입으로 스프링 컨테이너에 의해 movieFinder를 주입 받고 movieFinder는 SimpleMovieListener클래스에서 비지니스 로직을 처리합니다.
의존성을 가진다는 것과 의존성을 주입한다는 것은 다릅니다!
SimpleMovieListener클래스는 비지니스로직을 처리하기 위해서 MovieFinder를 변수로 가지고 있습니다. 이 상태를 "SimpleMovieListener 가 MovieFinder에 의존"한다고 합니다. 의존성을 가지는 경우, 어떠한 방법을 사용해서라도 의존성이 주입되어야 합니다. 그래야 어플리케이션에서 객체를 사용할 수 있습니다. 여기에는 크게 생성자 방식과 setter 방식이 있고 위의 예시에서는 생성자 주입을 사용했습니다.
생성자 주입이 된다는 것은 무엇일까요?
스프링 컨테이너가 생성되어 초기화 되면, 스프링 컨테이너는 설정 정보에 따라 빈을 생성하고 관리합니다. (위의 예시 코드에는 없지만 아마 MovieFinder는 스프링 컨테이너에 의해 빈으로 관리되고 있는 상황이라고 추측 가능합니다.) private final MovieFinder movieFinder로 선언된 변수가 있으며, SimpleMovieListener 생성자에 MovieFinder가 매개변수로 있기 때문에, "movieFinder는 SimpleMovieListener에 의존성이 주입"됩니다.
이후, movieFinder를 사용해서 비지니스 로직을 처리합니다.
각 관계를 다시 그림으로 정리하면 다음과 같습니다.
의존성을 가진다는 것과 의존성을 주입한다는 차이를 구분할 수 있으신가요? 스프링에서 객체를 사용하기 위해서는 의존성을 가지는 것, 협력하는 객체들이 의존성을 주입하는 것 2가지 모두가 필요합니다.
스프링 개념 문서로 살펴보기
스프링 공식 문서를 통해서 의존성 주입의 개념을 알아보겠습니다.
It is a process whereby objects define their dependencies (that is, the other objects they work with) only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method.
스프링 문서에 따른 의존성 주입이란 객체들이 생성자 매개변수, 팩토리 메서드에 있는 매개변수, 혹은 생성자가 생성되거나 팩터리 메서드로부터 반환된 후 객체 인스턴스에 설정된 설정 값을 통해서만 자신 객체들의 의존성(객체들이 함께 일하는 다른 객체들)을 정의하는 것입니다. 그리고 컨테이너는 빈을 생성했을 때, 의존성을 주입합니다.
공식문서에서는, 의존성을 "객체들이 함께 협력하는 다른 객체들"이라고도 표현합니다. 의존성을 주입한다는 것은 결국, 객체를 다른 객체로 주입, 연결한다는 것입니다. 또한, 특정 객체들이 자신들과 협력하는 다른 객체들을 주입하려면 스스로 하지 못하고 외부의 힘(Spring Container)을 빌려 주입시켜 줍니다.
위의 코드를 예시로 보면, MovieFinder 클래스의 객체가 SimpleMovieListener클래스에 주입되는 경우입니다. SimpleMovieListener에서 MovieFinder를 사용하기 위해서 new()를 사용하지 않습니다. Spring Container가 제공하는 생성자 방식을 사용하기 위해 SimpleMovieListener 생성자에 movieFinder를 주입하였습니다.
의존성 주입이 성공하면, SimpleMovieListener는 movieFinder를 사용하여 비지니스 로직을 처리할 수 있습니다.
"인터페이스나 추상 클래스에 의존성 있다면..." 이라는 내용이 있습니다. 즉, 클래스 A가 변수로 인터페이스나 추상 클래스를 가질 수 있다는 의미입니다. 의존성은 클래스뿐만 아니라 인터페이스나 추상 클래스에도 가질 수 있습니다. 의존성 주입을 활용하면 가짜 구현체를 만들 수 있어서 테스트하기 수월합니다.
"컨테이너가 빈(객체)를 생성했을 때, 의존성이 주입됩니다", 라는 문장도 있습니다. (빈은 스프링에서 객체를 관리하기 위한 단위이므로 객체와 동의어입니다.) 이를 통해, 컨테이너에 빈이 먼저 로드되고나서 의존성 주입 과정을 진행된다고 유추할 수 있습니다. 단, 빈이 생성된다는 것이 항상 성공적으로 의존성이 주입된다고 보장할 수는 없습니다. 의존성 주입 규칙에 어긋난 문제가 있으면 예외를 던지는 순환참조 오류가 있습니다. 컨테이너 구동 시 순환 참조가 없다면, 컨테이너는 빈을 생성하고, 설정에 따라서 의존성을 주입합니다.(의존성 주입에는 크게 생성자, setter 방식이 있고 뒤에서 살펴보겠습니다.)
정리하면, 스프링에서 객체를 빈으로 관리하며, 객체를 다른 객체와 사용하기 위해서는 의존성 주입이 되어있어야 합니다. 의존성 주입이 되었다는 뜻은, 어떤 객체가 스프링에서 동작할 수 있도록 협력하는 객체들이 주입되었다는 뜻입니다.
다음은 Oracle의 문서를 확인해보겠습니다.
Dependency injection enables you to turn regular Java classes into managed objects and to inject them into any other managed object. Using dependency injection, your code can declare dependencies on any managed object. The container automatically provides instances of these dependencies at the injection points at runtime, and it also manages the lifecycle of these instances for you.
출처 : https://docs.oracle.com/javaee/7/tutorial/injection002.htm
"의존성 주입으로 일반 자바 클래스를 관리되는 객체로 바꿀 수 있습니다. 그리고 관리되는 객체들을 또다른 관리되는 객체에 주입할 수 있습니다" 스프링 공식 문서에서처럼, 의존성 주입을 관리되는 객체끼리의 주입으로 봅니다. 의존성이 주입되면 관리되는 객체가 된다는 표현이 신선합니다
"의존성 주입을 사용하여, 당신의 코드는 어떤 관리되는 객체에도 의존성을 선언할 수 있습니다." 의존성 주입은 빈으로 관리되는 객체를 스프링에서 사용하기 위해 협력하는 행위입니다. 빈을 실제로 사용하기 위해서는 협력하는 객체들의 의존성 주입이 완료되어야 하기 때문입니다.
컨테이너는 자동으로 이 의존성들의 인스턴스들을 런타임에 제공해주며, 생명주기까지 관리합니다. 관리는 스프링 컨테이너가 하기 때문에, 개발자는 어떤 것을 빈으로 관리하고 어떻게 의존성 주입을 할 것인지만 고민하면 됩니다.
DI의 효과
- 결합도가 낮아집니다.
객체는 의존성을 조회하지 않고, 의존성의 클래스나 위치를 알지 못합니다.
- 클래스를 테스트하기 쉽습니다.
의존성이 인터페이스나 추상클래스에 선언되어 있다면, 단위 테스트에서 mock 구현을 할 수 있기 때문입니다.
DI 선언 방법
* Dependency Lookup
의존성 조회란 요청에 따라 인스턴스를 얻는 방법입니다. 인스턴스를 얻기 위한 여러 가지 방법이 있습니다.
1. new를 사용해서, 클래스 A의 인스턴스를 직접 얻을 수 있습니다.
A obj = new AImpl();
2. 스태틱 팩토리 메서드인 getA()를 호출해서 클래스 A의 인스턴스를 얻을 수 있습니다.
A obj = A.getA();
3. JNDI를 통해 인스턴스를 얻을 수 있습니다.
Context ctx = new InitialContext();
Context environmentCtx = (Context) ctx.lookup("java:comp/env");
A obj = (A)environmentCtx.lookup("A");
위의 의존성 조회 방식들에서 문제점이 있습니다.
- 강한 결합
의존성 조회로 코드의 결합성을 강화시킵니다. 인스턴스가 바뀌게되면, 코드에서 많은 변경을 해야 합니다.
- 테스트 어려움
어플리케이션 테스트가 어렵습니다.
스프링에서 사용하는 의존성 주입의 종류와 개념은 다음 글을 참고하세요 ( 참고 )
ApplicationContext부터 의존성 주입까지
의존성을 주입하기 위해서는 ApplicationContext 초기화, 유효성 검사 및 빈 생성, 의존성 주입의 과정이 필요합니다.
1. ApplicationContext 초기화
ApplicationContext가 생성되고, 메타데이터 설정으로 초기화됩니다. 메타데이터 설정은 XML, 자바, 어노테이션 등으로 가능합니다.
2. 유효성 검사 및 빈 생성
컨테이너가 생성될 때 싱글 스코프 빈과 빈들은 같이 생성됩니다. 그리고 각 빈의 유효성을 검사합니다. 빈 설정 값들은 빈이 실제로 생성되고 나서 설정됩니다. (빈은 빈 스코프에서 정의되고, 그렇지 않도록 설정하면, 빈은 실제로 요청을 받았을 때에만 생성됩니다. 그러면, 의존성들 사이에 오류는 늦게 발견될 수 있습니다)
3. 의존성 주입
팩토리 메서드, 팩토리 빈, 생성자, 설정들로부터 모든 빈 의존성들은 빈이 생성되고 나서 주입됩니다.
주의 할 것은, B가 A를 의존하고 있기 때문에, B가 먼저 초기화되고, A에 의존성이 주입됩니다
순환 의존성이란?
예를들어 클래스 A는 생성자 주입으로 클래스 B의 인스턴스를 요청합니다. 그리고 클래스 B는 생성자 주입으로 클래스 A의 인스턴스를 요청합니다. 만약에, 클래스 A와 B의 빈 설정을 서로 요청한다면, 스프링 IoC 컨테이너는 런타임에 순환 참조를 감지하고 BeanCurrentlyInCreationException 예외를 발생시킵니다.
A의 생성자에 의존성 주입이 되어 있기 위해서는 클래스 B의 설정이 모두 완료되어 있어야 합니다. 따라서 클래스 B는 자신의 설정을 위해서 의존성 설정을 하는데, 클래스 B의 생성자 주입은 클래스 클래스 A가 해야 하므로, 클래스 A가 설정이 되어 있어야 합니다. A와 B는 서로 생성자 주입을 해주기 위해서 꼬리에 꼬리를 물고 서로를 호출하게 됩니다.
한가지 해결책(Spring 공식문서)은 생성자보다는 setter로 설정된 클래스의 코드를 수정하는 것입니다. 즉, 생성자 주입을 제거하고 setter 주입만 사용하도록 합니다. 추천하는 방식은 아니지만, setter 주입으로 순환 참조를 해결할 수 있습니다.
일반적인 경우와 달리, 빈 A, B 사이에 순환 의존성은 둘 중 하나의 빈이 완전히 초기화되기 이전에 다른 빈에 주입되도록 강제합니다.(닭이 먼저냐 달걀이 먼져냐 문제)
스프링은 왜 싱글톤으로 미리 빈을 초기화할까?
스프링은 빈이 실제로 생성되더라도, 프로퍼티 설정과 의존성 주입을 늦게 할 수 있습니다. 즉, 스프링 컨테이너는 미리 초기화하지 않고 있다가, 실제로 객체를 요청했을 때, 객체 생성 혹은 의존성 설정에 문제가 있으면 그때 예외를 발생시킬 수 있습니다.
하지만 늦게 오류를 발생시킨다면, 몇몇 설정 문제가 나중에 발견되므로 ApplicationContext는 기본적으로 미리 빈들을 초기화하는 싱글톤 빈을 구현합니다. 빈들이 실제로 사용되기 이전에 미리 빈을 생성하는데, 약간의 선행 시간과 메모리를 대가로 ApplicationContext가 생성될 때 구성 문제를 발견 할 수 있습니다.
만약에 빈 A가 빈 B를 의존하고 있으면, 스프링 IoC 컨테이너는 빈 A에 있는 setter 메서드를 호출하기 이전에 빈 B를 먼저 초기화합니다.
아래 사진을 보면 알 수 있습니다. 만약에 A가 B를 의존한다면, 먼저 B가 컨테이너에 의해 초기화가 되고 그 이후에 B를 A에 주입하여 A를 초기화합니다.
스프링의 빈 scope는 다양하게 있는데 prototype은 매번 새로운 객체를 생성하고, request는 요청때마다 생성하고, session은 HTTP 세션마다 생성합니다.
* 참고
http://www.geekcoders.net/how-dependency-injection-works-in-spring/
https://www.amitph.com/spring-setter-injection-example/
https://wildeveloperetrain.tistory.com/139?category=1010863
'Spring' 카테고리의 다른 글
@Controller @RestContrller (0) | 2022.08.25 |
---|---|
@Controller, @Service, @Repository 차이 (0) | 2022.08.25 |
Servlet (0) | 2022.07.11 |
ApplicationContext (0) | 2022.07.08 |
Spring framework 로그(log) 알아보기 (1) | 2021.12.16 |