Bean Scope

- Bean이 존재할 수 있는 범위

- @Scope 어노테이션으로 지정 가능

 

Spring Bean 종류

- 싱글톤 : 스프링은 기본적으로 싱글톤 스코프로 관리됨, 스프링 컨테이너의 시작~종료까지 함께 함

- 프로토타입 : 스프링 컨테이너는 프로토타입 빈 생성과 의존관계 주입까지만 관여, 이후에는 관여하지 않고 해당 빈은 클라이언트가 관리해야 함

- : 웹 환경에서 사용되는 스코프, request, session, application, websocket

 

프로토타입 스코프

- @Scope("prototype")

- 싱글톤 스코프 빈은 스프링 컨테이너에 의해 단 한 개의 빈으로만 관리되지만, 프로토타입 스코프의 빈을 조회하면 스프링 컨테이너는 항상 새로운 빈을 만들어서 전달해줌 (매번 사용할 때마다 항상 새로 만들어서 반환)

- 스프링 컨테이너는 요청이 오면 프로토타입 스코프 빈을 생성, 의존관계 주입, 초기화 후에는 관리를 하지 않음, 그러므로 @PreDestory 같은 종료 메서드는 호출되지 않음

public class ProtoTypeTest {

    @Test
    void protoTypeBean() {
        AnnotationConfigApplicationContext 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("ProtoTypeBean.init");
        }
        @PreDestroy
        public void destroy() { // 호출 안됨 (prototype 빈 조회시에 생성되고 컨테이너에서 관리X)
            System.out.println("ProtoTypeBean.destroy");
        }
    }

}
ProtoTypeBean.init
ProtoTypeBean.init
protoTypeBean1 = hello.core.scope.ProtoTypeTest$ProtoTypeBean@30e868be
protoTypeBean2 = hello.core.scope.ProtoTypeTest$ProtoTypeBean@66c92293

=> 서로 다른 빈이 생성됨, destroy() 호출 안됨

 

싱글톤 빈에서 프로토타입 빈을 사용하는 경우 가능한 문제점

- 프로토타입 빈의 특징은 매 요청마다 항상 새로 만들어진다는 것임

- 프로토타입 빈이 싱글톤 빈에서 사용되는 경우 이 특징대로 동작하지 않을 수 있음

public class SingleTonWithPrototypeTest1 {   

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientSingleTonBean.class, PrototypeBean.class);

        ClientSingleTonBean clientSingleTonBean = ac.getBean(ClientSingleTonBean.class);
        int count1 = clientSingleTonBean.logic();
        assertThat(count1).isEqualTo(1);

        int count2 = clientSingleTonBean.logic();
        assertThat(count2).isEqualTo(2);
        
        // => 동일한 prototype 빈을 사용하고 있음
    }

    // 싱글톤 빈에 프로토타입 빈을 주입하는 방법
    @Scope("singleton")
    static class ClientSingleTonBean {
        private final PrototypeBean prototypeBean;  // singleton 빈 안에있는 prototype 빈
        
        @Autowired  // prototype 빈을 주입받음, 이때 컨테이너가 prototype 빈을 생성해서 주입해줌
        public ClientSingleTonBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }
        
        public int logic() {
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }

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

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

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

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

}

- 싱글톤 빈 ClientSingleTonBean에서 PrototypeBean을 주입받아 사용하게 됨

- singleTonClientUsePrototype() 메서드에서 logic()을 두 번 사용하는데, 원하는 동작은 logic() 호출시마다 새로운 프로토타입 빈이 생성되길 원했지만, 그렇게 동작하지 않음 (count2 값이 2 = 기존 프로토타입 빈의 count값을 증가시켰기 때문)

=> 스프링이 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 되는데, 싱글톤 빈은 생성시점에만 의존관계 주입을 받고 변경되지 않기 때문에, 프로토타입 빈이 새로 생성되기는 하지만 싱글톤 빈과 함께 유지되는 문제점이 있음

 

사용할 때마다 항상 새로운 프로토타입 빈을 생성하는 방법

- Dependency Lookup : 의존관계를 주입받지 않고, 직접 필요한 의존관계를 찾는 것 (싱글톤 빈에서 필요한 프로토타입 빈을 직접 찾아서 받음)

- Provider를 사용하여 구현 가능

ObjectProvider

