본문 바로가기

spring

빈 스코프

Scope 라는 단어는 범위 정도로 해석할 수 있을 것 같다.

변수 스코프란, 그 변수가 유효한 범위를 의미하듯이.

 

그런 의미로 스프링에서 빈 스코프는 빈이 유효한 범위 즉 라이프사이클의 범위를 의미한다.

 

스프링 빈은 기본적으로 싱글톤으로 생성되어 스프링 컨테이너가 생성되고 종료될때까지이다.

 

오늘 공부해볼 스프링의 빈 스코프는 싱글톤 스코프를 포함하여 세가지이다.

  • 싱글톤 : 스프링 컨테이너가 뜨고 종료될때까지 빈이 유지되는 스코프. 가장 넓은 범위이다.
  • 프로토타입 : 빈의 라이프사이클중 빈의 생성, 등록, 주입, 초기화까지만의 스코프를 가진다.
  • 웹 관련 스코프
    • request : 웹 요청이 들어오고 나갈때까지의 스코프를 가진다.
    • session : 세션이 생성되고 종료될 때까지의 스코프를 가진다.
    • application : 웹의 서플릿 컨텍스트와 같은 범위의 스코프를 가진다.
    • websocket : 웹 소켓과 같은 범위의 스코프를 가진다.

 

 

빈의 스코프 지정

스코프를 지정하는 것은 Bean 이기 때문에, Bean 등록하는 부분에 @Scope 에너테이션을 붙여 스콥을 지정할 수 있다.

 

@Scope
@Component
public class Car {
...
}
@Scope
@Bean
public 빈의타입 빈의이름 {
...
}

 

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

	@AliasFor("scopeName")
	String value() default "";

	@AliasFor("value")
	String scopeName() default "";

	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

}

 

에너테이션 정의를 보면, value Alias 를 가진 scopeName 옵션이 있는데, API 문서를 찾아보면 기본값은 "" 이고 기본값으로 지정하게 되면 위의 싱글톤 스코프로 동작하게 된다.

 

굳이 쓰자면 아래와 같다.

@Scope(value = "singleton")
@Component
public class Car {
...
}

 

 

 

프로토타입 스코프

싱글톤 스코프는 빈을 딱 하나만 만들고 스프링 컨테이너가 관리한다.

모든 클라이언트 요청들이 스프링 컨테이너 안의 그 빈 하나를 사용한다.

그에 반해 프로토타입 스코프는, 클라이언트의 요청마다 항상 새로운 인스턴스를 생성해서 반환한다.

 

  1. 클라이언트가 Bean 을 요청한다.
  2. 스프링 DI 컨테이너는 요청에 대해 새로운 Bean을 생성한다.
  3. 새로 만든 Bean 의 의존 관계 주입을 한다.
  4. @PostConstruct 와 같은 초기화작업을 진행한다.
  5. 이 생성된 빈을 스프링 컨테이너에서 관리하지 않고, 클라이언트에게 반환한다.

여기서 알 수 있는 점은, 각자 다른 클라이언트는 각자 다른 참조를 가진 빈을 가지게 된다는 것과

@PostConstruct 는 호출 되겠지만 @PreDestroy 는 호출되지 않을것이란 사실이다.

 

 

 

 

실습 코드를 통해 싱글톤 스코프와 프로토타입 스코프의 차이를 보자.

 

class SingletonScopeTest {

    @DisplayName("싱글톤 스코프 테스트")
    @Test
    void singleton() {
        // SingletonBean 에 @Component 를 붙이지 않아도 붙은것처럼 자동으로 빈 등록 해줌.
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        System.out.println("----Bean1 create----");
        SingletonBean bean1 = ac.getBean(SingletonBean.class);
        System.out.println("----Bean2 create----");
        SingletonBean bean2 = ac.getBean(SingletonBean.class);

        System.out.println("bean1 = " + bean1);
        System.out.println("bean2 = " + bean2);
        Assertions.assertThat(bean1).isSameAs(bean2);

        ac.close();

    }

