Java Spring Core

Bean Scope

Bean Scope라고 하는 것은 Spring Bean이 생존해 있는 범위를 의미한다. 즉, 어플리케이션을 실행하는 동안 Spring Bean이 활성화 되어 있는 상태를 유지하다가 소멸되기까지의 그 범위를 뜻한다.

Scope Types

Spring Bean은 다양한 Scope을 가진다.

Bean Type Description
Singleton 가장 긴 스코프를 가진 빈으로, 스프링 컨테이너의 시작과 끝까지 유지
Prototype 사용자의 요청에 의해 생성되며, 생성될때 의존관계 주입까지만 스프링 컨테이너가 관리하며, 그 이후 영역은 클라이언트의 몫
Request 웹 요청이 들어오고 나가기까지의 스코프
Session 웹 세션이 유지되는 기간 동안의 스코프
Application 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

스코프 명시 방법 (프로토타입 스코프 생성 예시)

@Scope("prototype")
class PrototypeClass{

}

스프링은 기본적으로 싱글톤 스코프를 생성하는게 기본으로 설정되어 있다.

Prototype Scope

프로토타입 스코프는 필요할 때 요청되며, 그 순간 생성되며 의존관계 주입까지만 스프링 컨테이너가 관리하며 그 이후는 스프링 컨테이너에서 관리하지 않는다.

아래 그림처럼, 스프링 컨테이너에서 매번 요청 시 새로운 인스턴스를 반환받게 된다. prototype_scope

의존관계 주입 및 초기화 관계까지만 관리되고, 그 이후 과정은 스프링이 관리하지 않으므로, @PreDestroy는 호출되지 않는다.

Test

public class PrototypeTest {
    @Test
    void prototypeBeanFindTest(){
        ConfigurableApplicationContext ac=new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close();
    }

    @Scope("prototype")
    static class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println("Init method executed");
        }
        @PreDestroy
        public void destroy(){
            System.out.println("Destroy method executed");
        }
    }
}

Results

Init method executed
Init method executed
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@7dfb0c0f
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@626abbd0

위의 결과를 보면 알듯이 매 호출마다 새로운 인스턴스를 반환받는 것을 확인할 수 있고, @PreConstruct(초기화 메소드)는 실행되지만, @PreDestroy(종료 메소드)는 실행되지 않는 것을 확인할 수 있다.

Singleton Combined with Prototype Scope

Prototype Scope Example

예제를 통해 싱글톤 스코프와 프로토타입 스코프를 같이 활용하는 Spring Bean을 살펴보자

prototype_example

위 처럼, Count를 저장하고 있는 프로토타입 스코프가 있다고 가정하자. client A가 addCount을 호출하고, client B가 addCount을 호출하더라도 count의 값은 각각 1이다. (client가 각각 참조하고 있는 Prototype 객체는 다르기 때문에)

Test

public class SingletonWithPrototypeTest1 {
    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);

    }

    @Scope("prototype")
    static class PrototypeBean{
        private int count=0;

        public void addCount(){
            count+=1;
        }

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }
        @PostConstruct
        public void init(){
            System.out.println("Init method executed: "+this);
        }

        @PreDestroy
        public void destroy(){
            System.out.println("Destroy method executed: "+this);
        }
    }
}

해당 테스트를 실행해보면 정상적으로 오류 없이 실행되는 것을 확인할 수 있다.

Combining With Singleton

여기서, 해당 프로토타입 스코프를 DI로 주입 받는 싱글톤을 생성한다고 해보자

singleton_prototype1

위와 같이 ClientBean이라는 싱글톤 스코프틑 이전의 ProtoypeBean을 DI로 받게 된다. Client Bean에서는 생성자 시점에서 DI를 받도록 구성해놓게 되면, Client Bean이 생성되는 단계에서 PrototypeBean을 생성하여 이를 DI로 주입받아 내부 변수로 보관하고 있게 된다.

이때, client가 ClientBean에 대해 logic(addCount)을 요청하면 어떻게 될까?

이전에는 prototype bean이 2개가 생성되어서 각각 count가 1로 유지되었지만, 이번에는 싱글톤 스코프에 요청을 하는 것이므로, 매번 같은 인스턴스에 접근하게된다. 그래서 count가 2로 설정되게 된다.(prototype bean이 매번 생성되는것이 아니라 client bean에 저장된 prototype bean이 참조된다.)

Test