@Scope("singleton")
static class ClientSingleTonBean {
	@AutoWired
    private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;

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

- ObjectProvider.getObject()를 호출하면 스프링 컨테이너를 통해 해당 프로토타입 빈을 찾아서 받아옴

 

Provider

@Scope("singleton")
static class ClientSingleTonBean {
    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;

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

- Provider는 자바 표준임, 스프링에 의존적이지 않음

- javax.inject:javax.inject:1 라이브러리 필요

- Provider.get()을 호출하면 스프링 컨테이너를 통해 해당 프로토타입 빈을 찾아서 받아옴

 

웹 스코프

- 웹 스코프는 웹 환경에서만 동작

- 해당 스코프의 시작~종료까지 컨테이너에서 관리됨, 종료 메서드가 호출됨

  • request : http 요청하나 가 들어와서 나갈 때까지 유지, 각 http요청마다 서로 다른 빈이 생성되고 관리됨
  • session : http session과 동일한 생명주기
  • application : ServletContext와 동일한 생명주기
  • websocket : 웹 소켓과 동일한 생명주기
@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 msg) {
        System.out.println("["+uuid+"]"+"["+requestURL+"]"+msg);
    }

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

    @PreDestroy
    public void destroy() {
        System.out.println("["+uuid+"] request scope bean close" + this);
    }
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        System.out.println("myLogger = " + myLogger);

        myLogger.setRequestURL(requestURL);
        myLogger.log("LogDemoController Test");

        logDemoService.logic("testId");

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

    private final MyLogger myLogger;

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

- 싱글톤 빈인 LogDemoController, LogDemoService에 웹 스코프 빈인 MyLogger를 주입받으려 함 : 오류남

=> 오류 이유 : request 스코프 빈은 실제 http요청이 들어와야 생기는 빈으로, LogDemoControllㄹer, LogDemoServicer는 아직 생기지도 않은 MyLogger를 주입받을 수 없음

=> Provider로 해결

 

ObjectProvider, Provider

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    //private final ObjectProvider<MyLogger> myLoggerObjectProvider;
    private final OProider<MyLogger> myLoggerProvider;
    private final LogDemoService logDemoService;

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

        myLogger.setRequestURL(requestURL);
        myLogger.log("LogDemoController Test");

        logDemoService.logic("testId");

        return "OK";
    }

}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    //private final ObjectProvider<MyLogger> myLoggerObjectProvider;
    private final Provider<MyLogger> myLoggerProvider;

    public void logic(String testId) {
        //MyLogger myLogger = myLoggerObjectProvider.getObject();
        MyLogger myLogger = myLoggerProvider.get();
        myLogger.log("LogDemoService id = " + testId);
    }
}

- ObjectProvider, Provider를 사용해서, 실제로 필요할 때(getObject() 호출/get() 호출)까지 주입을 받지 않음

 

프록시

- MyLogger의 가짜 프록시 클래스를 만들어두고, http요청과 상관없이 MyLogger를 상속받는 가짜 프록시 클래스를 다른 빈에 주입함

@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) // MyLogger의 가짜 프록시 클래스를 만들어 두고 HTTP request와 상관없이 가짜 프록시 클래스를 미리 주입해둠
public class MyLogger {

    ...
}

 

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final MyLogger myLogger;    // 가짜 MyLogger 프록시 클래스를 미리 주입받음
    
    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        System.out.println("myLogger = " + myLogger);   // myLogger = hello.core.common.MyLogger@6634cbd4   // 가짜 MyLogger 프록시 클래스

        myLogger.setRequestURL(requestURL);
        myLogger.log("LogDemoController Test");

        logDemoService.logic("testId");

        return "OK";    
    }

}
@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;    // 가짜 MyLogger 프록시 클래스를 미리 주입받음    

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

- @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)를 사용하면 스프링 컨테이너는 CGLIB 바이트코드 조작 라이브러리로 MyLogger가짜 프록시 클래스를 생성해서 빈 등록함

- LogDemoController, LogDemoService에 MyLogger의 가짜 프록시 클래스가 주입됨

- Client에서 실제로 사용하는 요청이 오면 그때 내부에서 진짜 MyLogger빈을 찾아서 위임함

 

 

=> Provider, 프록시의 핵심은 진짜 빈 조회를 실제로 필요한 시점까지 지연할 수 있다는 점

=> 아주 간단한 어노테이션 설정으로 가짜 프록시 객체를 만들 수 있는 것이 다형성, DI컨테이너의 큰 장점