    @Scope("singleton")
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

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

싱글톤 스코프로 빈을 등록했고, AnnotationConfigApplicationContext 로 스프링 컨테이너를 실행시켜 빈을 가져와보았다.

 

SingletonBean.init // 빈 생성은 컨테이너가 뜰 때.
----Bean1 create----
----Bean2 create----
bean1 = Tomas.core.scope.SingletonScopeTest$SingletonBean@2034b64c // 위와 같은 참조
bean2 = Tomas.core.scope.SingletonScopeTest$SingletonBean@2034b64c // 아래와 같은 참조

// 스프링 컨테이너 종료명령
17:38:46.095 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@3cc2931c, started on Sun Mar 21 17:38:45 KST 2021

// @PreDestroy
SingletonBean.destroy

AnnotationConfigApplicationContext 가 생성되고 빈들의 라이프사이클이 진행된 후 종료되는 과정을 볼 수 있다.

 

 

 

 

 

이번에는 프로토타입 스코프의 테스트를 작성해보자

public class PrototypeScopeTest {

    @DisplayName("프로토타입 스코프 테스트")
    @Test
    void singleton() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);

        System.out.println("----Bean1 create----");
        PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
        System.out.println("----Bean2 create----");
        PrototypeBean bean2 = ac.getBean(PrototypeBean.class);

        System.out.println("bean1 = " + bean1);
        System.out.println("bean2 = " + bean2);
        Assertions.assertThat(bean1).isNotSameAs(bean2);

        ac.close();
    }

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

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

 

 

각각의 빈 요청에 대해 따로 처리되는지를 알기 위해 getBean() 위에서 콘솔출력을 적었다.

 

 

----Bean1 create----
PrototypeBean.init
----Bean2 create----
PrototypeBean.init
bean1 = Tomas.core.scope.PrototypeScopeTest$PrototypeBean@2034b64c
bean2 = Tomas.core.scope.PrototypeScopeTest$PrototypeBean@75d3a5e0
17:44:03.658 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@3cc2931c, started on Sun Mar 21 17:44:03 KST 2021

 

  • 싱글톤 스코프에서 init 메서드가 한번만 호출된 것에 비해, 각각의 getBean() 에 대해 init() 가 실행된 것을 볼 수 있다.
  • 이렇게 따로 생성된 빈은 다른 참조를 가지고 있는 것을 볼 수 있다.
  • destroy() 는 실행되지 않는 것을 볼 수 있다.

또 주요한 차이로는, System.out.println("----Bean create----"); 의 출력시점을 보면 빈의 초기화가 언제 일어나는지 알 수 있다.

싱글톤 스코프같은 경우, 스프링 컨테이너가 뜰 시점에 빈들을 생성하고 초기화하기 때문에, init 메서드가 ----Bean create---- 보다 먼저 출력된 것을 볼 수 있다.

반면에, 프로토타입 스코프는 빈의 요청이 들어왔을시에 빈을 생성하고 반환하므로, ----Bean create---- 이후에 init 메서드가 각각 찍힌 것을 볼 수 있다.

 

 

프로토타입 빈은 스프링 컨테이너가 관리하지 않기 때문에, @PreDestroy 가 호출되지 않는다고 했다.
따라서 자원을 close() 하는 메서드를 빈 객체를 받은 클라이언트가 수동으로 관리해줘야한다.
예를 들어 DB connection 객체를 싱글톤 객체로 만든다면, @PreDestroy 로 자원을 반환하게 하면 되지만
프로토타입 빈은 직점 connection.close() 와 같은 코드를 사용해서 수동으로 자원을 반환하게 해야한다.

 

 

싱글톤 스코프와 프로토타입 스코프가 섞였을 때의 문제점

싱글톤 스코프 빈에 프로토타입 스코프 빈이 의존성이 있다면 문제가 발생한다.

 

싱글톤 스코프 빈의 특징을 잘 생각해보면 왜인지 알 수 있다.

싱글톤 스코프 빈은 스프링 컨테이너가 생성될 때 빈이 등록되고 의존성이 주입된다.

