간략하게 4가지 방법이 있다.
근데 이중에서 4번은.. 대부분의 포스팅에서 잘 다루지 않는 모양이다. 잘 안쓰는거부터, 권장하는 방법까지 알아보자
이 주입 타입은 짧은 생명주기때문에 다른 3가지 주입 방식보다 덜 사용된다.
기본적으로, 스프링의 모든 빈들은 싱글톤으로 생성된다. 이것은 컨테이너에서 한번 생성되고, 같은 객체가 필요로 하는 닫른 곳에 주입된다는 말이다. 반면 가끔 다른 전략이 필요할 때가 있다. 예를 들어서, 각 메소드 호출은 각기 다른 객체에서 수행되야 할 때가 있다.
이럴떄 lookup method 주입을 사용하여야 한다.
이 방식의 주입은 타 3가지의 주입 방식과 완전히 다른 방식의 주입이기 때문에, 장단점을 따질수가 없다.
이 방식은 setter 메소드를 사용하는 방식으로 의존성 주입이 이루어진다.
우선 class 파일을 작성하기 전에, 빈 설정을 xml 설정에 서 시도해보자
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="service1" class="example.Service1"/>
<bean id="service2" class="example.Service2"/>
<bean id="dependentService" class="example.DependentService">
<property name="service1" ref="service1"/>
<property name="service2" ref="service2"/>
</bean>
</beans>
2개의 서비스를 빈으로 등록하고, dependentService에서 service1과 service2를 프로퍼티로 등록한다.
꽤 직관적인 방식이고, 이제 빈으로 등록한 서비스를 다른 서비스에 주입한 후 사용하는 예제를 살펴보도록 하자.
class DependentService {
private Service1 service1;
private Service2 service2;
public void setService1(Service1 service1) {
this.service1 = service1;
}
public void setService2(Service2 service2) {
this.service2 = service2;
}
public void doSmth() {
service1.doSmth();
service2.doSmth();
}
}
이렇게 된다면, DependentService에서 사용하는 service1과 service2에 대한 의존성 주입은 완료되게 된다.
또한 이와 같은 작업은 @Autowired 라는 어노테이션에 의해서 같은 효과를 볼 수 있게 되는데,
class DependentService {
private Service1 service1;
private Service2 service2;
@Autowired
public void setService1(Service1 service1) {
this.service1 = service1;
}
@Autowired
public void setService2(Service2 service2) {
this.service2 = service2;
}
public void doSmth() {
service1.doSmth();
service2.doSmth();
}
}
종속성 해결 또는 개체 재구성의 유연성으로 언제든지 수행할 수 있고, 이러한 유연성은 생성자 주입의 순환 종속성 문제를 해결할 수 있다.
종속성이 헌재 설정되어 있지 않을 수 있기 때문에 Null 체크가 필수다 또한 종속성을 재정의할 가능성이 생기므로 생성자 주입보다 잠재적으로 오류가 발생할 수 있고 덜 안전하게 된다.
@Service
class DependentService {
@Autowired
private Service1 service1;
@Autowired
private Service2 service2;
public void doSmth() {
service1.doSmth();
service2.doSmth();
}
}
이 방식에서의 주입은 실제로 새로운 방식의 주입이 아니기 때문에 어노테이션 기반의 접근만 가능하다. 스프링은 이 방식의 주입을 사용하기 위해 리플렉션을 이용한다.
사용하기 쉽고 생성자나 설정자가 필요하지 않다. 또 생성자 및 설정자 접근 방식과 쉽게 결합 가능하다는 장점이 있다.
개체 인스턴스화에 대한 제어가 적다. 테스트를 위한 클래스의 객체를 인스턴스화 하려면 작성중인 테스트에 따라 구성된 스프링 컨테이너 또는 모의 라이브러리가 필요하게 된다. 또 종속성이 수십게에 이를때에야 설계에 문제가 있음을 알아차릴 수 있고, 불변성이 없다는 것이 단점이다.
이 방식은 의존성 주입에서 권장되는 방법이다. 종속 클래스에는 모든 종속성이 설정되는 생성자가 있고, XML, java 또는 어노테이션 기반 구성에 따라 스프링 컨테이너에서 제고오딘다.
@Service
public class DependentService {
private final Service1 service1;
private final Service2 service2;
public DependentService(Service1 service1, Service2 service2) {
this.service1 = service1;
this.service2 = service2;
}
public void doSmth() {
service1.doSmth();
service2.doSmth();
}
}
위의 예제에서는 2개의 독립적 서비스와 1개의 종속 서비스가 있고, 생성자를 통해서 의존성을 주입하게 된다.
생성된 객체는 변경할 수 없으며 완전히 초기화된 상태로 클라이언트에 반환된다. 이 접근 방식을 사용하면 종속성이 증거하는 문제를 즉시 확인할 수 있고, 종속성이 많을수록 생성자가 커진다.
차후에 개체의 종속성을 변경할 가능성이 없고, 순환 종속성을 가질 가능성이 더 높아진다.
대부분의 경우, Lombok라이브러리를 통해서 많은 일들을 처리하게 된다.
@AllArgsConstructor
@Service
public class DependentService {
private Service1 service1;
private Service2 service2;
public void doSmth() {
service1.doSmth();
service2.doSmth();
}
}
위의 예제에서 생성자를 @AllArgsConstructor가 만들게 된다. 결과적으로 컴파일을 하게 되면 모든 종속성을 가진 서비스들에 대한 생성자가 있는 생성자가 추가되어 있다.
@RequiredArgsConstructor
@Service
public class DependentService {
private final Service1 service1;
private final Service2 service2;
public void doSmth() {
service1.doSmth();
service2.doSmth();
}
}
모든 종속성을 가진 필드들을 생성자 파라메터에 추가하는 AllArgsConstructor와 다르게, 이 방식의 어노테이션은 final키워드가 붙은 필드들을 생성자 파라메터로 선언하여 생성자를 만들게 된다.
모든 객체들이 싱글톤으로 생성되고 관리되며 필드에 주입되는 객체들은 한번만 초기화되기 때문에 걸들면 안되며 조금 더 secure한 코딩 스타일을 위해서는 필드들을 final로 선언하고, @RequiredArgsConstructor로 생성자 주입 방식으로 만드는 것이 나아보인다.
이전에서도 의존성 역전(DI)에 대한 글을 Nest.js를 사용할 때 작성한 적이 있었다.