IoC, DI 그리고 컨테이너
스프링의 중요한 특징인 IoC와 DI에 대해서 정리해보려고 한다.
1. 제어의 역전 IoC (Incersion of Control)
프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라고 한다.
기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다.
원래 프로그램은 main()에서 시작해 사용할 오브젝트를 결정하고 그 오브젝트를 만들어 안에 있는 메소드를 호출하는 일을 반복한다.
모든 권한을 제 3자에게 넘긴다. 그냥 기능만 구현해 놓으면 자신은 필요할 때 호출되어 사용되는 수동적인 구조다.
제어의 역전은 하나의 설계 원칙이고 디자인 패턴이라고도 함.
※ IoC의 대표적인 예제!
- 프레임워크 VS 라이브러리
- 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit)
- 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.
2. 의존관계 주입 DI(Dependency Injection)
하나의 객체에서 다른 객체가 필요할 때, 객체를 직접 생성하지 않고, 이미 생성되어 있는 객체를 가져오는 작업을 "DI (Dependency Injection)" 혹은 한국말로 "의존성(의존관계) 주입"이라고 부른다.
객체를 사용할 때 객체에서 다른 객체를 new 하여 직접 생성하는 방식이 있고, 객체 외부에서 new로 생성되어 객체 내부로 주입하여 사용하는 방식이 있다.
의존성 주입은 이중 객체 외부에서 내부로 주입받아서 사용하는 방식!
의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
👉 만약 ProductRepository 클래스 생성 과정에서 DB의 id, password 정보를 받아야 하는 조건이 생겼다고 해 봅시다.
public class ProductRepository {
private String dbId;
private String dbPassword;
// 생성자
public ProductRepository(String dbId, String dbPassword) {
this.dbId = dbId;
this.dbPassword = dbPassword;
}
}
👉 ProductRepository 클래스 생성자가 변경되면, ProductRepository 를 생성하는 ProductService 클래스 코드도 변경되야 합니다.
public class ProductService {
// 멤버 변수 선언
private final ProductRepository productRepository;
// 생성자: ProductService() 가 생성될 때 호출됨
public ProductService(String dbId,String dbPassword) {
// 멤버 변수 생성
this.productRepository = new ProductRepository(dbId, dbPassword);
}
}
👉 그리고 만약 ProductService 에서도 DB 의 id, password 를 생성자에서 받아서 처리해야 한다면, ProductController 클래스 코드도 변경되어야 하죠.
public class ProductController {
// 멤버 변수 선언
private final ProductService productService;
// 생성자: ProductController() 가 생성될 때 호출됨
public ProductController() {
// 멤버 변수 생성
String dbId = "sa";
String dbPassword = "";
productService = new ProductService(dbId, dbPassword);
}
}
👉 이렇게 하나의 클래스 생성자 변화가 다른 클래스에 영향을 주는 이유는 "강한 결합" 이 되어 있기 때문입니다.
그렇다면 위에서 살펴본 문제점 (중복코드, 강한 결합)을 해결할 방법이 있을까?
일단, 각 객체는 1개만 필요하니 객체 생성은 1번만 하고, 생성된 객체가 필요할 때 가져다 사용하는 건 어떨까?
예를 들어, productRepository가 미리 생성되어 있다면 어떨까?
String dbId = "sa";
String dbPassword = "";
ProductRepository productRepository = new ProductRepository(dbId, dbPassword);
👉 ProductService 클래스에서는 미리 생성되어 있는 ProductRepository 객체를 그냥 가져다 사용
public class ProductService {
// 멤버 변수 선언
private final ProductRepository productRepository;
// 생성자: ProductService() 가 생성될 때 호출됨
public ProductService(ProductRepository productRepository) {
// 멤버 변수 생성
this.productRepository = productRepository;
}
}
ProductService productService = new ProductService(productRepository);
👉 ProductController 클래스도 마찬가지로 미리 생성되어 있는 ProductService 를 사용
public class ProductController {
// 멤버 변수 선언
private final ProductService productService;
// 생성자: ProductController() 가 생성될 때 호출됨
public ProductController(ProductService productService) {
// 멤버 변수 생성
this.productService = productService;
}
}
ProductController productController = new ProductController(productService);
👉 이 상태에서는 만약 ProductRepository 에 DB의 URL이 추가로 필요해졌다고 가정
public class ProductRepository {
private String dbId;
private String dbPassword;
// 추가!!
private String dbUrl;
// 생성자
public ProductRepository(String dbId, String dbPassword, String dbUrl) {
this.dbId = dbId;
this.dbPassword = dbPassword;
this.dbUrl = dbUrl;
}
}
👉 이제는 ProductRepository 생성자에 어떠한 변화가 생겨도, ProductService, ProductController 클래스를 모두 고칠 필요가 없어짐. ProductRepository 가 만들어지는 곳에서만 변경하면 됨.
※ 그래서 제어의 역전과 의존성 주입의 차이점은?
제어의 역전(Inversion of Control)은 일반적인 디자인 패턴 중 하나이고
의존성 주입(DI)는 이런 제어의 역전 패턴을 달성하는 방법 중 하나!
3. IoC 컨테이너, DI 컨테이너
객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
참고 : 내일배움캠프 spring 강의, 인프런 김영한님 스프링핵심원리 강의,
'Spring' 카테고리의 다른 글
정적 타입 언어 vs 동적 타입 언어 (0) | 2022.02.08 |
---|---|
Redis란? (0) | 2021.12.20 |
IntelliJ 단축키 정리 (0) | 2021.11.11 |
좋은 객체 지향 설계의 5가지 원칙 (SOLID) (0) | 2021.11.10 |
스프링(Spring)이란? (0) | 2021.11.04 |