스프링 빈 스코프 활용하기: Provider와 프록시를 이용한 문제 해결

조아·2025년 2월 4일
0

1. 프로토타입 빈의 문제점

이전 글에서 싱글톤 빈에서 프로토타입 빈을 사용할 때 예상과 다르게 동작하는 문제를 확인했음.

🔹 싱글톤 빈과 프로토타입 빈을 함께 사용할 때 문제 발생

싱글톤 빈은 스프링 컨테이너가 시작될 때 한 번만 생성되며 이후에도 같은 인스턴스를 계속 유지함. 반면 프로토타입 빈은 요청할 때마다 새로운 인스턴스를 반환하는데, 문제는 싱글톤 빈이 생성 시점에 프로토타입 빈을 주입받아 버리면 계속 같은 프로토타입 빈을 재사용하게 된다는 것임.

🔹 해결 방법

프로토타입 빈을 사용할 때마다 매번 새로 생성되도록 만들기 위해 다음 3가지 방법이 있음.
1. 스프링 컨테이너 직접 조회
2. ObjectProvider 사용
3. JSR-330 Provider 사용
4. 프록시(Proxy) 활용


2. 해결 방법

1: 스프링 컨테이너 직접 조회 (권장X)

가장 직관적인 방법은 필요할 때마다 스프링 컨테이너에 직접 요청하여 새로운 프로토타입 빈을 가져오는 것임.

@Component
public class SingletonClient {

    @Autowired
    private ApplicationContext ac; // 스프링 컨테이너 직접 주입

    public int logic() {
        PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); // 매번 새로운 빈 요청
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

🚨 단점

  • ApplicationContext를 직접 사용하면 스프링 컨테이너에 대한 의존성이 강해짐.
  • 단위 테스트가 어려워지고, 코드의 유연성이 떨어짐.
  • 스프링 컨테이너를 매번 호출하는 것은 성능상 불리할 수 있음.

2: 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를 직접 사용하지 않아 스프링 컨테이너에 대한 의존성이 줄어듦.
  • 간결하고 유지보수하기 쉬운 코드 작성 가능.

🚨 단점

  • 여전히 스프링 프레임워크에 의존적.

3: JSR-330 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() 메서드만 제공하여 추가적인 기능이 부족함.
  • 별도 라이브러리 추가 필요 (Spring Boot 3.0 미만: javax.inject, Spring Boot 3.0 이상: jakarta.inject).

4: 프록시(Proxy) 사용

스프링이 제공하는 프록시 기능을 활용하면 가짜 프록시 객체를 주입하여 요청이 올 때마다 실제 프로토타입 빈을 찾아서 반환하도록 만들 수 있음.

🔹 설정 방법

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}

🔹 프록시 동작 방식

  1. 스프링 컨테이너가 MyLogger의 가짜 프록시 객체를 생성하여 주입함.
  2. 가짜 프록시 객체는 내부적으로 실제 request 스코프 빈을 찾아서 위임하는 역할을 수행함.
  3. 실제 HTTP 요청이 발생할 때 request 스코프의 진짜 객체가 생성되어 사용됨.

3. 프록시 활용 예제

로그를 남기는 MyLogger를 request 스코프로 지정하고, 프록시를 적용하는 코드

🔹 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");
    }
}

🔹 LogDemoController (의존성 주입)

@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";
    }
}

🔹 LogDemoService

@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자바 표준 사용스프링에 종속되지 않음기능이 제한적
프록시 활용가짜 프록시 객체 주입싱글톤처럼 사용 가능초기 요청 시 다소 복잡
  • 실무에서는 ObjectProvider 또는 프록시 방식이 가장 많이 사용됨.
  • 프록시는 request, session과 같은 웹 스코프에서 자주 사용됨.

👉 두가지 중 적절한 방법을 선택하여 스프링 빈을 효율적으로 관리하는 것이 중요하다

profile
프론트엔드 개발자

0개의 댓글