Spring 프레임워크는 3가지 핵심 프로그래밍 모델을 지원하는데, 그 중 하나가 의존성 주입(Dependency Injection, DI)이다.
DI란 객체를 직접 생성하는 게 아니라 외부에서 생성한 후 주입
시켜주는 방식이다.
외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴으로, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해준다.
public class Store {
private Product product;
}
두 객체 간의 관계(의존성)를 맺어주는 것을 의존성 주입이라고 하며 생성자 주입, 필드 주입, 수정자 주입 등 다양한 주입 방법이 있는데, Spring 개발팀에서는 생성자 주입을 권장하고 있다.
생성자 주입(Constructor Injection)은 생성자를 통해 객체를 생성할 때 의존 관계를 주입하는 방법이다.
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
}
생성자 주입은 생성자의 호출 시점에 1회 호출 되는 것이 보장된다. 그렇기 때문에 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용할 수 있다.
수정자 주입(Setter 주입, Setter Injection)은 객체 생성 후, Setter로 의존성을 주입하는 방식이다.
Setter 주입은 생성자 주입과 다르게 주입받는 객체가 변경될 가능성이 있는 경우에 사용한다. (실제로 변경되는 경우는 드물다.)
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
필드 주입(Field Injection)은 필드에 바로 의존 관계를 주입하는 방법이다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
코드가 간결해져서 과거에 상당히 많이 이용되었던 주입 방법이다. 하지만 필드 주입은 외부에서 접근이 불가능하다는 단점이 존재하는데, 테스트 코드의 중요성이 부각됨에 따라 필드의 객체를 수정할 수 없는 필드 주입은 거의 사용되지 않게 되었다. 또한 필드 주입은 반드시 DI 프레임워크가 존재해야 하므로 반드시 사용을 지양해야 한다.
public class Store {
private Fruit fruit;
public Store() {
this.fruit = new Fruit;
}
위와 같은 Store 클래스는 현재 Fruit 클래스와 강하게 결합되어 있다는 문제점이 있다.
만약 Store에서 Fruit 아닌 무언가 다른 것을 판매하고자 한다면 Store클래스의 생성자에 변경이 필요하다. 즉, 유연성이 떨어진다.
이에 대한 해결책으로 상속을 떠올릴 수 있지만, 상속을 제약이 많고 확장성이 떨어지므로 피하는 것이 좋다.
위의 Store와 Fruit은 객체들 간의 관계가 아니라 클래스들 간의 관계가 맺어져 있다는 문제가 있다.
올바른 객체지향적 설계라면 객체들 간에 관계가 맺어져야 한다.
객체들 간에 관계가 맺어졌다면 다른 객체의 구체 클래스(Fruit인지 Milk 인지 등)를 전혀 알지 못하더라도, (해당 클래스가 인터페이스를 구현했다면) 인터페이스의 타입(Product)으로 사용할 수 있다.
결국 위와 같은 문제점이 발생하는 근본적인 이유는 Store에서 불필요하게 어떤 제품을 판매할 지에 대한 관심이 분리되지 않았기 때문이다. Spring에서는 DI를 적용하여 이러한 문제를 해결하고자 한다.
Pencil, Food 등 여러 가지 제품을 하나로 표현하기 위해서는 Product 라는 Interface가 필요하다.
public interface Product {}
public class Fruit implements Product {}
이제 Store와 Fruit이 강하게 결합되어 있는 부분을 제거해주어야 하는데, 이를 위해서는 외부에서 상품을 주입(Injection)받아야 한다. 그래야 Store에서 구체 클래스에 의존하지 않게 된다.
public class Store {
private Fruit fruit;
public Store(Fruit fruit) {
this.fruit = fruit;
}
}
여기서 Spring이 DI 컨테이너가 필요한 이유를 알 수 있는데, 우선 Store에서 Fruit 객체를 주입하기 위해서는 애플리케이션 실행 시점에 필요한 객체(Bean)를 생성해야 하며, 의존성이 있는 두 객체를 연결하기 위해 한 객체를 다른 객체로 주입시켜야 하기 때문이다.
다음과 같이 Fruit 이라는 객체를 만들고, 그 객체를 Store로 주입시켜주는 역할을 위해 DI 컨테이너가 필요하게 된 것이다.
public class BeanFactory {
public void store() {
Fruit fruit = new Fruit(); // Bean의 생성
// 의존성 주입
Store store = new Store(fruit); // 의존성 주입
}
}
IoC(Inversion of Control)란 제어의 역전
이라는 의미로, 말 그대로 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부에서 결정되는 것을 의미한다.
객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유지 보수를 편하게 할 수 있게 한다.
스프링에서는 다음과 같은 순서로 객체가 만들어지고 실행된다.
스프링이 모든 의존성 객체를 스프링이 실행될때 다 만들어주고 필요한곳에 주입시켜줌으로써 Bean들은 싱글톤 패턴
의 특징을 가지며, 제어의 흐름을 사용자가 컨트롤 하는 것이 아니라 스프링에게 맡겨 작업을 처리하게 된다.
📚참고
https://mangkyu.tistory.com/150
https://velog.io/@gillog/Spring-DIDependency-Injection