이후 하나의 빈 객체를 사용하게 된다.

 

의존 관계 빈이 이후 변경되지 않는다는 것이다.

프로토타입 스코프 빈이 의존관계 주입됬더라도 말이다.

 

하지만 프로토타입 스코프 빈을 사용하는 사용자는 매번 새로운 객체로 따로 사용하길 바랬을 것이다.

 

싱글톤 빈 안에 있는 프로토타입 빈은 싱글톤 빈 의존 관계 주입시점에 새로 생기긴 하지만 그 참조가 계속 쓰일 것이기 때문에 

새로 생기기는 하지만, 싱글톤 객체와 함께 유지된다.

 

 

싱글톤 빈을 요청할 떄 마다 내부의 프로토타입 빈을 새로 생성하는 해결법 1 : Dependency Lookup

프로토타입 빈은 컨테이너에게 새로 요청할 때 마다 새로운 객체를 생성해서 준다.

이를 이용하여, 싱글톤 빈이 프로토타입 빈을 이용할 때 마다 컨테이너에게 새로 요청하면 된다.

 

어떻게 하냐?

 

간단하다. 그냥 스프링 에플리케이션 컨텍스트를 주입받아서 getBean 하면된다.

 

class SingletonWithPrototypeTest {

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

        ClientBean bean1 = ac.getBean(ClientBean.class);
        int count1 = bean1.logic();
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean bean2 = ac.getBean(ClientBean.class);
        int count2 = bean1.logic();
        Assertions.assertThat(count2).isEqualTo(1);
    }

    static class ClientBean {

		// 스프링 빈 컨테이너 자채를 의존 관계로 주입받는다.
        @Autowired
        ApplicationContext annotationConfigApplicationContext;

        public int logic() {
        	// 이후 프로토타입 스코프 빈이 필요할 때 컨테이너에서 직접 의존 관계 빈을 찾아 쓴다.
            PrototypeBean prototypeBean = annotationConfigApplicationContext.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }


    @Scope("prototype")
    static class PrototypeBean {

        private int count = 0;

        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

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

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }
    }
}

 

 

@Autowired는 스프링 빈을 자동 주입해주는 기능 이외에,
ApplicationContext 같은 것도 편리하게 찾을 수 있는 부가 기능도 제공한다.

ApplicationContext bean = ac.getBean(ApplicationContext.class); // 에러

@Autowired ApplicationContext ac;

 

이렇게 필요한 의존관계를 직접 찾아 쓰는 방법을 Dependency Lookup 이라 한다.

우리가 알고있는 방법인 의존관계를 주입해주는 Dependency Injection 과는 다르다.

 

 

하지만 이 방법보다는 좋은 방법이 있다.

이 방법의 문제점은 다음과 같다.

  • 이 코드 자체가 스프링 컨텍스트 전체에 종속적이게 된다.
  • 단위테스트가 어려워진다.

모든 컨텍스트를 불러오는것이 아닌, 해당 프로토타입 빈만 딱 컨테이너에서 Dependency Lookup 방법이 알고싶다면 계속 아래로 이동하자.

 

 

 

싱글톤 빈을 요청할 떄 마다 내부의 프로토타입 빈을 새로 생성하는 해결법 2 : ObjectProvider

ObjectProvider, ObjectFactory 라는 클래스가 있다.

ObjectFactory 가 먼저 나왔고 후에 편의기능을 추가하여 확장시킨 클래스가 ObjectProvider 이다.

 

아무거나 써도 되지만, 기능도 많고 최신거를 기준으로 실습해봤다.

 

class SingletonWithPrototypeTest {

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

