이전 글에서 싱글톤 빈에서 프로토타입 빈을 사용할 때 예상과 다르게 동작하는 문제를 확인했음.
싱글톤 빈은 스프링 컨테이너가 시작될 때 한 번만 생성되며 이후에도 같은 인스턴스를 계속 유지함. 반면 프로토타입 빈은 요청할 때마다 새로운 인스턴스를 반환하는데, 문제는 싱글톤 빈이 생성 시점에 프로토타입 빈을 주입받아 버리면 계속 같은 프로토타입 빈을 재사용하게 된다는 것임.
프로토타입 빈을 사용할 때마다 매번 새로 생성되도록 만들기 위해 다음 3가지 방법이 있음.
1. 스프링 컨테이너 직접 조회
2. ObjectProvider
사용
3. JSR-330 Provider
사용
4. 프록시(Proxy) 활용
가장 직관적인 방법은 필요할 때마다 스프링 컨테이너에 직접 요청하여 새로운 프로토타입 빈을 가져오는 것임.
@Component
public class SingletonClient {
@Autowired
private ApplicationContext ac; // 스프링 컨테이너 직접 주입
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); // 매번 새로운 빈 요청
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
ApplicationContext
를 직접 사용하면 스프링 컨테이너에 대한 의존성이 강해짐.ObjectProvider
사용스프링 4.3부터 제공되는 ObjectProvider
를 사용하면 보다 간결한 코드로 프로토타입 빈을 지연 조회할 수 있음.
@Component
public class SingletonClient {
private final ObjectProvider<PrototypeBean> prototypeBeanProvider;
@Autowired
public SingletonClient(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); // 필요할 때 조회
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
ObjectProvider.getObject()
를 호출하는 시점에 새로운 프로토타입 빈을 생성함.ApplicationContext
를 직접 사용하지 않아 스프링 컨테이너에 대한 의존성이 줄어듦.Provider
사용javax.inject.Provider
또는 최신 jakarta.inject.Provider
를 활용하면 스프링이 아닌 다른 DI 컨테이너에서도 활용 가능한 방식이 됨.
import javax.inject.Provider;
@Component
public class SingletonClient {
private final Provider<PrototypeBean> prototypeBeanProvider;
@Autowired
public SingletonClient(Provider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get(); // 요청 시점에 생성
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
javax.inject.Provider
는 스프링 컨테이너에 종속되지 않음.ObjectProvider
보다 더 범용적인 자바 표준 인터페이스임.get()
메서드만 제공하여 추가적인 기능이 부족함.javax.inject
, Spring Boot 3.0 이상: jakarta.inject
).스프링이 제공하는 프록시 기능을 활용하면 가짜 프록시 객체를 주입하여 요청이 올 때마다 실제 프로토타입 빈을 찾아서 반환하도록 만들 수 있음.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
로그를 남기는 MyLogger
를 request 스코프로 지정하고, 프록시를 적용하는 코드
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "][" + requestURL + "] " + message);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create");
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close");
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger; // 프록시 객체가 주입됨
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
[12345] request scope bean create
[12345][http://localhost:8080/log-demo] controller test
[12345][http://localhost:8080/log-demo] service id = testId
[12345] request scope bean close
해결 방법 | 특징 | 장점 | 단점 |
---|---|---|---|
ApplicationContext 직접 조회 | 매번 컨테이너에서 조회 | 직관적 | 스프링 의존성 강함 |
ObjectProvider | 필요할 때만 조회 | 코드가 간결함 | 스프링 의존성 있음 |
JSR-330 Provider | 자바 표준 사용 | 스프링에 종속되지 않음 | 기능이 제한적 |
프록시 활용 | 가짜 프록시 객체 주입 | 싱글톤처럼 사용 가능 | 초기 요청 시 다소 복잡 |
👉 두가지 중 적절한 방법을 선택하여 스프링 빈을 효율적으로 관리하는 것이 중요하다