의존성 주입을 알기 전에 Dependency(의존) 를 알아야 한다.
우선 의존성을 종속성이라고도 불린다
A가 B를 의존한다는 표현은 어떤 의미일까?
의존대상 B가 변하면 그것이 A에 영향을 미친다
B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에게 미친다.
public class ConsumerController {
private ConsumerNormalService consumerService;
public ConsumerController() {
this.consumerService = new ConsumerService();
}
}
현재 컨트롤러는 노말 서비스에 의존하고 있다.
서비스의 코드가 바뀌면 컨트롤러까지 영향이 미치는 것이다.
의존관계를 인터페이스로 추상화하기
위의 예시를 보면 컨트롤러는 노말 서비스에 의존하고 있다.
더 다양한 서비스 의존을 받을 수 있게 구현하려면 인터페이스로 추상화해야한다.
public class ConsumerController {
private ConsumerService consumerService;
public ConsumerController() {
this.consumerService = new ConsumerEventService();
}
}
public interface ConsumerService {}
public class ConsumerNormalService implements ConsumerService{}
public class ConsumerEventService implements ConsumerService{}
의존관계를 인터페이스로 추상화하면 더 다양한 의존관계를 맺을 수 있다.
실제 구현 클래스간의 관계가 느슨해지고, 결합도가 낮아진다.
의존성 주입은?
의존관계가 무엇인지 알아본 다음 의존성 주입에 대해 알아야한다.
지금까지 구현은 컨트롤러에서 내부적으로 서비스를 어떤 서비스를 가질지 직접 정하고 있다.
컨트롤러는 의존성을 직접 어떤 것을 주입할지 정하고 있다.
근데 외부에서 어떤 사장님이 이때 이벤트 해! 저때는 평소대로 해! 라고 지시를 내린다고 하자 그게 의존성 주입이다. 사장님이 외부에서 의존관계를 결정하고 주입하는 것
토비의 스프링에서는 다음 세가지 조건을 충족하는 작업을 의존성 주입이라고 얘기한다.
- 클래스 모델이나 코드에는 런타임 시전의 의존관계가 드러나기 않는다. 그러기 위해서는 인터페이스만 의존하고 있어야한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
의존성 주입 구현 방법
DI는 의존관계를 외부에서 결정하는 것이기 때문에, 클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법이다. 런타임 시점의 의존관계를 외부에서 주입하여 DI 구현이 완성 된다.
- 생성자 주입 방식
class ConsumerController {
private ConsumerService consumerService;
public ConsumerController(ConsumerService cosumerService) {
this.consumerService = consumerService;
}
}
class CosumerContainer {
private ConsumerController consumerController = new ConsumerController(new ConsumerNormalService()));
public void doEvent() {
consumerController = new ConsumerController(new ConsumerEventService());
}
}
- 메소드 이용 주입 방식
class ConsumerController {
private ConsumerService consumerService;
public void setConsumerService(ConsumerService consumerService) {
this.consumerService = consumerService;
}
}
class CosumerContainer {
private ConsumerController consumerController = new ConsumerController(new ConsumerNormalService()));
public void doEvent() {
consumerController.setConsumerService(new ConsumerEventService());
}
public void doNormal() {
consumerController.setConsumerService(new ConsumerNormalService());
}
}
DI의 장점은?
- 의존성이 줄어든다.
- 의존한다는 것은 의존대상의 변화에 취약하다는 것
- DI로 구현하게 되었을 때, 주입받는 대상이 변하더라고 그 구현 자체를 수정할 일이 없거나 줄어들게 된다.
- 재사용성이 높은 코드가 된다.
- 기존에 Controller 내부에서만 사용되었던 Service를 별도로 구분하여 구현하면, 다른 클래스에서 재사용할 수가 있다.
- 테스트하기 좋은 코드가 된다
- 모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉬워진다.
- 컨트롤러와 서비스의 테스트를 분리해서 진행할 수 있다.
- 가독성이 높아진다.
- Service의 기능들을 별도로 분리하게 되어 자연스레 가독성이 높아진다.
- 애플리케이션 의존성 방향이 일관되어진다.
- 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어주기 때문이다.
IoC
흐름의 제어를 IoC 외부 (컨테이너 혹은 프레임워크)로 이전하는 소프트웨어 엔지니어링의 원칙이다.
기존 쌩 자바 언어로는 개발자가 직접 흐름을 제어 하는 반면 SpringFramework에서는 IoC 컨테이너가 대신해준다. 그래서 개발자는 로직에 집중할 수 있는 장점이 있다.
- IoC 컨테이너는 객체의 생성을 책임지고, 의존성을 관리한다.
- POJO의 생성, 초기화, 서비스, 소멸에 대한 권한을 가진다.
- 개발자들이 직접 POJO를 생성할 수 있지만 컨테이너에게 맡긴다.
- 그래서 개발자는 비즈니스 로직에 더욱 집중할 수 있다.
- 객체 생성 코드가 없으므로 TDD가 용이하다
💡 POJO?
Plain Old Java Object
주로 특정 자바 모델이나 기능, 프레임워크를 따르지 않는 Java Object를 지칭한다.
IoC Container
💡 컨테이너?
컨테이너는 보통 객체의 생명주기를 관리, 생성된 인스턴스들에게 추가적인 기능을 제공해주는 개념이다.
스프링 프레임워크도 이런 컨테이너가 있는데 그게 IoC Container(= Spring Container) 이다.
'TIL' 카테고리의 다른 글
TIL 2024-02-07 Docker를 사용하는 이유 / 이미지, 컨테이너 (0) | 2024.02.08 |
---|---|
TIL 2024-02-06 EventPublisher / EventListener / 동기, 비동기 (0) | 2024.02.06 |
TIL 2024-02-02 OSI 7계층 요약 (0) | 2024.02.03 |
TIL 2024-02-01 통합 테스트 HttpRequest No thread-bound request found 오류 (0) | 2024.02.01 |
TIL 2024-01-31 로깅에 대한 고민을하다 알게된 글~~ (0) | 2024.02.01 |