        ClientBean bean1 = ac.getBean(ClientBean.class);
        int count1 = bean1.logic();
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean bean2 = ac.getBean(ClientBean.class);
        int count2 = bean1.logic();
        Assertions.assertThat(count2).isEqualTo(1);
    }

    static class ClientBean {

//        @Autowired
//        AnnotationConfigApplicationContext annotationConfigApplicationContext;

        private ObjectProvider<PrototypeBean> prototypeBean;

        @Autowired
        public ClientBean(ObjectProvider<PrototypeBean> prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        public int logic() {
//            PrototypeBean prototypeBean = annotationConfigApplicationContext.getBean(PrototypeBean.class);
            PrototypeBean prototypeBeanObject = this.prototypeBean.getObject();
            prototypeBeanObject.addCount();
            int count = prototypeBeanObject.getCount();
            return count;
        }
    }


    @Scope("prototype")
    static class PrototypeBean {

        private int count = 0;

        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

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

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }
    }
}

 

코드를 보면, 프로토타입 스코프 빈을 의존관계에 주입한 것이 아니라

프로토타입 스코프 빈을 타입으로 가지는 ObjectProvider 를 의존 관계 빈으로 주입했다.

 

그리고 프로토타입 스코프 빈이 필요할 때 getObject() 메서드를 통해 프로토타입 스코프 빈을 새로 생성해 가져올 수 있다.

 

프로토타입 스코프 빈을 처음부터 생성하는 것이 아니라, ObjectProvider 를 이용하여 진짜 필요한 시점까지 지연시키는 것이다.

 

이 방법의 유일한 단점이라면, ObjectProvider ObjectFactory 의 패키지를 보면

import org.springframework.beans.factory.ObjectProvider;

이렇게 스프링에 의존하는데, 스프링이 아닌 컨테이너에서는 사용할 수 없다. (뭐 스프링이 아닌 컨테이너 사용 기회가 있을까 싶다만 .. )

 

따라서 자바 표준인 다음 방법을 이용할 수 있다.

 

 

 

싱글톤 빈을 요청할 떄 마다 내부의 프로토타입 빈을 새로 생성하는 해결법 3 : JSR-330 자바 표준 Provider

  • javax.inject.Provider 클래스에 있으며, 자바 표준이다.
  • javax.inject:javax.inject:1 의 의존성을 gradle 이나 maven 에 추가시켜야한다.
  • 사용법은 위의 ObjectProvider 와 비슷하다.
package javax.inject;
public interface Provider<T> {
    T get(); 
}
    static class ClientBean {

        private Provider<PrototypeBean> provider;

        @Autowired
        public ClientBean(Provider<PrototypeBean> provider) {
            this.provider = provider;
        }

        public int logic() {
            PrototypeBean prototypeBeanObject = this.provider.get();
            prototypeBeanObject.addCount();
            int count = prototypeBeanObject.getCount();
            return count;
        }
    }

 

 

프로토타입 스코프 ? new ?

사실 위의 문제는 언뜻보면 new 로 처리하면 될 걸 왜 저렇게 하나 라는 생각이 들 수 있다.

하지만, 프로토타입 스코프가 무엇이었는지 잘 생각해보면

 

의존관계 주입과 초기화가 끝난 객체를 매번 새로 생성하고 싶을 때 사용하면 된다.

new 로 생성한 객체는 이런 기능이 없다.

 

 

스프링 ObjectProvider ? JSR-330 Provider ? 

스프링 컨테이너가 아닌 다른 컨테이너에서도 돌려야한다면 자바 표준을 사용해야한다.

그게 아니라면, 보통은 스프링 기능이 다양한 편리한 기능을 제공하기에 스프링을 사용하면 된다.

이건 Provider 이외에도 적용되는 부분이다.

 

 

ObjectProvider, Provider 한마디

이 클래스들은 DL, 즉 필요한 의존 관계를 찾아서 주입하기 위한 클래스들이다.

프로토타입을 위한 클래스가 아니고, 프로토타입을 위해 쓸 수 있는 클래스인 것이다.

 

 

 

 

 

 

 

 

웹 스코프

  • 웹 환경에서만 동작한다. (springboot web starter 의존성 라이브러리 필요)
  • 프로토타입 스코프와는 다르게, 스코프의 종료까지 스프링 컨테이너가 관리해준다.

옵션 종류

 

  • request : 웹 요청이 들어오고 나갈때까지의 스코프를 가진다.
  • session : 세션이 생성되고 종료될 때까지의 스코프를 가진다.
  • application : 웹의 서플릿 컨텍스트와 같은 범위의 스코프를 가진다.
  • websocket : 웹 소켓과 같은 범위의 스코프를 가진다.