@Test
void singletonClientUserPrototype(){
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.class);

    ClientBean clientBean1 = ac.getBean(ClientBean.class);
    ClientBean clientBean2 = ac.getBean(ClientBean.class);

    int count1=clientBean1.logic();
    assertThat(count1).isEqualTo(1);
    System.out.println("PrototypeBean = " + clientBean1.getPrototypeBean());

    int count2=clientBean2.logic();
    assertThat(count2).isEqualTo(1);
    System.out.println("PrototypeBean = " + clientBean2.getPrototypeBean());

}

Results

PrototypeBean = hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@23f5b5dc
PrototypeBean = hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@23f5b5dc

위의 테스트가 정상적으로 시행되는 것을 확인할 수 있고, prototype 인스턴스도 같은 것을 확인할 수 있다.

하지만, 실제로 우리가 원하는 실행 결과가 아닐 것이다. 매번 새로운 Prototype Scope를 받아서 실행되어 count가 1로 되게끔 하고 싶다면 어떻게 해야될까?

Using Spring Container

가장 간단한 방법으로는, Spring Container에서 매번 Spring Bean을 받아와서 Prototype Bean에 등록해주는 것이다.

@Autowired
private ApplicationContext ac;

public int logic() {
  PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
  prototypeBean.addCount();
  int count = prototypeBean.getCount();
  return count;
}

다음 처럼, 스프링 컨테이너 변수를 멤버 변수를 가지고 있다가, logic 호출 될때마다 Prototype Bean을 요청해서 매번 새로운 인스턴스를 활용하는 것이다.

이렇게 DI가 아닌, 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL)이라고 한다.

하지만, 다음과 같이 Spring Container를 변수로 두고 활용하게 되면 Client Bean은 스프링에 종속적인 코드가 되어, 이후의 단위테스트를 진행하는 것이 어렵다, DL만을 위해 Spring Container을 주입받아서 확인하는 것은 너무 비효율적이다.

Using ObjectProvider

Spring에서는 DL을 제공하기 위해 ObjectProvider 라는 클래스를 제공한다.

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
  PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
  prototypeBean.addCount();
  int count = prototypeBean.getCount();
  return count;
}

이렇게 되면 ObjectProvider을 이용해서 DL 기능을 수행할 수 있을 뿐더러, Spring의 기능을 제한적으로 사용하여 단위테스트 또한 수월해진다.

JSR-330 Provider

ObjectProvider가 스프링에서 제공해주는 라이브러리 였다면, JSR-330 Provider은 자바 표준의 라이브러리이다.

build.gradle

implementation 'javax.inject:javax.inject:1'

위의 부분을 build.gradle에 추가 시켜 라이브러리를 추가한다.

@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
  PrototypeBean prototypeBean = provider.get();
  prototypeBean.addCount();
  int count = prototypeBean.getCount();
  return count;
}

ObjectProvider와 달리 JSR-330 Provider은 javax표준의 라이브러리로 스프링 컨테이너가 아닌 곳에도 활용할 수 있다.

PrototypeBean과 같이 매 사용마다 새로운 인스턴스를 활용해야되는 경우 위와 같이 Provider을 이용해서 매번 새로운 인스턴스를 요청할 수 있다. 하지만 웬만한 경우, 싱글톤 스코프로 해결된다.

JSR-330, ObjectProvider 모두 DL 기능을 수행하기 위한 객체들이다. 어떤 것을 사용해도 상관없으므로, 필요에 따라 선택해서 사용하면 된다. 대부분, 스플링 환경에서 application을 생성할 일이 많으므로 Spring 방식의 ObjectProvider을 활용하는 것이 좋다.

WebScope

Webscope는 웹 환경 아래에서 작동하는데, Prototype Scope와 달리 종료시점까지 스프링이 관리하게 된다. 따라서, @PostConstruct, @PreDestroy 메소도도 호출된다.

Webscope의 종류에는 Request,Session,Application이 있는데, Request을 중점적으로 다뤄보도록 하자.

request_scope

request scope는 위와 같이 매 Http 요청마다 생성되어서 관리된다. Controller 와 Service는 request를 이용해서 사용자의 요청을 처리한다.

Request Scope Example

build.gradle

웹스코프를 다루기 위해서는 웹환경 내에서 app이 작동해야하므로, tomcat library를 추하가한다.

//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'

Request Scope

@Component
@Scope(value="request")
public class MyLogger {
    private String uuid;
    private String requestedURL;

    public void setRequestedURL(String requestedURL) {
        this.requestedURL = requestedURL;
    }

    public void log(String message){
        System.out.println("["+ uuid + "]"+"["+ requestedURL + "]"+"["+ message + "]");
    }

    @PostConstruct
    public void init(){
        uuid=UUID.randomUUID().toString();
        System.out.println("["+ uuid + "]"+"["+ requestedURL + "]"+"[request scope created: "+this+"]");
    }

