본문으로 건너뛰기
김신건의 로그

[Spring] Bean Scope 심화: Singleton, Prototype, Web Scope

· 수정 · 📖 약 2분 · 850자/단어 #spring #bean #scope #web
spring bean scope, singleton scope, prototype scope, request scope, session scope, scoped proxy, @Scope, Bean 스코프

정의

spring-ioc-di 가 5가지 scope 의 개요 를 다룬다면, 이 페이지는 그 중 함정 (prototype-into-singleton)해결 패턴 (ScopedProxy, ObjectProvider) 을 깊게 다룬다.

5 표준 Scope

Scope인스턴스 수생명주기사용 사례
singleton (기본)컨테이너당 1개컨테이너 시작 ~ 종료stateless service, repository
prototype주입/조회마다 새로생성 후 컨테이너 추적 안 함mutable state, throwaway 객체
requestHTTP 요청마다 새로요청 시작 ~ 종료요청별 데이터 (현재 user 등)
sessionHTTP 세션마다 새로세션 시작 ~ 만료로그인 사용자 정보
applicationServletContext 당 1개앱 시작 ~ 종료singleton 과 비슷하지만 모든 DispatcherServlet 공유
websocketWebSocket session 당session 시작 ~ 종료STOMP 사용자별 상태

singleton컨테이너당 1개 이지 JVM당 1개가 아니다. 하나의 JVM 에 root + dispatcher 의 두 ApplicationContext 가 있다면 singleton Bean 도 2개.

선언

@Component
@Scope("prototype")
public class TaskWorker { ... }

// 또는
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CurrentRequestUser { ... }

@Bean 방식도 동일:

@Bean
@Scope("prototype")
public TaskWorker worker() { return new TaskWorker(); }

함정: Prototype-Into-Singleton

singleton 안에 prototype 을 주입하면:

@Component  // singleton
public class Dispatcher {
    @Autowired
    TaskWorker worker;   // ← prototype 으로 선언했어도

    public void dispatch() {
        worker.run();    // 매번 같은 worker 인스턴스!
    }
}

Dispatcher 가 1번 생성될 때 worker 도 1번만 주입된다. 이후 worker 필드는 같은 인스턴스를 가리킴. prototype 의 의도 (매번 새 인스턴스) 가 무효화된다.

java
@Component @Scope("prototype")
class TaskWorker {
  static final AtomicInteger SEQ = new AtomicInteger();
  final int id = SEQ.incrementAndGet();
  public void run() { System.out.println("worker#" + id); }
}

@Component
class Dispatcher {
  @Autowired TaskWorker worker;
  public void dispatch() { worker.run(); }
}

// main
Dispatcher d = ctx.getBean(Dispatcher.class);
d.dispatch();
d.dispatch();
d.dispatch();
log
worker#1
worker#1
worker#1

같은 worker 인스턴스가 반복 사용됨. ObjectProvider 로 바꾸면:

java
@Component
class Dispatcher {
  @Autowired ObjectProvider<TaskWorker> workerProvider;
  public void dispatch() {
      TaskWorker w = workerProvider.getObject();
      w.run();
  }
}
log
worker#1
worker#2
worker#3

매번 새 인스턴스. prototype 의도가 살아난다.

해결 1: ObjectProvider

@Component
public class Dispatcher {
    @Autowired
    ObjectProvider<TaskWorker> workerProvider;

    public void dispatch() {
        TaskWorker worker = workerProvider.getObject();  // 호출마다 새 인스턴스
        worker.run();
    }
}

Spring 4.3+ 의 권장 방식. Optional<T>, Iterable<T>, lazy / unique 같은 부가 기능도 함께.

해결 2: jakarta.inject.Provider

JSR-330 표준. ObjectProvider 와 거의 동일하지만 framework 독립:

@Inject
Provider<TaskWorker> workerProvider;

TaskWorker worker = workerProvider.get();

해결 3: @Lookup

@Component
public abstract class Dispatcher {
    public void dispatch() {
        TaskWorker w = createWorker();
        w.run();
    }

    @Lookup
    protected abstract TaskWorker createWorker();
}

Spring 이 CGLIB 으로 abstract 메서드를 구현. 추상 클래스가 필요해 깔끔하진 않음.

해결 4: ScopedProxy

scope 자체에 proxy 옵션:

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

주입받는 쪽은 평범하게:

@Component
public class Dispatcher {
    @Autowired
    TaskWorker worker;       // ← 실제는 proxy

    public void dispatch() {
        worker.run();        // proxy 가 호출마다 새 인스턴스 lookup
    }
}

가장 깔끔하지만 proxy 오버헤드. web scope 에는 거의 필수 (singleton service 가 request-scoped Bean 을 주입받을 때).

ScopedProxyMode

@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)   // CGLIB (클래스 상속)
@Scope(proxyMode = ScopedProxyMode.INTERFACES)     // JDK dynamic proxy (인터페이스만)
Mode기반장점단점
TARGET_CLASSCGLIB (바이트코드)클래스 자체 주입 가능final 클래스/메서드 안 됨, 의존성 추가
INTERFACESJDK Proxy표준, 가벼움인터페이스를 통해서만 호출 가능

