IoC / DI
라이브러리와 프레임워크의 차이
라이브러리
라이브러리는 특정 기능을 수행하는 함수, 클래스, 또는 메서드들의 집합으로, 개발자가 필요할 때 호출하여 하용하는 도구이다.
개발자가 제어권을 가진다.
개발시 필요한 도구를 제공한다.
프레임워크
애플리케이션의 구조와 동작 방식을 정의하고, 개발자가 해당 구조 안에서 코드를 작성하도록 설계된 도구이다.
프레임워크가 제어권을 가진다.
애플리케이션의 뼈대를 제공한다.
1. IoC(Inversion of Control)
제어의 역전이라는 의미로, 스프링에서 오브젝트(빈)의 생성과 의존 관계 설정, 사용, 제거 등의 작업을 코드 대신 스프링 컨테이너가 담당한다.
이를 스프링 컨테이너가 오브젝트에 대한 제어권을 가지고 있다고 해서 IoC 라고 부른다. (스프링 자체가 라이브러리가 아닌 프레임워크이기 때문)
따라서, 스프링 컨테이너를 IoC 컨테이너라고 부른다.
오브젝트(빈)을 IoC 컨테이너에서 관리하므로, 개발자는 비지니스 로직에 집중할 수 있다.
대신, 어떻게 빈이 프레임워크에서 관리되는지는 알아야겠지?
1-1. IoC 컨테이너란?
스프링에서는 IoC 를 담당하는 컨테이너를 빈 팩토리, DI 컨테이너, 애플리케이션 컨텍스트 라고 다양하게 부른다.
오브젝트 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI 관점으로 바라볼 때, 컨테이너를 빈 팩토리, 또는 DI 컨테이너라 부른다.
그러나 스프링 컨테이너는 단순한 DI 작업보다 더 많은 일을 하는데, DI 를 위한 빈 팩토리의 여러가지 기능을 추가한 것을 애플리케이션 컨텍스트라고 부른다.
정리하면, 애플리케이션 컨텍스트는 그 자체로 IoC와 DI 그 이상의 기능을 가졌다고 생각하면 된다.
1-2. 빈 팩토리와 애플리케이션 컨텍스트의 관계
빈 팩토리
스프링 컨테이너의 최상위 인터페이스
스프링 빈을 관리하고 조회하는 역할을 담당한다.
대표적으로
getBean()
메서도를 제공한다.
애플리케이션 컨텍스트
애플리케이션 컨텍스트는 빈 팩토리의 기능을 모두 상속받아서 사용한다.
때문에 빈 팩토리의 없는 다음의 기능들을 가지고 있다.
메시지 소스를 활용한 국제화 기능
환경변수
애플리케이션 이벤트
편리한 리소스 조회
...
1-3. 설정 메타 정보
IoC 컨테이너의 가장 기초적인 역할은 오브젝트를 생성하고 관리하는 것이다.
스프링 컨테이너가 관리하는 이러한 오브젝트를 빈이라고 한다.
스프링 컨테이너는 자바코드(생성자, 수정자...), XML, Groovy 등 다양한 설정 정보를 받아들일 수 있도록 유연하게 설계되어 있다.
1-4. 스프링 빈 설정 메타 정보(BeanDefinition)
스프링이 이러한 다양한 설정을 제공하는데 있어 그 중심에는
BeanDefinition
이라는 추상화가 있다.쉽게 말하면, 설정정보(XML, 자바코드, 어노테이션) 를 읽어서
BeanDefinition
을 만든다.따라서 스프링 컨테이너는 오직
BeanDefinition
만 알면 된다.이상적인 OOP 가 지켜진 것을 확인할 수 있다.
2. DI(의존관계 주입)
2-1. 의존관계(Dependency)
프로그래밍에서 의존이란 A 객체를 수정할 때 B 의 기능이 추가되거나 변경되면 두 객체는 서로 의존관계에 있다고 볼 수 있다.
객체지향 프로그래밍에서는 구체화된 객체를 의존하는 것이 아닌, 인터페이스를 의존하게 되면, 확장성 있는 의존관계를 맺을 수 있다.
2-2. 의존관계 주입(Dependency Injection)
의존 관계를 외부에서 결정하는 것을 DI 라고 한다.
DI 를 통해서 객체간의 의존성이 줄어든다.
메서드 매개변수를 통해서 의존관계를 주입한다. -> 요구사항이 변경되었을 때 구현체만 변경하면 된다.
스프링에서는 외부의 대상이 IoC 컨테이너가 되어, 빈을 알아서 주입해 준다.
IoC 는 프레임워크의 특징이다.
2-3. 의존관계 주입의 방법
XML 등을 사용한 의존관계 주입도 있지만, 현재는 사용하고 있지 않다고 보는게 맞다!
래거시 프로젝트에서만 XML 을 통한 의존성 주입의 코드를 볼 수 있을 것이다.
필드 주입
의존성을 주입받을 필드에
@Autowired
를 붙이는 방식
장점
간단하고 코드가 짧아짐
테스트 및 빠른 개발 단계에서 빠르게 작성 가능
단점
테스트 어려움 : 필드가
private
이므로Mock
객체를 직접 주입하기가 어렵다.강한 결합 : 의존성을 외부에서 변경할 수 없기에 의존성이 떨어짐
권장되지 않음 : 스프링 커뮤니티에서 권장하지 않는 방법이다.
수정자 주입
@Autowired
를 사용하여 수정자 메서드를 통해 의존성을 주입받는 방식
장점
유연성 : 의존성을 필요에 따라 변경하거나 주입하지 않을 수도 있다.
테스트 편리성 :
setter
메서드를 통해서 쉽게Mock
객체 주입 가능.
단점
의존성이 필수인 경우에도 명시적으로 표시되지 않음 (컴파일 타임에 체크 불가)
생성자 주입보다 DI 강제성이 낮다.
생성자 주입
생성자를 통해 의존성을 주입받는 방식으로, 의존성을 생성자 매개변수로 전달받는다.
장점
불변성 :
final
키워드 사용 가능, 의존성이 변경되지 않음을 보장의존성 강제 : 객체 생성 시 필요한 의존성을 강제하므로, 누락될 가능성이 없다.
테스트 용이 : 생성자를 통해 의존성을 명시적으로 주입하므로,
Mock
객체 사용이 쉽다.권장되는 방식 : 스프링 커뮤니티에서 권장하는 방식이다.
단점
아래와 같이 의존성이 많을 경우 코드가 길어질 수 있다.
Lombok 의
@RequiredArgsConstructor
를 사용하면 이 단점을 극복할 수 있다.
2-4. 순환 참조(Circular Reference)
스프링 순환 참조랑 서로 다른 빈들이 서로 참조를 맞물리게 주입되면서 생기는 현상이다.
BeanA 에서 BeanB 를 참조하게 되는데, BeanB 에서 BeanA 를 참조해야 하는 경우 순환참조 문제가 생긴다.
즉, 스프링에서 어떤 스프링 빈을 먼저 만들어야 할 지 결정할 수 없게 되는 상황이라 할 수 있다.
이 순환 참조 문제도 DI 를 하는 방법 3가지 상황에서 발생할 수 있다.
순환 참조가 발생하는 3가지 케이스
생성자 주입 방식
어플리케이션 구동시, 스프링 컨테이너는 BeanA 를 생성하기 위해 BeanB 를 주입해주어야 하기 때문에 BeanB 를 찾을것이다.
근데, BeanB 를 생성하려 하니 BeanA 가 필요해서 BeanA 를 찾게 되며 무한 반복이 생기게 된다.
필드, Setter 주입 방식
이 두가지 DI 방식은 어플리케이션 구동 시 DI 를 하지 않는다.
이 두가지 방식은 어플리케이션 구동 시점에서 필요한 의존성이 없다면
null
상태로 유지하고 실제로 사용하는 시점에 주입하기 때문에,이 두가지 방식은 모두 순환참조를 일으킬 수 있는 메서드를 호출하는 시점에 순환참조 문제가 발생할 것이다.
해결책
결국 순환을 끊으므로써 순환참조 문제를 해결해야 하는데, 스프링에서는
@Lazy
라는 애노테이션을 통해 이런 순환참조를 끊을 수 있도록 한다.하지만 스프링 커뮤니티에서는 이러한 방식을 추천하지 않는다. 그 이유는 다음과 같다.
설계 결함이 가려짐
순환 참조는 설계상 오류일 가능성이 크다.
@Lazy
를 사용하면 문제가 해결된 것처럼 보이지만, 애플리케이션의 의존성 구조가 비대하고 복잡한 상태로 유지된다.
런타임 지연 가능성
Bean
초기화가 지연되면서 실행 시 성능 저하나 예상치 못한.LazyInitializationException
이 발생할 수 있다.특히, 지연 초기화된 Bean 에서 다른 프록시 Bean 을 호출할 때 복잡한 버그기 발생할 가능성이 높아진다.
테스트 및 디버깅 어려움
의존성이 지연 초기화되므로 어떤 시점에 Bean 이 생성되었는지 추적하기가 어렵다.
순환 참조 문제가 코드 리뷰나 테스트 단계에서 드러나지 않을 수 있다.
의존성 설계 복잡화
순환 참조 구조가 유지되면서 의존 관계가 복잡해지고, 코드의 모듈성을 해칩니다.
결론적으로 순환참조가 발생하지 않는 구조로 설계를 변경하는 것이 좋다.
2-5. @Autowired
DI 를 할 때 사용하는 어노테이션으로, 의존 관계의 타입에 해당하는 빈을 찾아 주입하는 역할을 한다.
스프링 서버가 올라갈 때 애플리케이션 컨텍스트가
@Bean
,@Service
,@Controller
등 어노테이션을 이용하여 등록한 빈을 생성하고,@Autowired
어노테이션이 붙은 위치에 의존관계 주입을 실행하게 된다.해당 어노테이션에 빈을 주입하는 것은
BeanPostProcessor
라는 내용을 찾을 수 있고, 그것의 구현체는AutowiredAnnotationBeanPostProcessor
인 것을 확인할 수 있다.
3. 결론(DI 와 IoC 의 차이는?)
DI : 의존관계를 어떻게 주입할 것인가?
생성자, 필드, 수정자
IoC : 누가 소프트웨어의 제어권을 가지고 있는가?
스프링 컨테이너가 빈을 생성할 때 Bean 간에 의존관계를 DI 를 통해서 해결한다.
DI 는 IoC 를 필수적으로 사용하지 않는다.
자바 코드(생성자, 수정자) 를 통해서 의존성을 직접 주입할 수도 있다.
Last updated