빈(Bean), IoC 컨테이너, DI 알아보기
안녕하세요 🐸
오늘은 스프링을 공부하면 반드시 배우는 개념에 대해 정리해보겠습니다.
Bean
빈(Bean)은 ioc 컨테이너가 관리하는 객체를 의미합니다.
일반적으로 객체는 개발자가 직접 new
해서 생성하지만 빈으로 등록한 객체는 스프링에서 관리합니다.
이처럼 객체의 관리를 개발자가 아닌 스프링이 하는 것을 IoC(Inversion of Control) 제어 역전이라고 합니다.
빈으로 생성된 객체는 재사용 가능한 객채로써 DI, 싱글톤 패턴을 따르게 됩니다.
BeanFactory
스프링의 빈(Bean) 컨테이너에 접근하기 위한 루트 인터페이스입니다. 빈의 정의, 생성 및 관리를 담당합니다. DI를 지원하고 빈의 설정에 따라 객체를 반환합니다.
문서 내에서는 독립된 객체 혹은 단일 공유 객체를 반환한다 하며, 여기서 단일 공유 객체란 싱글톤 패턴에 사용되는 객체입니다.
++독립된 객체는 프로토타입 패턴 구조에서 사용되지만 저는 아직까지 프로토타입의 사용 경우를 보지못했습니다.
IoC (Inversion of Control)
IoC는 직역하면 제어의 역전을 의미합니다.
앞서 빈(Bean)은 개발자가 아닌 스프링이 그 생명주기를 관리한다 했습니다. 이처럼 개발자에게 제어권이 있는 것이 아니고 스프링에 제어권을 부여하는 것을 IoC라고 합니다.
DI와 IoC는 밀접한 관계를 갖는 용어이다보니 항상 함께 설명되고 이 둘의 설명 또한 차이가 모호한 경우가 많습니다.
하지만 DI와 IoC는 엄연히 다른 개념입니다. IoC는 객체의 제어 흐름을 스프링이 갖는 것을 뜻하며, DI는 의존성을 외부에서 주입받는 것입니다. DI는 IoC를 구현하는 방법으로 보는 것이 좋습니다.
IoC 컨테이너
IoC컨테이너는 DI 컨테이너, 스프링 컨테이너 등으로 불리기도 합니다. 통상적으로 전부 같은 뜻을 의미하고 있습니다.
IoC 컨테이너는 IoC를 실제 구현한 환경 등을 포함하는 개념입니다. 앞선 BeanFactory 와 같이 특정 클래스(혹은 인터페이스)를 지칭하는 말이 아닙니다.
이 IoC 컨테이너를 구현하기 위한 인터페이스로 사용 되는 것이 BeanFactory
, ApplicationContext
입니다.
ApplicationContext
은 BeanFactory
를 상속받고 추가적인 기능을 지원합니다. 다음은 공식 문서에서 설명하는 기능입니다.
- Bean factory methods for accessing application components. Inherited from ListableBeanFactory.
- The ability to load file resources in a generic fashion. Inherited from the ResourceLoader interface.
- The ability to publish events to registered listeners. Inherited from the ApplicationEventPublisher interface.
- The ability to resolve messages, supporting internationalization. Inherited from the MessageSource interface.
- Inheritance from a parent context. Definitions in a descendant context will always take priority. This means, for example, that a single parent context can be used by an entire web application, while each servlet has its own child context that is independent of that of any other servlet.
내용을 번역하여 간단하게 요약해보자면 다음과 같습니다.
- 빈 접근
- 다양한 파일 읽기
- 이벤트 리스너에게 이벤트 알림
- 다양한 언어 관리 지원
이런 IoC 컨테이너를 구현하기 위해 사용되는 클래스의 상관 관계 표 입니다.
출처 : https://dog-developers.tistory.com/12
DI(Dependency Injection)
지겹도록 많이 듣고 중요하다고 얘기하지만 사실 저처럼 개발 경력이 짧고 학습이 부족한 개발자들에게는 실질적으로 와닿는 개념은 아닐 것입니다.
왜냐하면 이미 모든 환경에서 DI를 사용중이기 때문에 이것이 당연한 것이라고 느껴지고 오히려 사용하지 않은 경우의 불편함을 생각하기가 어렵습니다.
DI(Dependency Injection)란?
DI는 직역하면 의존성 주입 이라는 뜻입니다. 어떠한 객체에 다른 객체의 의존성을 주입하는 것입니다.
개발 용어가 늘 그렇듯 의존성, 주입 등 어려운 용어들 뿐입니다.
좀 더 쉽게 설명 해보자면 객체 A
가 객체 B
를 필요로 한다면 객체 A
는 객체 B
를 반드시 필요로 하기 때문에 객체 A
는 객체 B
를 의존한다고 표현합니다.
그리고 객체 A
내에서 객체 B
를 직접적으로 생성하여 사용한다면 이것은 의존성은 있지만 의존성을 주입 했다고는 표현하지 않습니다.
주입 이란 말에서 알 수 있듯이 외부의 요인으로 영향을 받는 행위이기 때문에, 의존성을 객체A
가 만드는 것이 아니고 외부에서 주입 받을 경우를 얘기합니다.
DI의 이점
도와줘요 GPT!!📢
그래서 DI를 사용하기 전과 후를 비교해서 실질적으로 어떤 장점이 있는지를 비교해보겠습니다.
다음의 코드는 DI를 사용하지 않았을 때의 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class OrderService {
private PaymentService paymentService;
private ShippingService shippingService;
public OrderService() {
this.paymentService = new PaymentService();
this.shippingService = new ShippingService();
}
public void processOrder(Order order) {
paymentService.processPayment(order);
shippingService.shipOrder(order);
}
}
// 다른 클래스들
public class PaymentService {
public void processPayment(Order order) {
// 결제 처리 로직
}
}
public class ShippingService {
public void shipOrder(Order order) {
// 배송 처리 로직
}
}
다음은 DI를 사용한 이후의 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Service 클래스
public class OrderService {
private final PaymentService paymentService;
private final ShippingService shippingService;
@Autowired
public OrderService(PaymentService paymentService, ShippingService shippingService) {
this.paymentService = paymentService;
this.shippingService = shippingService;
}
public void processOrder(Order order) {
paymentService.processPayment(order);
shippingService.shipOrder(order);
}
}
// 다른 클래스들
public class PaymentService {
public void processPayment(Order order) {
// 결제 처리 로직
}
}
public class ShippingService {
public void shipOrder(Order order) {
// 배송 처리 로직
}
}
DI를 사용해서 얻을 수 있는 이점으로
- 클래스 변경시 최소한의 코드 수정
- 테스트 작성 용이 정도가 있습니다.
사실 코드만 이렇게 놓고 본다면 다른 부분을 꼽아 본다면 두 가지 정도입니다.
@Autowired
어노테이션의 사용 여부- 지역 변수
paymentService
,shippingService
의 생성 방법의 차이 하지만 이 별 것 아닌 몇 줄이 큰 차이였습니다(!!)
먼저 첫 번째 이점으로 얘기한 클래스 변경시 최소한의 코드 수정 에 대해 예시를 들어보겠습니다.
먼저, 다음의 상황이라고 가정해보겠습니다.
PaymentService
를NewPaymentService
로 바꿔야함.PaymentService
를 사용하는 코드가 500개가 넘음. 이런 상황일 때 DI를 사용하지 않은 경우 개발자는 모든 프로젝트 내 모든new PaymentService()
구문을 일일이 찾아서 수정 해야 합니다.
DI를 사용한 코드의 경우 PaymentService
와 ShippingService
을 외부에서 주입받고 있습니다.
이런 경우 NewPaymentService
를 구현할 때 PaymentService
를 상속받게 하여 @Service
어노테이션을 사용하는 것 만으로 최소한의 코드 변화로 작업 할 수 있습니다.
그리고 두 번째 이점으로 얘기한 테스트 작성 용이 에 대해서도 보겠습니다.
DI가 사용되지 않은 코드에서는 실제로 PaymentService
와 ShippingService
를 생성하여 사용하고 있기 때문에 만약 OrderService
의 단위 테스트를 목적으로 할 경우에 불편함이 있습니다.
반드시 PaymentService
와 ShippingService
가 구현이 되어있어야만 테스트를 해야 하기 때문입니다.
반면, DI를 사용한 경우 Mock 객체를 통해 실제 위 두 객체를 생성하지 않고도 테스트가 가능합니다.
참조 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/BeanFactory.html
참조 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationContext.html
참조 : https://dog-developers.tistory.com/12