Spring Framework를 공부하기 위해 책이나 강의를 접하면, 굉장히 DI(Dependency Injection)에 관한 설명을 많이 듣게 됩니다.
Spring Framework의 핵심 개념 IoC, DI, AOP 중에 하나이기 때문이기도 하겠지만요 😎
사전적 정의, Definition만으로는 우리의 지적 호기심을 채워주기는 어렵고 너무 딱딱해서 이해하기 어려울 때가 많습니다.
그래도 Definition을 가볍게 읽어보고 시작하겠습니다 ~ 😁 그럴거면 어렵다고 하지를 말던가..(ㅍㅍㅍ)
DI는 Dependency Injeciton의 약자로서, 의존성 주입을 말합니다.
필요한 의존성을 직접 주입받게 되면 결합도가 증가하고 이로 인하여 변경이 생길 경우 변경되는 모듈이 한 개 이상이 되는 좋지 못한 설계를 할 가능성이 높아집니다.
설계 관점에서는 결합도는 낮고, 응집도가 높아야 유연한 설계라고 볼 수 있기 때문에 결합도(Coupling)이 높으면 좋지 않습니다.
의존성을 주입하는 모듈과 의존성을 주입받는 모듈 사이에 Dependency Injector(의존성 주입자)를 두어 직접적으로 모듈을 주입하지 않도록 하여 느슨한 결합을 유도하고 이를 통해 Decoupling 즉, 결합도가 낮아지는 효과를 얻을 수 있습니다.
물론 모든 기술이나 방법론에는 절대 좋은 점만 존재하지 않습니다.
장점으로는 위에서 말씀드린대로 결합도가 낮아지는 것을 꼽을 수 있고, 단점으로는 클래스의 개수가 증가해 약간의 런타임 패널티가 존재할 수 있다는 점이 있습니다.
위의 설명은 면접 질문 CS를 공부할 때 외우고 공부했던 극히 사전적인 정의를 약간 풀어서 쓴 것이기 때문에 아무래도 이해하기에 좋은 설명은 아닐 것입니다.
이제는 DI가 무엇인지, 어떤 방법으로 이루어지는지 차근 차근 살펴보도록 하겠습니다.
DI의 핵심은 객체를 직접 생성하는 것이 아니라, 외부에서 생성한 후 주입을 시켜주는 방식입니다.
예제를 하나 살펴보도록 하겠습니다.
A라는 객체에서 B,C라는 객체를 사용한다면 아래의 그림과 같이 두 가지 방법이 있습니다.
방법 1 : A객체가 B와 C객체를 New 생성자를 통해서 직접 생성하는 방법
방법 2 : 외부에서 생성 된 객체를 setter( )나 생성자를 통해 사용하는 방법
Spring에서는 다른 객체들이 사용하고, 다른 서비스를 위해 사용할 수 있는 클래스를 컨테이너 형태로 제공해줍니다. A라는 객체에서 B,C 객체를 사용 (의존)할 때 A객체에서 직접 생성을 하는 것이 아닌 외부(IoC 컨테이너)에서 생성된 B,C 객체를 주입시켜 사용할 수 있게 되는 것입니다.
스프링에서는 객체를 Bean 이라고 부릅니다.
프로젝트가 실행될때 사용자가 Bean으로 관리되는 객체들의 생성과 소멸에 관련한 작업(Life-Cycle)을 자동적으로 수행해주게 되는데, 객체가 생성되는 곳을 스프링에서는 Bean Container라고 부릅니다.
일단 Spring의 공식 reference에서도 가장 권장하는 방식인 생성자 주입(Counstructor Injection)이 가장 많이 사용되고 있습니다.
Spring에서 의존성을 주입할 수 있는 방법 3가지가 있는데 지금 부터 한개씩 알아보도록 하겠습니다.
생성자 주입은 말 그대로 객체를 생성할때 생성자를 사용해 주입받는 방식입니다.
@Controller
public class testController {
// final을 붙일 수 있다.
private final TestService testService;
// 생략도 가능하다
@Autowired
public TestController(TestService testService) {
this.testService = testService;
}
}
클래스의 생성자가 하나이고, 그 생성자로 주입받을 객체가 빈으로 등록되어 있다면 @Autowired는
생략 가능합니다.
대부분 많이 사용하고 있는, Lombok 라이브러리를 사용 중 이라면, 생성자 주입을 더 간단히 할 수 있습니다.
@Controller
@RequiredArgsConstructor
public class testController {
// final을 붙일 수 있다.
private final TestService testService;
}
주입 받을 객체를 final로 선언하고, @RequiredArgsConstructor 애너테이션만 붙여주면 됩니다. 자세한 내용은 Lombok에 대해서 검색해보시길 권합니다.
Setter 메서드에 @Autowired 애너테이션을 붙이는 방법으로 주입하는 방식입니다.
@Controller
public class TestController {
private TestService testService;
@Autowired
public void setTestService(TestService testService) {
this.testService = testService;
}
}
수정자 주입의 단점으로는 setXXX 메서드를 public으로 열어두어야 하기 때문에 언제 어디서든 변경이 가능하다는 점이 있습니다.
필드에 @Autowired 애너테이션만 붙여주면 자동으로 의존성이 주입되는 방법으로, 매우 간단하게 사용할 수 있기 때문에 가장 많이 접할 수 있는 방법입니다.
@Controller
public class TestController {
@Autowired
private TestService testService;
}
필드 주입은 사용법이 매우 간단하다는 장점이 있지만, 두 가지 단점이 존재하고 있습니다.
필드 주입의 단점
1. 순환참조 방지
개발을 하다 보면 본의 아니게 여러 컴포넌트 간에 의존성이 생깁니다.
필드 주입과 수정자 주입은 빈이 생성된 후에 참조를 하기 때문에, 어플리케이션이 아무런 오류나 경고 없이 구동이 됩니다. 풀어서 설명하면, 실제 코드가 호출될 때까지 문제를 알 수 없다는 것입니다.
반면에 생성자를 통해 주입하고 실행하면 BeanCurrentlyInCreationException이 발생하게 됩니다. 순환 참조 뿐만아니라 의존 관계의 내용을 외부로 노출 시킴으로써 어플리케이션을 실행하는 시점에서 오류를 체크 할 수 있습니다.
2. 불변성(Immutability)
생성자로 의존성을 주입할때 보통 final로 많이 선언을 합니다. Lombok과의 연동을 위해 아무런 생각없이 여지껏 그렇게 주입을 해왔지만 사실 이렇게 하는 이유는 불변성에 있었습니다.
final로 선언하게 되면 런타임에서 의존성을 주입받는 객체가 변할 일이 없습니다. 하지만 수정자 주입이나 일반 메서드 주입을 이용하게 되면 불필요한 수정의 가능성을 열어두게 되는 것이죠.
또한 이는 OOP 설계 5원칙 중 하나인 OCP(Open-Closed Principal, 개방-폐쇄의 원칙)를 위반하게 됩니다.
필드 주입 방식은 null이 만들어질 가능성도 있는데, final로 선언한 생성자 주입 방식은 null이 불가능 합니다. 그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋습니다.
3. 테스트에 용이하다.
생성자 주입을 사용하게 되면 테스트 코드를 좀 더 편리하게 작성할 수 있다고 합니다.
DI의 핵심은 관리되는 클래스가 DI 컨테이너에 의존성이 없어야 한다는 것이 핵심입니다.
독립적으로 인스턴스화가 가능한 POJO(Plain Old Java Object)여야 한다는 것입니다.
POJO에 대해서는 다음에 추가적으로 다뤄 보도록 하겠습니다.
여지껏 Lombok의 @RequiredArgsConstructor 애너테이션을 붙이며 기계적으로 final 키워드를 사용해서 생성자 주입 이라는 미명하에 아무 생각이나 원리 없이 주입해온 나날을 반성하게 된 게시글 인 것 같습니다. 선배 개발자들님들은 DI의 주입 방식 하나 하나에도 섬세한 설계 원칙과 다양한 케이스들을 분석해서 어떤 것을 권장하는지 이유까지 공식 Reference에 있을 줄 몰랐습니다.
이제서야 왜 공식 Doc..Doc..(개 아닙니다 🤣) 했는지 알 것 같습니다.
앞으로도 원리와 기술의 사용 이유에 대해 더 깊이 탐구할 수 있는 제가 되길 바라며 마칩니다.