이런 옵션을 줄 수 있는데, 동작 방식은 모두 비슷하다. request 를 예시로 공부했다.

 

 

Http 요청과 웹 스코프 request

Http 요청은 많은 사용자가 요청을 보낼 수 있다.

웹 스코프 request 는 그 많은 요청들마다 각각의 빈을 생성해서 사용하도록 하는 것이다.

요청이 들어오고 요청에 대한 반환값을 클라이언트에게 줄 때 까지가 빈의 스코프이다.

어떤 면에서는 프로토타입 스코프와 비슷하다 볼 수 있겠다. (종료까지 관리해주느냐의 차이가 가장 큰 차이.)

스프링 부트는 웹 라이브러리가 추가되면, 스프링 빈 관리를 AnnotationConfigApplicationContext가 아닌
AnnotationConfigServletWebServerApplicationContext 로 관리한다.
이 컨텍스트는 웹과 관련된 추가 설정들이 들어있다.

 

 

요청마다 각각의 빈을 사용하는 것을 확인하기 위해 요청마다 각각의 UUID 를 부여하고 서로 다른 값을 가지는지 확인해보자.

스콥은 @Scope(value = "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() {
        // 번개 3번 연속맞을 확률보다 낮은 확률로 겹치는 유일한 UUID
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create : " + this);
    }

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

1. 필드로는 요청받은 Url, 해당 요청을 유일한 값으로 나타낸 UUID 를 가진다.

2. url 을 셋팅하는 메서드를 따로 둔다.

3. 로그를 찍는 log 메서드를 가진다.

4. 빈이 생성되고 종료되는 시점에 출력할 메서드를 작성한다.

 

 

 

이후 이 로그 클래스를 컨트롤러, 서비스 클래스 모두에서 사용한다 생각해보자.

코딩한다.

@Scope
@Controller
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    @Autowired
    public LogDemoController(LogDemoService logDemoService, ObjectProvider<MyLogger> myLoggerObjectProvider) {
        this.logDemoService = logDemoService;
        this.myLoggerObjectProvider = myLoggerObjectProvider;
    }




    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestUrl = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerObjectProvider.getObject();

        myLogger.log("controller test");
        logDemoService.logic("testId");

        return "OK";
    }
}

 

 

@Service
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    public LogDemoService(ObjectProvider<MyLogger> myLoggerObjectProvider) {
        this.myLoggerObjectProvider = myLoggerObjectProvider;
    }


    public void logic(String id) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();
    }
}

 

주의할 점은 이번에도 ObjectProvider 를 사용하여 Dependency Lookup 방식으로 로그 빈을 가져왔다는 것이다.

 

왜냐하면, @Scope("request") 는 Http 요청이 올 떄 빈을 생성한다 했다.

따라서 처음 스프링 컨테이너를 실행할 떄에는 로그 빈이 존재하지 않는다. 따라서 일반적으로 의존성 주입을 하면 에러가난다.

 

그래서 MyLogger 를 사용하는 시점, 즉 Http 요청이 들어온 시점에 생성된 빈을 스프링 컨테이너에서 가져오는 것이다.

 

