위의 포스팅에서 잘못 정리된 내용들이 있어 정정하고자 재포스팅을 진행하였다. 해당 포스트에서는 IoC의 한 가지 방법 중 하나인 것이 DI라고 소개하고 있다. 이 포스트를 작성할 때엔 DI가 IoC에 속하는 개념이라고 생각했다.
하지만 IoC와 DI는 각각 따로 존재하는 개념이고, Spring에서 DI가 이루어질 때 IoC가 사용된다가 더 정확한 설명이라는 것을 알게 되었다.
Spring에서의 DI를 살펴본다. 생성자를 통한 주입 방법, setter를 통한 주입 방법, 어노테이션을 통한 주입 방법 3가지가 존재한다. 이 중 생성자를 통한 주입 방법, setter를 통한 주입 방법에서 IoC가 적용되고 있는 것을 살펴볼 수 있다.
# 생성자를 통한 주입
public class A {
private Service service;
public A(Service service) {
this.service = service;
}
}
# 수정자를 통한 주입
public class B {
private ServiceB serviceB;
@Autowired
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
위의 두 방법 모두 class A와 class B에서 직접 Service와 ServiceB를 능동적으로 new를 통해 생성하지 않고 있다. 외부의 누군가에게 class A와 Service, class B와 ServiceB를 연결해주도록 맡기고 있다. 생성자와 setter 메소드를 외부에서 대신 실행해주어 class A와 class B가 생성된다.
@Autowired 어노테이션을 통한 주입 방법에 대해 살펴본다.
public class C {
@Autowired
private ServiceC serviceC;
}
위의 코드만 보았을 때에는 제어가 역전된 것인지 아닌지 알 수 없다. 생성자를 통한 주입, 수정자를 통한 주입 방법에서는 생성자 메소드를, setServiceB라는 메소드를 클래스 내부가 아닌 Spring에서 실행해준다. 하지만 어노테이션을 통한 주입 방법에서는 인스턴스 변수가 선언되어 있고 어노테이션이 붙어있을 뿐 호출되고 있는 정확한 메소드가 명시되어 있지 않다. 따라서 언제 어떻게 실행될지, IoC인지 아닌지 알 수가 없다.
field injection 방법을 사용하는 것을 지양해야한다는 글을 많이 볼 수 있다. 순환 참조 때문에 그렇기도 하지만, field injection을 지양해야 하는 이유 중 하나가 바로 이것 때문이다. field injection은 Spring이 지향하는 IoC를 통한 DI에 맞지도 않기 때문이다.
마찬가지로 구글 주스를 통한 DI 방법에서도 IoC가 적용되고 있다고 볼 수 없다.
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
bind(BillingService.class).to(RealBillingService.class);
}
}
public class RealBillingService implements BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
@Inject
public RealBillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
}
위의 코드를 보면 @Inject 어노테이션을 통해 DatabaseTransactionLog 클래스와 PaypalCreditCardProcessor 클래스를 주입받고 있다. 생성자 메소드가 존재하나 DatabaseTransactionLog 클래스와 PaypalCreditCardProcessor 클래스는 어떻게 주입받고 있는지 알 수 없는 것이다.
이처럼 IoC와 서로 다른 개념이다.
정리하면 아래와 같다.
Spring Bean에 대한 부분 역시 제대로 이해하지 못하고 포스팅 한 것 같아 좀더 내용을 알기 쉽게 다듬고자 한다.
이전 포스팅에서 Spring Bean은 Singleton이라고 하였다. 하지만 엄밀히 말하면 Spring Bean은 Singleton이 아니다.
public class Singleton {
private ObjectMapper om;
public Singleton(ObjectMapper om) {
this.om = om; -> (1)
}
public void doSomething() {
ObjectMapper = new ObjectMapper(); -> (2)
}
public void doSomething2() {
om.convertValue(..); -> (3)
}
}
Spring Bean이 정말로 Singleton이었다면 메모리에 only one 하나만 존재해야 한다. 그렇다면 (1)과 (2)는 동일한 객체여야 한다. 하지만 (1)과 (2)는 다른 객체이다. 하지만 (1)과 (3)은 동일한 객체이다.
(1)의 ObjectMapper는 IoC가 주입해준 Spring Bean이다. IoC는 Spring Bean을 Singleton과 같도록 보장해준다. 따라서 Spring을 통해 주입받은 ObjectMapper인 (1)과 (3)은 동일한 객체이다. 반면 (2)의 경우 Spring을 통해 생성된 객체가 아니라 new를 통해 생성된 새로운 객체이다. 따라서 (2)의 경우 Spring에 의해 관리되지 않는다. 따라서 (1), (3)과 동일하게 관리되지 않는 것이다.
이전 포스팅에서 동일하게 언급하였듯 Java Singleton 객체를 만들기 위해서는 아래와 같은 방법들이 필요하다.
Spring에서는 Bean들을 Singleton으로 관리하고자 하였고, 1~4와 같은 과정을 프로그래머가 알지 않아도 되도록 알아서 Bean이 Singleton과 같은 역할을 할 수 있도록 보장해주는 역할을 하는 것이다. 따라서 Spring Bean은 Singleton과 같이 관리되는 것이지 Singleton 객체인 것은 아니다.
만약 A라는 객체를 전부 B로 변환하는 역할을 하는 ObjectMapper를 생성하고 Bean으로 만들었다고 가정한다. 이 경우 ObjectMapper는 Singleton으로 관리되므로 A에서 B로 변환하는 역할밖에 하지 못한다. Spring Bean이 정말 메모리에 only one만 존재하는 singleton 객체로 만들어졌다면 A에서 C로 변환하는 ObjectMapper가 필요할 때 곤란한 상황이 발생하게 된다.
하지만 Spring에서는 Spring Bean만을 실제 Singleton 객체가 아니라 Singleton 객체처럼 동작하도록 보장해주고 있으므로 A에서 C로 변환하는 ObjectMapper가 필요할 때 new를 통해 새로 ObjectMapper 객체를 생성해 A에서 C로 변환하도록 구현하면 된다.
[참조] https://madplay.github.io/post/why-constructor-injection-is-better-than-field-injection
[참조] https://maro-matta.tistory.com/m/entry/%EC%A0%9C%EC%96%B4%EC%9D%98-%EC%97%AD%EC%A0%84%EC%9D%B4-%EB%AD%90%EC%9E%84-Inversion-of-Control
[참조] https://www.popit.kr/%EB%B2%88%EC%97%AD-%EA%B5%AC%EA%B8%80-%EC%A3%BC%EC%8A%A4google-guice-%EC%86%8C%EA%B0%9C/
[참조] https://jodu.tistory.com/23