스프링 빈 스코프 제대로 이해하기: 싱글톤 vs 프로토타입 차이점

조아·2025년 2월 3일
0

빈 스코프란?

스코프(scope)는 빈이 생성되고 유지되는 범위를 뜻함.

1. 주요 빈 스코프 종류

스코프생성 시점유지 기간특징
Singleton컨테이너 시작 시컨테이너 종료 시스프링 기본 스코프, 인스턴스 1개 유지
Prototype요청할 때마다반환 즉시 관리 종료새로운 객체 반환, 컨테이너가 관리 안 함
Request웹 요청 시작 시요청 종료 시웹 요청마다 별도 객체 생성
Session웹 세션 시작 시세션 종료 시세션 동안 같은 객체 유지
Application애플리케이션 시작 시애플리케이션 종료 시서블릿 컨텍스트와 동일한 라이프사이클

2. 빈 스코프 지정 방법

자동 등록 (컴포넌트 스캔)

@Component
@Scope("prototype")
public class HelloBean {}

수동 등록

@Bean
@Scope("prototype")
public PrototypeBean helloBean() {
    return new HelloBean();
}

싱글톤 vs 프로토타입 스코프

1. 싱글톤 빈

싱글톤 빈을 스프링 컨테이너에서 요청하면 항상 같은 인스턴스를 반환함.

싱글톤 빈 요청 과정
1. 스프링 컨테이너에 싱글톤 스코프의 빈을 요청.
2. 컨테이너는 해당 빈을 생성(최초 1회)하고 이후 요청에도 같은 인스턴스를 반환.

테스트 코드

public class SingletonTest {

    @Test
    void singletonScopeTest() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean bean1 = context.getBean(SingletonBean.class);
        SingletonBean bean2 = context.getBean(SingletonBean.class);

        assertThat(bean1).isSameAs(bean2); // 같은 인스턴스인지 확인

        context.close();
    }

    @Component
    @Scope("singleton") // 기본값이라 생략 가능
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }
    }
}

출력 결과

SingletonBean.init
SingletonBean.destroy

→ 싱글톤 빈은 컨테이너 종료 시점까지 유지됨.


2. 프로토타입 빈

프로토타입 빈을 컨테이너에서 요청하면 매번 새로운 인스턴스를 반환함.

프로토타입 빈 요청 과정
1. 컨테이너에 프로토타입 빈 요청.
2. 요청이 올 때마다 새로운 인스턴스를 생성하여 반환.
3. 반환된 빈은 클라이언트가 직접 관리해야 함.

테스트 코드

public class PrototypeTest {

    @Test
    void prototypeScopeTest() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PrototypeBean.class);

        PrototypeBean bean1 = context.getBean(PrototypeBean.class);
        PrototypeBean bean2 = context.getBean(PrototypeBean.class);

        assertThat(bean1).isNotSameAs(bean2); // 다른 인스턴스인지 확인

        context.close();
    }

    @Component
    @Scope("prototype")
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

출력 결과

PrototypeBean.init
PrototypeBean.init

→ 두 개의 요청에 대해 각각 새로운 인스턴스를 생성했음.
@PreDestroy가 호출되지 않음(컨테이너가 관리하지 않기 때문).


싱글톤 빈에서 프로토타입 빈을 사용할 때 문제

싱글톤 빈이 의존관계 주입(DI) 으로 프로토타입 빈을 받으면 초기 주입된 프로토타입 빈을 계속 재사용함.

문제 코드

@Component
public class SingletonClient {

    private final PrototypeBean prototypeBean;

    @Autowired
    public SingletonClient(PrototypeBean prototypeBean) {
        this.prototypeBean = prototypeBean;
    }

    public int logic() {
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

테스트 코드

@Test
void singletonUsesSamePrototypeInstance() {
    AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(SingletonClient.class, PrototypeBean.class);

    SingletonClient client1 = context.getBean(SingletonClient.class);
    SingletonClient client2 = context.getBean(SingletonClient.class);

    int count1 = client1.logic(); // 1 증가
    int count2 = client2.logic(); // 기존 프로토타입 객체를 사용하여 2 증가

    assertThat(count1).isEqualTo(1);
    assertThat(count2).isEqualTo(2);

    context.close();
}

출력

PrototypeBean.init
1
2

→ 프로토타입 빈이 새로운 객체로 생성되지 않고, 기존 객체를 계속 사용함.


해결 방법

1. 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();
    }
}

2. 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();
    }
}

웹 스코프

웹 환경에서는 Request, Session, Application 스코프를 활용할 수 있음.

Request 스코프 예제

@Component
@Scope(value = "request")
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");
    }
}

요약

  1. 싱글톤 빈: 스프링 컨테이너가 생성과 종료까지 관리.
  2. 프로토타입 빈: 요청할 때마다 새 인스턴스를 생성, 관리하지 않음.
  3. 싱글톤 빈에서 프로토타입 빈을 사용할 때 문제 발생 → 주입 시점의 프로토타입 빈을 계속 사용.
  4. 해결책: ObjectProvider 또는 Provider 사용.
  5. 웹 스코프: @Scope("request") 등으로 활용 가능.
profile
프론트엔드 개발자

0개의 댓글