[Spring] 빈 스코프

개념

빈 스코프란 스프링 빈이 존재할 수 있는 범위를 이야기한다. 스프링은 다음과 같은 다양한 스코프들을 지원한다.

싱글톤 : 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 기본 스코프이다.
프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하는 짧은 범위의 스코프이다.
request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프이다.
session : 웹 세션이 생성되고 종료될때까지 유지되는 스코프이다.

 

 

싱글톤 (Singleton)

@Scope("singleton")

 

싱글톤 스코프란 앞서 공부했듯이 어플리케이션 전반에 걸쳐 해당 빈의 인스턴스를 오직 하나만 생성해서 사용하는 방법이다. 자바상의 싱글톤 패턴은 여러가지 단점을 가져오기도 하지만 스프링은 그러한 단점들을 모두 해결해준다.

참고) https://eckrin.tistory.com/entry/Spring-%EC%8B%B1%EA%B8%80%ED%86%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88

 

 @Scope("singleton")
 static class SingletonBean {
 
     @PostConstruct
     public void init() {
         System.out.println("SingletonBean.init");
     }
     
     @PreDestroy
     public void destroy() {
         System.out.println("SingletonBean.destroy");
     }
 }
 SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
 SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
 
 System.out.println("singletonBean1 = " + singletonBean1);
 System.out.println("singletonBean2 = " + singletonBean2);
 
 assertThat(singletonBean1).isSameAs(singletonBean2);
 //true

 

 

 

프로토타입 (Prototype)

@Scope("prototype")

싱글톤과 반대로 프로토타입 스코프의 경우 스프링 컨테이너가 요청시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한 후 반환해준다. 클라이언트에 빈을 반환한 이후, 스프링 컨테이너는 생성한 프로토타입 빈을 관리하지 않는다. 따라서 초기화는 이루어지지만 종료 메소드는 호출되지 않는다.

 

 

 @Scope("prototype")
 //@Component << 스코프 등록시 또 등록해줄필요없다
 static class SingletonBean {
 
     @PostConstruct
     public void init() {
         System.out.println("PrototypeBean.init");
     }
     
     @PreDestroy
     public void destroy() {
         System.out.println("PrototypeBean.destroy");
     }
 }
 PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
 PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
 
 System.out.println("prototypeBean1 = " + prototypeBean1);
 System.out.println("prototypeBean2 = " + prototypeBean2);
 
 assertThat(prototypeBean1).isSameAs(prototypeBean2);
 //false
 
 
 //직접 호출해주지 않으면 종료메소드가 호출되지 않는다.
 prototypeBean1.destroy();
 prototypeBean2.destroy();

 

 

 

주의 : 싱글톤에 종속적인 프로토타입

 

만약 싱글톤으로 생성된 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하면 문제가 발생할 수 있다. 프로토타입 빈은 사용시점마다 새롭게 생성되는 방식으로 동작하기를 기대하고 사용한다. 그런데 만약 싱글톤 빈이 프로토타입 빈을 주입받게 되면, 싱글톤 내의 프로토타입 빈은 주입시점에만 생성되고 이후에 클라이언트의 접근이 새로 이루어진다고 해서 또 다시 생성되지는 않는다. 

 

static class SingletonBean {
    private final PrototypeBean prototypeBean;
    
    @Autowired //싱글톤 주입
    public SingletonBean(PrototypeBean prototypeBean) {
        this.prototypeBean = prototypeBean; //생성자 실행시 프로토타입 주입
    }
    
    public void addProCnt() {
        prototypeBean.addCnt();
    }
    
}

@Scope("prototype")
static class PrototypeBean {
    private int cnt=0;
    
    public void addCnt() {
        cnt++;
    }
    
    public int getCnt() {
        return cnt;
    }
}

프로토타입으로 설정된 PrototypeBean이 싱글톤 빈 내부에서 @Autowired로 의존관계 주입이 이루어지고 있다.

 

만약 이 때 클라이언트 2개가 SingletonBean을 통해서 PrototypeBean의 cnt를 증가시킨다고 해보자.

SingletonBean sb1 = ac.getBean(SingletonBean.class);
sb1.addProCnt(); //cnt=1

SingletonBean sb2 = ac.getBean(SingletonBean.class);
sb2.addProCnt(); //cnt=2

싱글톤으로 설정되었으므로 SingletonBean을 getBean을 통해서 조회했을 때 항상 같은 싱글톤 빈을 조회하게 된다. 그런데 싱글톤 빈 내의 프로토타입 빈도 이미 과거에 주입이 끝난 빈으로서, 싱글톤과 생명주기를 같이하게 된다.

 

즉, 프로토타입빈의 cnt값도 클라이언트 간에 공유되며, 프로토타입으로 설정한 의미를 상실하게 된다.

 

 

ObjectProvider

위와 같은 문제를 해결하기 위해서 간단하게 addProCnt()메소드에 getBean을 통해서 프로토타입 빈을 주입받는 방법이 있다.

@Autowired
private ApplicationContext ac;

public void addProCnt() {
    PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
    prototypeBean.addCnt();
}

그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 된다. 이렇게 코드 내부에 getBean을 통해서 찾는 이러한 코드를 Dependency Lookup(DL)이라고 하는데, 이러한 코드는 과도하게 사용시 Dependency Injection(DI)와 반대로 관련 코드들이 오브젝트 내에 위치하여 코드의 복잡성이 증가하고 테스트하기도 어려워지는 등의 단점이 있기 때문에, 이를 위해서 ObjectProvider를 사용하자.

 

@Autowired //(생성자 주입으로 사용해도 된다)
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public void addProCnt() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
    prototypeBean.addCnt();
}

말 그대로 provider는 컬렉션과 같이 프로토타입 빈을 제공해주는 역할을 한다. ObjectProvider의 getObject()를 호출하면 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

 

 

 

웹 (Web)

request : HTTP요청 하나가 들어오고 나갈때까지 유지되는 스코프. 각각의 HTTP요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
session : HTTP session과 동일한 생명주기를 갖는 스코프
application : 서블릿 컨텍스트와 동일한 생명주기를 갖는 스코프
websocket : 웹 소켓과 동일한 생명주기를 갖는 스코프

 

request

동시에 여러 HTTP요청이 오면 어떤 요청이 남긴 로그인지 구분하기 위해서 사용할 수 있다. 

@Scope(value = "request")
public class MyLogger {...}

스코프를 request로 지정해주면 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸한다. 또 서비스 계층에서는 웹 기술에 종속되지 않고 작동하는 것이 좋은데, request scope를 사용하지 않으면 서비스 계층까지 웹에 관련된 정보들이 넘어가기 때문에 좋지 않다.

request scope를 사용한다면 웹 정보를 위의 MyLogger같은 request scope class에서 멤버변수로 전달받아 저장,처리할 수 있다.