본문 바로가기

Spring 프레임워크/이론

Spring의 빈 범위(Bean Scope)

스프링에서 Bean 객체를 생성한다는 것은 실제로는 Bean 객체를 생성하기 위한 일종의 레시피, 클래스를 정의하는 것이다. 이런 레시피를 작성하는 것은 객체의 의존성이나 설정값뿐 아니라 해당 객체의 범위(scope)를 조절할 수 있다는 특징이 있다. 특히 기존 자바 언어의 스코프와는 다른 스프링 프레임워크만의 스코프를 Bean 객체들에게 적용할 수 있는데 스프링에서는 다음과 같은 다섯 가지 스코프를 제공한다.

  • singleton: 해당 객체는 스프링 컨테이너에서 단 하나만 존재한다.

  • prototype: 해당 객체는 스프링 컨테이너에 몇 개든 존재할 수 있다.

  • request: 해당 객체는 web-aware 스프링 컨테이너에 전송된 HTTP 요청마다 생성된다.

  • session: 해당 객체는 web-aware 스프링 컨테이너에서 맺어진 HTTP 세션마다 생성된다.

  • global session: 해당 객체는 web-aware 스프링 컨테이너에서 맺어진 global HTTP 세션마다 생성된다.

위의 스코프 중 request, session, global session은 HTTP 통신과 관련된 스코프기 때문에 스프링으로 웹 기반 애플리케이션을 작성할 때만 적용할 수 있다. 이런 스코프는 @Scope 어노테이션에 매개변수로 넘겨서 지정할 수 있는데 이들에 대해 자세히 알아보자.

Singleton

Bean 객체가 Singleton, 싱글턴 스코프를 갖는다는 것은 현재 스프링 컨테이너에서 같은 종류의 Bean 객체는 오직 단 하나의 인스턴스만 유지된다는 것을 의미한다. 이는 스프링에서 기본 스코프이기도 하다.

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class);

Greeter g1 = ctx.getBean("greeter", Greeter.class);
Greeter g2 = ctx.getBean("greeter", Greeter.class);

System.out.println(String.format("%s: %s", "Is g1 and g2 same?", g1 == g2));
// Is g1 and g2 same?: true

위의 코드처럼 다른 시점에 얻은 객체라도 같은 Bean 객체라면 동일한 인스턴스인 것을 볼 수 있다. GoF 디자인 패턴에서 소개되는 싱글턴 패턴과 유사하면서도 다른 점은 GoF의 싱글턴에서는 개발자가 하드 코딩한 단 하나의 객체가 존재하는 것이라면 스프링의 싱글턴에서는 스프링 컨테이너가 직접 하나의 객체만 생성해서 제공한다는 점이다.

Prototype

이 Prototype, 프로토타입 스코프는 싱글턴 스코프와 반대되는 개념이다. 즉 의존 주입이나 getBean 메서드 호출처럼 매번 Bean 객체가 요청될 때마다 새로운 객체를 생성해서 반환해준다. 그렇기 때문에 상태를 유지할 필요가 있는 객체는 프로토타입 스코프를, 유지할 필요가 없는 객체는 싱글턴 스코프를 적용하는 것이 일반적이다.

    @Bean
    @Scope("prototype")
    public Greeter greeter(){
        Greeter g = new Greeter();
        ...
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class);
Greeter g1 = ctx.getBean("greeter", Greeter.class);
Greeter g2 = ctx.getBean("greeter", Greeter.class);

String msg1 = g1.greet("Spring");
g1.setFormat("%s, bye!");
String msg2 = g2.greet("Spring");

System.out.println(msg1); // Spring, Hello!
System.out.println(msg2); // Spring, Hello!
System.out.println(String.format("%s: %s", "Is g1 and g2 same?", g1 == g2)); // Is g1 and g2 same?: false

위처럼 @Scope("prototype") 어노테이션을 이용하여 프로토타입 스코프를 지정한 결과 getBean 메서드로 얻은 "greeter" 객체가 마치 매번 new 연산자로 생성한 것처럼 서로 다른 객체인 것을 확인할 수 있다.

 

유의할 점은 이전 포스트에서 언급한 Bean 객체의 생명주기의 차이다. 스프링 컨테이너에서는 Bean 객체의 생성, 초기화, 사용, 소멸을 담당하지만 이는 싱글턴 스코프의 객체에만 해당되는 얘기다. 정확히는 생명 주기의 생성 메서드는 호출하지만 소멸 메서드는 호출하지 않기 때문에 프로토타입 스코프의 객체는 사용자가 직접 자원 해제 코드를 실행해주거나 커스텀 Bean Post-processor를 작성해야 한다.

 

위의 싱글턴 스코프와 프로토타입 스코프를 섞어서 사용한다면 어떨까? 예를 들어 싱글턴 스코프의 객체에서 프로토타입 스코프의 객체를 의존한다고 해보자. 여러 싱글턴 스코프에서 같은 프로토타입 스코프의 객체를 의존한다고 했을 때 프로토타입 스코프의 객체는 요청할 때마다 생성되므로 각 싱글턴 스코프 객체가 서로 다른 프로토타입 스코프 객체를 갖게 될까? 결론적으로 말하면 그렇지 않다.

