IoC 컨테이너에 등록된 모든 빈들은 Scope이라는 개념이 있다. 정리해보자. 😆
기본적으로 등록되는 빈들의 Scope는 싱글톤이다. 싱글톤은 애플리케이션의 전반에 걸쳐 해당 빈의 인스턴스가 오직 하나만을 가진다는 것을 의미한다.
실제로 싱글톤으로 등록된 빈들을 조회해보면 동일한 인스턴스를 가지고 있다는 것을 확인할 수 있다.
@Component
public class Proto {
}
@Component
public class Single {
@Autowired
Proto proto;
public Proto getProto() {
return proto;
}
}
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
Single single;
@Autowired
Proto proto;
@Override
public void run(ApplicationArguments args) throws Exception {
// 싱글톤 스코프는 같은 인스턴스를 사용
System.out.println(proto); // AppRunner 클래스에서 주입 받아온 proto
System.out.println(single.getProto()); // Single이 참고하고 있는 proto
}
}
싱글톤 Scope 외에도 다른 옵션들이 존재한다.
나열된 모든 옵션들은 모두 프로토타입과 비슷하다. 프로토타입 Scope만 본다면, 싱글톤과 다르게 매번 빈을 받아올 때마다 새로운 인스턴스를 생성한다.
@Component
@Scope("prototype")
public class Proto {
}
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
ApplicationContext ctx;
@Override
public void run(ApplicationArguments args) throws Exception {
// 매번 다른 인스턴스를 사용
System.out.println(ctx.getBean(Proto.class));
System.out.println(ctx.getBean(Proto.class));
System.out.println(ctx.getBean(Proto.class));
}
}
싱글톤과 같이 특정 빈에서 짧은 생명 주기를 가진 빈들을 주입받을 때에는 다음과 같은 경우를 생각해야한다고 한다.
이 경우에는 아무런 문제가 발생하지 않는다. 프로토타입 빈은 매번 인스턴스가 새로 생성이 되지만, 프로토타입 빈이 참조하는 싱글톤 빈은 항상 동일하기 때문에 의도한 바에서 벗어나지 않아 큰 문제가 없다.
@Component
@Scope("prototype")
public class Proto {
@Autowired
Single single;
}
이 경우는 생각해볼 필요가 있다.
싱글톤 빈은 단 하나의 인스턴스만 생성이 된다. 그리고 그 내부에 참조한 프로토타입 빈도 설정이 같이 된다. 그렇기 때문에 싱글톤 빈을 사용하더라도 내부에 참조한 프로토타입의 빈은 변경되지 않는다. 즉, 매번 새로운 인스턴스를 만드려는 의도와 다르게 단 하나만의 변경되지 않는 빈을 가지게 되어 문제가 발생한다는 의미다.
@Component
public class Single {
@Autowired
Proto proto;
public Proto getProto() {
return proto;
}
}
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
ApplicationContext ctx;
@Override
public void run(ApplicationArguments args) throws Exception {
// 동일한 싱글톤 인스턴스와 그 내부에 참조하고 있는 동일한 프로토타입 인스턴스
System.out.println(ctx.getBean(Single.class).getProto());
System.out.println(ctx.getBean(Single.class).getProto());
System.out.println(ctx.getBean(Single.class).getProto());
}
}
그럼 어떻게 해결할 수 있을까?
싱글톤 빈이 프로토타입 빈을 직접 참조하지 않도록 프록시로 빈을 감싸주어 해결할 수 있다.
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) // CGLIB를 이용한 다이나믹 프록시
public class Proto {
@Autowired
Single single;
}
이미지를 보면 알 수 있듯이, 포로토타입을 감싼 프록시 인스턴스를 빈으로 등록하고, 이 빈을 싱글톤 빈이 참조하고 있는 빈에게 넣어주는 것이다. (여기서 프록시 빈은 내부의 빈을 상속받아 구현했기 때문에 타입은 동일하여 주입이 가능하다.)
💡 TARGET_CLASS? CGLIB란?
원래 자바에서의 다이나믹 프록시는 인터페이스 프록시만 제공하지만 CGLIB는 서드 파티 라이브러리로, 클래스도 프록시로 만들어준다. 따라서 위의 경우에는 TARGET_CLASS 설정으로 CGLIB 기반의 클래스 프록시를 만들어주는 것이다. 만약 인터페이스였다면, INTERFACE 옵션을 사용할 수 있다.
ObjectProvider
에는 스프링 코드가 들어가서 백기선님은 별로 좋아하지 않으신 듯하다.
@Component
public class Single {
@Autowired
ObjectProvider<Proto> proto;
public Proto getProto() {
return proto.getIfAvailable();
}
}
거의 대부분 싱글톤 Scope을 사용한다고 하니, 지금의 내 수준에서는 많이 생각할 필요는 없어보이지만 Scope에 대한 설정만으로도 다양한 작업을 처리할 수 있을 것 같다는 생각이 든다.