[Spring] 스프링 DI(Dependency Injection)은 어떻게 이루어지나
IoC (Inversion of Control)
DI를 알아보기 전에 알아야 할 용어가 IoC입니다. IoC는 "제어의 역전"이라는 말로 메서드나 객체의 호출 작업을 개발자가 결정하는 것이 아니라 외부에서 결정되는 것을 의미합니다. 여기서 외부는 Spring Framework입니다.
기존에는 객체를 클래스 내부에서 생성하고 사용했지만 IoC를 적용하면 미리 생성해놓은 객체를 주입받아 사용하기만 하면 됩니다. 이를 통해 얻을 수 있는 장점은 아래와 같습니다.
제어의 역전의 장점
- 객체 간 결합도를 낮춘다
- 유연한 코드 작성 가능
- 가독성 증진
- 코드 중복 방지
- 유지 보수 용이
DI (Dependency Injection)
위에서 알아본 IoC에서 객체를 주입받아 사용한다고 했습니다. 바로 이것이 DI이죠. 즉, DI를 통해서 IoC를 이룰 수 있는 것입니다.
DI를 한글로 풀이하면 의존성 주입이죠. 처음에 DI에 대해서 들었을 때는 정확하게 이해가 가지 않았습니다. 하지만 대충 넘어가게 됐죠.
객체를 외부에서 넣어준다는 건 알겠는데 이게 왜 의존성(Dependency)이지?
일단 프로그래밍 설계를 공부하다 보면 결합도(Coupling), 응집도(Cohesion)을 들어보셨을 겁니다.
이 중에 결합도는 다른 모듈과의 의존성 정도라고 할 수 있습니다.
예를 들어 A 클래스를 사용하는데 내부 로직 상 B, C 클래스의 메소드를 활용하는데 그 응답에 따라 진행하는 로직이 변경된다고 가정합니다. 만약 유지보수 과정에서 B,C 클래스의 응답형식이 변경된다면 A클래스도 변경되어야 할겁니다. 그리고 A클래스에서 로직이 변경되어 B,C 클래스로 보내는 파라미터 객체에 변경이 되어진다면 이를 기존과 같게 변경하는 로직을 추가해야할것 같습니다.
즉, B,C 클래스에 의존도가 높은 코드를 작성하게 되고 이는 결합도가 높다고 합니다.
위의 의존도와 결합도의 관계를 유의하며 DI의 개념부터 살펴보겠습니다. 간단하게 객체를 이용하는데 필요한 의존 중인 객체들을 외부에서 주입받는 방식입니다. 클래스 내에서 new A(), new B()를 하지 않고 외부에서 이미 생성되어 있는 인스턴스를 가져와서 사용하게 되죠.
기존의 방식대로 A 객체 내에서 생성하는 방식이 아니기 때문에 B, C를 로직이 허용하는 한 마음대로 갈아 끼울 수 있게 됩니다. 이를 통해서 앞에서 얘기한 의존성을 낮출 수 있게 되고 즉, 결합도를 낮추게 되는 장점이 생깁니다.
그렇다면 갈아 끼운다고 표현했는데 어떤 방식이 있을까요? 아래의 3가지 방법이 있습니다.
DI를 하는 방식
- Field Injection(필드 주입)
- Setter Injection(수정자 주입)
- Constructor Injection(생성자 주입) (*추천)
@Component
public class SampleComponent {
//Field Injection
@Autowired
private FieldService fieldService;
//Setter Injection
private SetterService setterService;
@Autowired
public void setSetterService(SetterService setterService) {
this.setterService = setterService;
}
//Constructor Injection
private final ConstructorService constructorService;
@Autowired
public SampleComponent(ConstructorService constructorService) {
this.constructorService = constructorService;
}
}
위에서 가장 추천하는 방식은 생성자 주입이고 Spring에서도 권장하는 방식입니다.
생성자 주입 외에 2가지 방식은 어떠한 단점이 있는지 알아보겠습니다.
Field Injection(필드 주입)의 단점
- SRP(단일 책임 원칙)의 원칙을 해칠 가능성이 높아진다.
- 간단한 사용법 아래 개수 제한 없이 무한정 추가하게 되고 Class가 많은 책임을 떠안게 된다
- 의존성이 숨는다.
- 필드 주입의 경우에는 외부에서 해당 클래스가 어떤 의존성의 책임을 지고 있는지 알 수 없다.
- DI 컨테이너의 결합성과 테스트 용이성
- DI 프레임워크의 핵심은 관리되는 Class가 DI Container에 의존성이 없어야 한다. 이는 DI Container가 필요한 의존성을 전달만 하면 그 이후엔 독립된 개체가 되어야 한다는 것이다.
- 독립된 개체가 되면 Test에서도 용이하다.
- 하지만 필드 주입은 DI Container에 의존성을 가지게 된다.
- 불변성을 활용할 수 없다
- 필드 주입은 final을 선언할 수 없다.
Setter Injection(생성자 주입)의 단점
- NullPointerException이 발생할 수 있다.
- 선택적 주입을 할 수 있기 때문에 클래스 인스턴스 생성 시에 의존성 주입을 해주지 않아도 생성이 된다. 이러한 상황에서 사용하면 NullPointerException이 발생한다.
Constructor Injection(생성자 주입)을 사용해야 하는 이유
- NullPointerException의 발생을 막는다.
- 기존 Setter Injection의 단점을 보완한다. 생성시에 의존성 없이는 생성 불가능하기 때문에 일부러 null을 주입하지 않는 이상 발생하지 않는다.
- 불변성을 활용할 수 있다.
- final로 선언 가능하다.
- SRP(단일 책임 원칙)을 지킬 수 있도록 유도한다.
- 사용하기가 약간 불편하고 생성자의 코드가 길어짐을 느낄 수밖에 없게 되어 있다. 따라서 코드가 길어진다면 단일 책임 원칙을 다시 한번 생각하게 해 준다.
DI는 누가 해주는 걸까?
위에서 DI의 개념과 결합도를 낮춘다는 장점까지 알아보고 어떻게 주입까지 하는지 알아봤습니다.
그렇다면 이걸 수행하는 주체는 누구일까?
간단히 생각하면 Spring Framework가 해주는 겁니다. 하지만 정확하게 Spring Framework내에서 어떤 요소가 해주는지를 알아보겠습니다.
Bean (빈 객체)
Spring에서 가장 먼저 듣게 되는 것이 Bean입니다. 대충 보면 그냥 일반적으로 생성한 객체와 다를 바가 없어 보입니다. 일반 객체와 빈의 차이점은 누가 관리하냐입니다.
Spring IoC 컨테이너가 관리하는 자바 객체를 Bean(빈)이라고 부릅니다. 즉 new 연산자로 생성해서 개발자가 관리하는 객체가 아닌 등록만 해주면 스프링 컨테이너가 관리하는 객체입니다. 그리고 여기서 스프링 컨테이너는 코드 상에서는 ApplicationContext라는 인터페이스입니다.
위의 그림과 같이 Spring Container 안에는 Bean 저장소가 있습니다. 우리가 등록을 요청하면 Bean 저장소에 저장이 되고 이를 사용하게 되는 거죠.
Bean 저장 형식
- Bean 이름 + Bean 객체
- Bean 이름은 어노테이션으로 등록하는 방식 기준으로 @Bean 어노테이션이 붙은 메서드 이름을 사용
- @Bean(name = "...") 방식으로 커스텀 가능
- Bean 객체는 Singleton scope(Default), Prototype scope로 정의되고 사용됨
Bean의 의존관계 주입
Bean 객체를 생성한다고 모두 끝난 것은 아닙니다. 우리가 설정해놓은 의존관계들을 스프링 컨테이너가 인식하고 필요할 때 주입시켜줘야 하죠. 그래서 스프링 컨테이너는 Bean 생성 후에 의존관계를 주입합니다. (추가. 이는 XML로 Bean과 의존관계를 설정해주는 방식을 사용하면 명확히 보입니다)
- 스프링 컨테이너는 파라미터로 넘어온 클래스 정보에 따라 의존관계를 주입
- 동적인 객체 인스턴스 관계를 컨테이너가 연결
BeanFactory
그럼 ApplicationContext가 Bean을 조회, 관리하는 기능을 제공하는 인터페이스인가?
Spring에 대해서 공부를 하던 중 얼핏 들었던 것 중에 BeanFactory라는 게 있었습니다. 하지만 거의 관심이 없다가 이번에 알게 되었습니다.
Bean을 조회하고 관리하는 기능은 ApplicationContext도 BeanFactory라는 최상위 인터페이스를 상속받아서 제공하고 있는 기능들입니다.
BeanFactory 개요
- 스프링 컨테이너의 최상위 인터페이스
- Bean을 조회하고 관리하는 기능 제공
- ApplicationContext는 BeanFactory의 기능에 부가 기능이 조금 더 더해진 것
- 직접 사용할 일은 거의 없다
정리
DI에 대해서 개념부터 Bean에 대해서까지 얄팍하게 알아보았습니다. 위의 내용을 정리하면 아래와 같습니다.
- 의존 중인 객체를 외부로부터 주입받는 방식이 DI이다.
- DI를 통해 IoC를 이루고 이는 결합도를 줄이고 가독성을 높이고 유지 보수를 편하게 해 준다.
- DI를 수행하는 것은 스프링 컨테이너(= ApplicationContext)이다.
- Bean 객체는 스프링 컨테이너가 관리하는 객체를 의미한다.
- 스프링 컨테이너 내부에 Bean 저장소에 Bean 객체의 정보를 저장한다.
- Bean은 기본적으로는 싱글톤 스코프로 생성, 관리되고 프로토타입 스코프도 사용 가능하다.
- ApplicationContext는 BeanFactory라는 최상위 인터페이스를 상속받는다.