@Configuration
public class AppContext {
    @Bean(initMethod = "constructBean", destroyMethod = "destroyBean")
    @Scope("singleton")
    public Greeter greeter(){
        Greeter g = new Greeter();
        g.setProhibiter(prohibiter());
        g.setFormat("%s, Hello!");
        return g;
    }

    @Bean
    @Scope("prototype")
    public Prohibiter prohibiter(){
        return new Prohibiter();
    }
}
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class);
Greeter g1 = ctx.getBean("greeter", Greeter.class);
Greeter g2 = ctx.getBean("greeter", Greeter.class);

System.out.println(String.format("%s: %s", "Is g1 and g2 same?", g1 == g2));
System.out.println(String.format("%s: %s", "Is prohibiter of g1 and g2 same?", 
                                 g1.getProhibiter() == g2.getProhibiter()));
...

위처럼 싱글턴 스코프의 객체인 greeter 내부에 프로토타입 스코프의 객체인 prohibiter를 주입시켰다. 그리고 greeter 객체를 두 개 생성하여 각 객체가 내부에서 의존하고 있는 Prohibiter 객체가 동일한지 확인했을 때 그냥 생각하면 매 호출마다 다른 객체가 생성되는 프로토타입 스코프 특성상 두 객체가 동일하지 않다고 예상할 수 있을 것이다.

하지만 실제로 실행시켜보면 두 객체가 동일하다고 출력되는 것을 볼 수 있다. 왜 그런 것일까? 이는 Prohibiter 객체에 대한 의존성이 컨테이너 초기화 때 단 한 번만 주입되기 때문이다. 즉 싱글턴 스코프 객체에서 프로토타입 스코프 객체를 의존한다고 해도 일단 프로토타입 스코프 객체를 생성해서 주입하긴 하겠지만 처음 한 번만 그러고 나머지 싱글턴 스코프 객체에는 프로토타입 스코프 객체를 더 생성하지 않고 이전에 주입했던 객체를 그대로 다시 사용하는 것이다.

 

만약 그걸 원한다면 문제가 되진 않겠지만 매 싱글턴 스코프 객체에 각기 다른 프로토타입 스코프 객체를 주입하고 싶다면 메서드 인젝션이라는 방법을 사용할 수 있다고 하며 역시 이곳에서는 다루지 않겠다.

 

이 블로그에서 어떤 때 싱글턴 스코프를 쓰고 어떤 때 프로토타입 스코프를 사용하는지 정리해둔 게 있다. 단순히 어떤 개념이 존재하는지 인지하는 것뿐 아니라 왜 사용하는지 이해하는 것도 중요하기 때문에 한번 읽어봐야겠다.

 

[Spring] Spring Bean의 개념과 Bean Scope 종류 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

Request

Request, 리퀘스트 스코프는 말 그대로 HTTP Request 당 객체를 생성하는 방식이다. 그렇기 때문에 한 HTTP Request에서 부여받은 객체에 어떤 작업을 하던지 간에 다른 HTTP Request에 부여된 객체에는 영향을 끼치지 않는다. 요청에 대한 처리가 끝난 후 생성된 빈은 폐기(discard)된다.

Session

리퀘스트 스코프와 동일하지만 Session, 세션 스코프에서는 HTTP 요청이 아닌 서버와 클라이언트 간 세션 연결을 기반으로 객체를 생성한다. 마찬가지로 각 세션의 객체들은 서로 변경사항이 반영되지 않는 독립적인 객체들이며 사용이 끝난 뒤 폐기된다.

Global Session

Global Session, 글로벌 세션 스코프는 세션 스코프와 비슷하지만 포틀릿 기반 웹 애플리케이션에 적용된다고 한다. 아직은 서블릿이니 포틀릿이니 하는 개념에 익숙하지 않아서 이해하진 못했으나 이 블로그의 포스트가 이해하는데 도움이 될 수 있을 것이다.

 

포틀릿이란 무엇인가?

spring bean scope 중 globalSession 은 포틀릿 컨테이너를 지원한다. 포틀릿 컨테이너는 여러개의 포틀릿으로 구성되어있고 각 포틀릿은 세션을 소유하고 있다. 만약 전체적으로 변수를 저장하기 원한

circlee7.medium.com

 

 

이 외에도 애플리케이션 스코프, 웹소켓 스코프, 커스텀 스코프 등이 있는데 이는 이곳이나 이 문서를 참고하자.

 

Spring bean scopes [Updated for Spring 5] - HowToDoInJava

The beans in spring 3 container can be created in five different spring bean scopes i.e. singleton, prototype, request, session and global-session.

howtodoinjava.com

 

Quick Guide to Spring Bean Scopes | Baeldung

Learn how to quickly navigate the available bean scopes in the Spring framework.

www.baeldung.com

 

[참고 | docs.spring.io/spring-framework/docs/3.0.0.M3/reference/html/ch04s04.html]