[Spring] DI 의존성 주입 | Field 주입, Setter 주입, 생성자 주입
Spring은 @Autowired 어노테이션을 이용한 다양한 의존성 주입(DI: Dependency Injection) 방법을 제공한다.
의존성 주입은 new 연산자를 통해 필요한 객체를 직접 생성하는 것이 아니라 외부로부터 객체를 받아 사용하는 것이다.
이때 외부란 추상적으로 스프링 컨테이너라는 애플리케이션 내 별도의 공간을 의미한다.
의존성 주입(DI)이 필요한 이유
예를 들어 연필이라는 상품과 1개의 연필을 판매하는 Store 클래스가 있다고 하자.
public class Store {
private Pencil pencil;
public Store() {
this.pencil = new Pencil();
}
}
위와 같은 예시 클래스는 이러한 문제점을 가지고 있다.
- 두 클래스가 강하게 결합되어 있다.
만약 Store에서 Pencil이 아닌 Food와 같은 다른 상품을 판매하고자 하면, Store 클래스의 생성자에 변경이 필요하고, 이는 유연성이 떨어진다.
- 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어진다.
올바른 객체지향적 설계라면 객체들 간에 관계가 맺어져야 한다. 객체들 간에 관계가 맺어졌다면 다른 객체의 구체 클래스(Pencil인지 Food인지 등)를 전혀 알지 못하더라도, 해당 클래스가 인터페이스를 구현했다면 인터페이스의 타입(Product)으로 사용할 수 있다.
의존성 주입(DI)을 통한 문제 해결
위와 같은 문제를 해결하기 위해서는 우선 다형성이 필요하다.
Pencil, Food 등 여러 가지 제품을 하나로 표현하기 위해서는 Product라는 Interface가 필요하다.
먼저 Pencil에서 Product 인터페이스를 구현한다.
public interface Product {
}
public class Pencil implements Product {
}
이제 우리는 Store와 Pencil이 강하게 결합되어 있는 부분을 제거해 주어야 한다.
이를 제거하기 위해 다음과 같이 외부에서 상품을 주입(Injection) 받아야 한다.
public calss Store {
private Product product;
public Store(Product product) {
this.product = product;
}
}
이러한 이유로 우리는 Spring이라는 DI 컨테이너를 필요로 하는 것이다.
Store에서 Product 객체를 주입하기 위해서는 애플리케이션 실행 시점에 필요한 객체(빈)을 생성해야 하며, 의존성이 있는 두 객체를 연결하기 위해 한 객체를 다른 객체로 주입시켜야 한다.
public class BeanFactory {
public void store() {
// Bean의 생성
Product pencil = new Pencil();
// 의존성 주입
Store store = new Store(pencil);
}
}
Spring은 의존성 주입을 도와주는 DI 컨테이너로써,
강하게 결합된 클래스들을 분리하고, 애플리케이션 실행 시점에 객체 간의 관계를 결정해 줌으로써
결합도를 낮추고 유연성을 확보해준다.
이러한 방법은 상속보다 훨씬 유연하다. 단, 한 객체가 다른 객체를 주입받으려면 반드시 DI 컨테이너에 의해 관리되어야 한다는 것이다.
의존성 주입의 3가지 방법
1. 생성자 주입(Constructor Injection)
2. 필드 주입(Field Injection)
3. 수정자 주입(Setter Injection)
생성자 주입(Constructor Injection)
@Controller
public class helloController {
// final 붙일 수 있음
private final helloService helloService;
//@Autowired
public helloController(helloService helloService) {
this.helloService = helloService;
}
}
클래스의 생성자가 하나이고, 그 생성자로 주입받을 객체가 빈으로 등록되어 있다면 @Autowired를 생략할 수 있다.
필드 주입(Field Injection)
@Controller
public class helloController {
@Autowired
private helloService helloService;
}
필드에 @Autowired 어노테이션만 붙여주면 자동으로 의존성이 주입된다.
코드가 간결하지만, 외부에서 변경하기 힘들다.
또한 프레임워크에 의존적이고 객체지향적으로 좋지 않다.
수정자 주입(Setter Injection)
@Controller
public class helloController {
private helloService helloService;
@Autowired
public void setHelloService(helloService helloService) {
this.helloService = helloService;
}
}
set 메서드를 public으로 열어두어야 하기 때문에 언제 어디서든 변경이 가능하다는 단점이 있다.
생성자 주입을 권장하는 이유
Spring Framework reference에서 권장하는 방법은 생성자를 통한 주입이다.
@Autowired 어노테이션 만으로 간단하게 의존성을 주입할 수 있는데, 왜 생성자 주입 방법을 권장하는 것일까?
1. 순환 참조를 방지할 수 있다.
필드 주입과 수정자 주입은 빈이 생성된 후에 참조를 하기 때문에 어플리케이션이 아무런 오류 그리고 경고 없이 구동된다.
반면, 생성자를 통해 주입하고 실행하면 BeanCurrentlyInCreationException이 발생하게 된다.
2. 불변성
생성자로 의존성을 주입할 때 final로 선언할 수 있고, 이로 인해 런타임에서 의존성을 주입 받는 객체가 변할 일이 없어지게 된다.
하지만 수정자 주입이나 일반 메소드 주입을 이용하게 되면 불필요하게 수정의 가능성을 열어두게 되고,
이는 OOP의 5가지 원칙 중 OCP(Open-Closed Principal, 개방-폐쇄의 원칙)를 위반하게 된다.
그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋다.
또한 final로 선언한 생성자 주입 방식은 null이 불가능하다.
3. 테스트에 용이하다.
생성자 주입을 사용하게 되면 테스트 코드를 좀 더 편리하게 작성할 수 있다.
독립적으로 인스턴스화가 가능한 POJO를 사용하면, DI 컨테이너 없이도 의존성을 주입하여 사용할 수 있다.
https://dev-coco.tistory.com/70
https://mangkyu.tistory.com/150