기본은 NO (proxy 없음). 함정에 안 빠지려면 거의 항상 TARGET_CLASS 명시.

Web Scope 사용 예

request scope

@Component
@RequestScope         // = @Scope("request", proxyMode = TARGET_CLASS) 단축
public class RequestMetrics {
    private final long startTime = System.nanoTime();
    private int dbQueryCount;

    public void incrementQueries() { dbQueryCount++; }
    public long elapsedMs() { return (System.nanoTime() - startTime) / 1_000_000; }
}

@Service
public class UserService {
    @Autowired RequestMetrics metrics;   // request 마다 새 인스턴스 (proxy)

    public User find(Long id) {
        metrics.incrementQueries();
        return repo.findById(id);
    }
}

각 HTTP 요청마다 새 RequestMetrics 인스턴스. 요청 종료 시 자동 소멸.

session scope

@Component
@SessionScope
public class ShoppingCart {
    private final List<Item> items = new ArrayList<>();
    public void add(Item i) { items.add(i); }
}

메모리 함정: 세션 만료 (기본 30분) 까지 살아있음. 사용자 수 × 평균 cart 크기 = 메모리 압박. Redis 등 외부 저장소가 유리.

application scope

singleton 과 비슷하지만 ServletContext 에 저장. 멀티 DispatcherServlet 환경에서도 공유. 거의 안 쓰임 (대부분 singleton 으로 충분).

커스텀 Scope

Scope 인터페이스 구현 후 등록:

public class TenantScope implements Scope {
    @Override
    public Object get(String name, ObjectFactory<?> factory) {
        String tenant = TenantContext.current();
        return getStore(tenant).computeIfAbsent(name, k -> factory.getObject());
    }
    // ... remove, registerDestructionCallback, etc.
}

@Configuration
public class ScopeConfig {
    @Bean
    public static CustomScopeConfigurer customScope() {
        CustomScopeConfigurer cfg = new CustomScopeConfigurer();
        cfg.addScope("tenant", new TenantScope());
        return cfg;
    }
}

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

SimpleThreadScope 는 Spring 내장이지만 thread 풀에서 위험 (이전 요청의 인스턴스 누수).

함정과 베스트 프랙티스

  • prototype scope 의 destroy callback 호출 안 됨: @PreDestroy 무용. 직접 정리.
  • web scope 에는 거의 항상 proxyMode = TARGET_CLASS: singleton 의 함정 회피
  • session scope 는 메모리 폭탄 후보: 1000 명 × 1 MB = 1 GB
  • prototype 을 매번 새로 받으려면 ObjectProvider 우선: @Lookup 보다 깔끔
  • @RefreshScope (Spring Cloud Config) 도 ScopedProxy 패턴: config 갱신 시 새 인스턴스
  • WebFlux 환경에서는 web scope 부재: ThreadLocal 안 쓰는 Reactor Context 기반

관련 위키

이 글의 용어 (7개)
[Java] ThreadLocaljava
정의 는 각 스레드마다 독립된 변수 사본을 가지게 해 주는 컨테이너. 로 접근하며, 한 스레드의 값은 다른 스레드에서 볼 수 없다. 쓰임은 두 가지: (1) 같은 인스턴스를 여러…
[Spring] Bean Lifecycle: 초기화와 소멸 콜백spring
정의 가 Bean 등록과 주입 을 다룬다면, 이 페이지는 등록 이후 생성 → 사용 → 소멸 사이에 끼어드는 콜백을 다룬다. Spring 컨테이너가 단순히 객체를 만들고 끝내는 게…
[Spring] BeanPostProcessor와 BeanFactoryPostProcessorspring
정의 Spring 컨테이너의 2가지 확장점: - BeanFactoryPostProcessor (BFPP): Bean 인스턴스가 만들어지기 전, (메타데이터) 을 수정. 클래스명,…
[Spring] IoC와 DI: Bean, ApplicationContext, @Autowiredspring
정의 IoC (Inversion of Control)는 객체 생성/생명주기 제어를 프레임워크가 가져가는 원칙. DI (Dependency Injection)는 그 구현 방법. S…
[Spring] RequestContextHolder, LocaleContextHolder: ThreadLocal 기반 요청 컨텍스트spring
정의 Spring 은 HTTP 요청 / 응답 / locale / 트랜잭션 / 인증 같은 요청 범위 데이터를 ThreadLocal 에 저장 해 전역 노출한다. 메서드 인자로 일일이…
[Spring] Spring MVC: @RestController, @RequestMappingspring
정의 Spring MVC는 Servlet 기반 동기 웹 프레임워크. 이 요청을 받아 핸들러(컨트롤러 메서드)에 라우팅. REST API와 전통 MVC(HTML) 모두 지원. We…
[Spring] WebFlux: Reactive, Mono, Fluxspring
정의 Spring WebFlux는 Project Reactor 기반의 비동기·non-blocking 웹 프레임워크. Spring MVC와 별개. Netty 기본 (Tomcat도 …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기