[df1f1a10-8077-4ab8-8267-20644c1e4a46] request scope bean create : Tomas.core.common.MyLogger@3b0c4af5
[df1f1a10-8077-4ab8-8267-20644c1e4a46][http://localhost:8080/log-demo/]controller test
[df1f1a10-8077-4ab8-8267-20644c1e4a46][http://localhost:8080/log-demo/]testId
[df1f1a10-8077-4ab8-8267-20644c1e4a46] request scope bean destroy :Tomas.core.common.MyLogger@3b0c4af5

결과로 getObject() 시의 @PostConstruct 메서드,

컨트롤러의 log(),

서비스의 log(),

결과값 반환시 @PreDestroy 메서드 순으로 출력됬다.

 

또, 컨트롤러단이든 서비스단이든 한 요청에 대해서는 같은 하나의 빈 객체를 사용함을 알 수 있다.

컨트롤러에서도 getObject() 를 하였고, 서비스에서도 getOjbect() 를 해서 새로운 빈 객체를 생성해야할 것 같지만, 하나의 request 가 스코프이므로 하나의 요청에서는 컨테이너에 등록된 하나의 객체를 공유한다.

따라서 사용자는 여러 요청이 있더라도, 요청에 따라 따로 구분해주지 않아도 된다.

 

 

 

프록시 방식을 사용한 더 간단한 방법.

프록시란 앞단에서 대신 무언가 처리를 해주는 대리자 같은 것이다.

이 방법을 사용하면, ObjectProvider 를 사용하지 않고 원래 의존성 주입을 하듯이 해도 된다.

 

스프링 컨테이너가 뜰때에는 요청이 들어올때 생성해야하는 로그 빈을 등록하는 것이 아니라 프록시 객체를 빈으로 대신 등록한다.

 

@Configuration 처럼 CGLIB 바이트코드 조작 라이브러리를 이용하여 프록시 (가짜 객체) 를 일단 빈으로 등록해놓는 것이다.
의존관계 주입도 가짜 프록시 객체로 주입된다.


이 프록시 빈에는 실제 요청이 들어왔을때 진짜 실제 객체를 스프링에서 찾아오는 로직이 있다.
프록시 빈이 실제 빈을 찾는 방법을 알고 있다는 것이다.

 

따라서 이 방법 역시 ObjectProvider 처럼 무언가 껍데기를 미리 두고, 실제 객체 생성은 요청까지 지연시키는 것이다.


가짜 프록시 객체는 원본을 상속하여 사용하기 때문에, 객체를 사용하는 클라이언트는 가짠지 진짠지 모른다. (다형성)

또 이 가짜 프록시는 싱글톤 빈처럼 등록되어 모든 사용자의 요청은 같은 가짜 프록시 빈을 사용하고, 이 가짜 프록시 빈은 요청 각각에 다른 빈을 생성하게 해주는 것이다.

 

방법은, Scope 에너테이션에 proxyMode = ScopedProxyMode.TARGET_CLASS 를 지정하는 것이다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) // HTTP 요청당 하나의 빈을 생성하고, 요청이 끝날때 없애게 하는 스코프
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() {
        // 번개 3번 연속맞을 확률보다 낮은 확률로 겹치는 유일한 UUID
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create : " + this);
    }

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

 

@Scope
@Controller
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger; // 프록시 객체 의존 관계 주입

    public LogDemoController(LogDemoService logDemoService, MyLogger myLogger) {
        this.logDemoService = logDemoService;
        this.myLogger = myLogger;
    }

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

        myLogger.log("controller test"); // 가짜 프록시 객체의 메서드를 실행한 것. 그럼 가짜 프록시 객체는 내부적으로 진짜 객체의 메서드를 실행시켜주는 프록시 패턴.
        logDemoService.logic("testId");

        return "OK";
    }
}
@Service
public class LogDemoService {

    private final MyLogger myLogger;

    public LogDemoService(MyLogger myLogger) {
        this.myLogger = myLogger;
    }

    public void logic(String id) {
        myLogger.log(id);
    }
}

 

핵심은 provider 던, proxy 던 요청이 오기 전까지 빈 등록, 주입을 할 수 없는데, 가짜를 통해 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 것이다.
다형성, DI 컨테이너를 이용하여 애너테이션 설정만으로 원본 객체를 어떻게 처리할 수 있다는 것이다. 이것이 스프링의 강점이다.
AOP 도 비슷하게 동작한다. 가짜를 만들어 지연처리하는 그런 방식.

 

 

자바의 웹 서버는 Http 요청마다 요청을 하기 위해 ThreadLocal 이라는 클래스를 사용한다.
이 클래스는 UUID 를 사용해서 요청마다의 고유한 스레드를 만들어준다.

따라서 Http 요청마다 자동으로 병렬처리가 된다.