[Spring] 의존성주입(DI)이란?
Spring프레임워크의 3가지 핵심 프로그래밍 모델중 하나로,
외부에서 두 객체간의 관계를 결정해주는 디자인패턴으로 인터페이스를 사이에 두고 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임시에 관계를 동적으로 주입하여 결합도를 낮출수 있게 하는 기법이다.
DI (Dependency Injection)
의존성 주입은 IoC(Invesoin of Control, 의존성 역전) 원칙하에 객체간의 결합을 약하게해주고 유지보수가좋은 코드를 만들어준다.
즉, 외부에서 생성된 객체를 이용하는 것이다.
한 객체가 어떤 객체에 의존할것인지는 별도의 관심사이다. DI컨테이너를 통해 서로 강하게 결합되어있는 두 클래스를 분리하고,
두 객체간 관계를 결정해줌으로서 결합도를 낮추고 유연성을 확보하고자 한다.
(이때 다른 빈을 주입받으려면 자기자신도 반드시 컨테이너의 빈이여야 한다.)
예를들어, 배터리(의존객체)를 사용하는 장난감(어떤객체)에게 배터리를 넣어주는것을 의존성주입이라고 생각하면 좋다.
배터리 일체형의 경우 : 생성자에서만 의존성을 주입해주는 상황이라 배터리가 떨어지면 다른 배터리로 교체하는것이 아니라 새로운 장난감으로 바꿔줘야한다.
배터리 분리형의 경우 : setter, 생성자를 이용해서 외부에서 주입해주는 상황은 외부에서 배터리를 교체해줄수 있기 때문에 일체형보다 유연하다.
강한결합
객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조이다.
A클래서 내부에서 B라는 객체를 직접 생성하고 있다면 (new연산자를 통해) B객체를 C객체로 바꾸고 싶은 경우에 A클래스도 수정해야 하는 방식이기 때문에 강한 결합이다.
public class Pencil {
}
public class Store {
private Pencil pencil;
public Store() {
this.pencil = new Pencil();
}
}
예를들어, Store클래스에서 Pencil을 판매하고자하여 new연산자를 통해 pencil객체를 Store클래스내부에서 생성할경우, Food로 바꾸고싶을때 Store클래스에서의 생성자 변경이 필요할것이다.
또한, 위 코드는 객체들간의 관계가 아니라 클래스간 관계가 맺어지고 있다.
이는 근본적으로 Store에서 불필요하게 어떤 제품을 판매할지에 대한 관심이 분리되지 않았기 때문이다.
[강한경합의 문제점 : https://hongjinhyeon.tistory.com/141 ]
이런경우에 의존성주입을 통해 문제를 해결할수 있다.
먼저 다형성이 필요하다. Pencil, Food등 여러가지 제품을 하나로 표현하기 위해 Product라는 Interface가 피룡하다.
그리고 Product인터페이스를 구현하는 구현클래스(Pencil, Food)를 만든다.
그 후에 Store클래스에서는 외부에서 상품(product)를 주입받을수 있도록 한다.
public class Store {
private Product product;
public Store(Product product) {
this.product = product;
}
}
여기서 Store에서 Product객체를 주입하기 위해서는 애플리케이션 실행시점에 필요한 빈을 생성하여, 의존성이 있는 두 객체를 연결하기위해 한 객체를 다른 객체로 주입시켜줘야 한다.
예를들어 Product를 구현한 Pencil클래스 객체를 생성하고, 그 객체를 Store로 주입시켜주는 역할을 위해 DI컨테이너가 필요한것이다.
느슨한 결합
객체를 주입받는다는것은 외부에서 생성된 객체를 인터페이스를 통해 넘겨받는 것이다.
이렇게 하면 런타임시에 의존관계가 결정되기 때문에 결합도를 낮출수 있다.
스프링의 의존성 주입
만약 위 코드에서 PostsService와 PostRepository가 둘다 Bean(*스프링 컨테이너 상에서 생성된 객체)으로 등록되어 있다면, PostsService의 생성자만 만들어주면 스프링 IoC컨테이너가 PostsRepository에 의존성을 알아서 해준다.
(*컨테이너 : 쉽게말해 bean객체들이 들어있는 통)
스프링은 다양한 의존성 주입방법을 제공한다.
@Autowired 애노테이션을 이용하면 Spring에게 의존성을 주입하라 라고 명령하는것과 같은데 생성자, 필드, 세터에 붙일수 있다.
1. 생성자 주입
생성자에 @Autowired를 붙여 의존성을 주입받을 수 있다.
(Spring 4.3부터는 클래스의 생성자가 하나이고 그 생성자로 주입받을 객체가 빈으로 등록되어있따면 @Autowired를 생략할 수 있다.)
생성자주입은 생성자 호출시점에 (해당클래스의 인스턴스생성시) 1회 호출되는것이 보장된다.
따라서 주입받은 객체가 변하지 않거나, 반드시 객체주입이 필요한 경우에 강제하기 위해 사용된다.
@Component
public class SampleController {
private SampleRepository sampleRepository;
@Autowired // 생성자가 한개만 있을시에 생략가능
public SampleController(SampleRepository sampleRepository) {
this.sampleRepository = sampleRepository;
}
}
2. 필드 주입
멤버변수 선언부에 @Autowired애노테이션을 붙인다.
필드주입을 이용하면 코드가 간결해져서 과거에 많이 사용했던 방법이다.
하지만 필드 주입은 외부에서 변경이 불가능하다는 단점이 존재한다. 테스트코드의 중요성이 부각됨에 따라 필드의 필드객체를 수정할 수 없는 필드주입은 거의 사용하지 않게되었다.
또한 필드주입은 반드시 DI프레임워크가 존재해야 하므로 사용을 지양해야한다.
@Component
public class SampleController {
@Autowired
private SampleRepository sampleRepository;
}
3. Setter주입
Setter메소드에 @Autowired애노테이션을 붙인다.
필드값을 변경하는 Setter를 통해서 의존관계를 주입하는 방법이다.
Setter주입은 생성자 주입과 달리 주입받는 객체가 변경될 가능성이 있는 경우에 사용한다. (다만 실제로 변경이 필요한 경우는 극히 드뭄)
@Component
public class SampleController {
private SampleRepository sampleRepository;
@Autowired
public void setSampleRepository(SampleRepository sampleRepository) {
this.sampleRepository = sampleRepository;
}
}
위 3개의 코드예시는 모두 동일하게 SampleController에 SampleRepository를 주입한다.
생성자를 통한 주입을 사용해야 하는 이유
Spring에서 가장 권장하는 방법은 생성자를 통한 주입이다.
1. 생성자를 사용하면 필수적으로 사용해야 하는 의존성 없이는 인스턴스를 만들지 못하도록 강제할 수 있기 때문이다.
만약 SampleController가 SampleRepository없이는 제대로 동작할 수 없다면 SampleController에서 생성자를 통해 강제로 SampleRepository를 무조건 주입받은 후에야 인스턴스를 생성할 수 있도록 해야할것이다.
2. 생성자주입을 통해 변경의가능성을 배제하고 불변성을 보장할 수 있다.
3. final 키워드 작성 및 롬복과의 결합
생성자주입을 사용하면 필드객체에 final키워드를 사용할 수 있다. 이는 컴파일 시점에 누락된 의존성을 확인할수 있다.
반면 생성자 주입을 제외한 다른 주입방법들은 객체생성 이후 (생성자호출이후) 에 호출되므로 final키워드를 사용할 수 없다. ( 객체생성 이후에 필드값을 변하게 하는것이니까!)
또한 final키워드를 붙임으로서 Lombok과 결합되어 코드를 더 간결하게 작성할 수 있다.
롬복에는 final변수를 위한 생성자를 대신해주는 @RequiredArgsConstructor가 있다.
@Slf4j
@RequiredArgsConstructor // Repository를 주입하기 위해 사용
@Service
public class PostService {
private final PostRepository postsRepository;
private final ContractRepository contractRepository;
private final UserRepository userRepository;
private final UserService userService;
}
위와 같은 코드가 가능한 이유는 Spring에서 생성자가 1개인 경우 @Autowired를 생략할수 있도록 지원해주고, 해당 생성자를 Lombok(@RequiredArgsConstructor)로 구현했기 때문이다.
References :
https://devlog-wjdrbs96.tistory.com/165