    @PreDestroy
    public void destroy(){
        System.out.println("["+ uuid + "]"+"["+ requestedURL + "]"+"[request scope closed: "+this+"]");
    }
}

Scope

@Scope("request")

Request Scope를 설정하기 위해 scope를 request로 등록한다.

UUID

@PostConstruct
public void init(){
    uuid=UUID.randomUUID().toString();
    System.out.println("["+ uuid + "]"+"["+ requestedURL + "]"+"[request scope created: "+this+"]");
}

UUID.randomUUID()를 통해 Request Scope 객체에 고유한 Id 값을 할당하여, 다른 Request 객체랑 구분할 수 있다.

RequestedURL은 Request 객체가 생선되는 시점에서는 알수 없으므로 setter 함수를 통해 나중에 입력받는다 –> 지연 실행과 관련 있는 부분

MyLoggerService

Request 객체를 처리하는 Service 객체를 생성한다. Service 객체 내에 Request 객체를 통해 message를 등록하고 있는데, 이를 통해 불필요한 웹 관련 정보가 Controller에서 Service로 넘어가는 것을 막아준다. 즉, Service에서는 Web 관련 정보를 직접적으로 다루지 않고, Controller에서 끝낼 수 있다. 이 또한, 정보의 은닉화가 진행된 것으로 생각해도 된다.

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;

    public void logic(String id){
        myLogger.log("service id="+ id);
    }
}

MyLoggerController

Request 객체와, Service 객체를 처리하는 Controller 객체를 생성한다.

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestedURL = request.getRequestURL().toString();

        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestedURL(requestedURL);
        myLogger.log("controller-test");
        logDemoService.logic("test-id");
        return "OK";
    }
}

Expected Result

[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close

Actual Result

Error creating bean with name 'myLogger': Scope 'request' is not active for the
current thread; consider defining a scoped proxy for this bean if you intend to
refer to it from a singleton;

Http Request 요청이 없는 상태에서는 Request 객체가 생성되지 않기 때문에 해당 에러가 발생되면서 실행이 되지 않는다. 이럴 때, Request 객체가 지금 당장에는 없더라도 Request 객체를 사용할 수 있도록 지연 실행 처리를 진행해야한다. 이때 사용하는 것이 Provider 이다.

ObjectProvider 사용

Controller

private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
  String requestURL = request.getRequestURL().toString();
  MyLogger myLogger = myLoggerProvider.getObject();
  ...
}

Service

private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id){
    MyLogger myLogger=myLoggerProvider.getObject();
    myLogger.log("service id="+ id);
}

ObjectProvider을 이용해서 ObjectProvider.getObject()를 호출하기 전까지 Request Scope의 생성을 지연할 수 있다. 즉, Request Scope가 없는 상태에서도 정상적으로 동작할 수 있다.

Scope, Proxy

Provider 방식 말고 이용할 수 있는 다른 방법은 Scope와 Proxy를 이용하는 것이다.

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

위와 같이 Logger (Request Scope)에 대해서 proxy 설정을 해둔다.

ProxyMode |Mode|Description| |–|–| |TARGET_CLASS|클래스에 대한 프록시 설정| |TARGET_INTERFACES|인터페이스에 대한 프록시 설정|

이렇게 하면 ObjectProvider을 이용하지 않더라도, Request 객체에 대한 지연 처리를 진행할 수 있다.

Proxy Mechanism

프록시는 이전에 배운 AOP의 적용된 것과 유사하게, 클래스에 대한 가공을 통해 실제 클래스의 실행 전에 프록시를 통한 중계가 이루어진다.

System.out.println("myLogger = " + myLogger.getClass());

위의 코드를 실행해보면 아래의 클래스가 반환된 것을 확인할 수 있다.

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$60e946e8

기존의 MyLogger 클래스가 아닌, MyLogger 클래스에 CGLIB 클래스의 형태로 가공된 형태의 클래스임을 확인할 수 있다.

scope_proxy

위의 그림처럼, client은 Request에 대한 요청을 수행하지만, 실제 Request 객체가 실행을 처리하기 전에, Proxy 객체가 이 작업 요청을 받아서, 실제 클래스에 작업 요청을 의뢰한다.

이렇게 하므로써 Request 객체가 없더라도, Request 객체를 사용하는데는 아무런 문제가 없다. 나중에 Http Request을 통해 Request 객체가 생성되게 되더라도, Request Proxy가 실제 Request 객체로 요청을 의뢰하게 된다.

References

link: inflearn

link:springcore

link:spring_framework

댓글남기기