빈 스코프

  • 빈이 존재할 수 있는 범위를 뜻함
  • 대표적으로 싱글톤과 프로토타입이 있다. 프로토타입빈의 생성과 의존관계 주입만 관여하고 그 후에는  스프링 컨테이너가 관리하지 않는 매우 짧은 범위의 스코프. 항상 새로운 인스턴스를 생성해서 반환함.
  • 웹 관련 스코프는 request, session, application이 있다.
  • request: 웹 요청이 들어오고 나갈 때까지 유지되는 스코프(ex. 요청별 트랜잭션 ID 저장)
  • session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프(ex. 로그인 사용자 정보 저장)
  • application: 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프(ex. 애플리케이션 설정 정보 저장)
public class SingletonTest {

    @Test
    void singletonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);

        assertThat(singletonBean1).isSameAs(singletonBean2);

        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");
        }
    }
}



// ------------------------------------------------------------------



public class PrototypeTest {

    @Test
    void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

        // 클라이언트에서 직접 호출 해줘야함.
        prototypeBean1.destroy();
        prototypeBean2.destroy();
        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");
        }
    }
}

 

 

 

  • 싱글톤 스코프 빈이 프로토타입 스코프 빈을 주입받으면, 그 프로토타입 스코프 빈은 그대로 유지된다. 그래서 다음의 문제가 발생함.

싱글톤 스코프 빈이 프로토타입 스코프 빈을 주입받으면 생기는 문제

 

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Prototypebean.class);

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

        Prototypebean prototypeBean2 = ac.getBean(Prototypebean.class);
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, Prototypebean.class);

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

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }

    @Scope("singleton")
    static class ClientBean {
        private final Prototypebean prototypebean; // 생성 시점에 주입돼서, 똑같은 프로토타입 빈이 계속 쓰임. ClientBean은 싱글톤이기 때문

        @Autowired
        public ClientBean(Prototypebean prototypebean) {
            this.prototypebean = prototypebean;
        }

        public int logic() {
            prototypebean.addCount();
            int count = prototypebean.getCount();
            return count;
        }
    }

    @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 " + this);
        }

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




싱글톤 스코프 빈과 프로토타입 스코프 빈 동시 사용 해결책들

  • 의존관계를 주입받는 것이 아니라, 직접 찾는 것을 DL(Dependency Lookup)이라고 함.
  • ObjectFactory는 getObject()만 제공해서 가벼움. 스프링에 의존
  • ObjectProvider는 ObjectFactory를 상속해서 여러 다른 편의 기능을 제공. 스프링에 의존 
  • JSR-330 Provider는 get()만 제공함. 자바 표준이고, 지금 딱 필요한 DL 정도만 제공. 별도 라이브러리 가져와야 함.
  • 프로토타입 스코프 빈은 거의 사용하지 않긴 한다. 이런게 있다 정도로 알고 가자.

 

 

 

웹 스코프

  • 웹 환경에서만 동작. 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출됨.
  • 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 message) {
        System.out.println("[" + uuid + "]" + "[" + requestURL + "]" + "[" + message + "]");
    }

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

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



// --------------------------------------------------------------------



@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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



// -------------------------------------------------------------------------



@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURI().toString();
        myLogger.setRequestURL(requestURL);

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

 

  • 이러면 MyLogger가 request 스코프라, 오류가 발생한다. 요청이 들어와야 생성되는데, 요청이 안들어 왔으니 스프링 컨테이너가 없어서 주입을 못해주는 것이다.

 

 

 

스코프와 Provider

  • ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있음.
@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

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



// ------------------------------------------------------------




@Controller
@RequiredArgsConstructor
public class LogDemoController {

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

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

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

 

클라이언트 마다 별도 빈이 생성되고, 다른 데서 접근해도 동일한 스프링 빈을 접근하게 됨.

 

 

 

 

 

스코프와 프록시

  • 적용 대상이 클래스면, proxyMode를 TARGET_CLASS, 인터페이스면 TARGET_INTERFACE로 선택.
  • 실제 MyLogger가 아니라, 가짜 MyLogger를 넣어둠. 그러다가 요청이 들어오면 그 때 생성해서 진짜를 주입해주는 것.
  • 가짜 프록시 객체의 log()가 호출되고, 가짜 프록시 객체가 실제 request 스코프의  myLogger의 log()를 호출해줌.
  • 덕분에 싱글톤 스코프의 빈을 사용하는 것처럼 편하게 request 스코프 빈을 사용할 수 있다.
  • 핵심 원리는, Provider든 프록시든 실제 요청이 들어올 때까지(진짜 객체 조회를 꼭 필요한 시점까지) 생성을 지연한다는 것임.
  • 마치 싱글톤을 사용하는 것 같아서 주의해서 사용해야 함. 즉, 꼭 필요한 곳에만 최소화해서 사용해야 유지보수에 용이하다.

프록시 myLogger가 실제 myLogger.log() 호출

 

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
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() {
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "]" + "request scope bean create: " + this);
    }

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



// --------------------------------------------------------------------------------




@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURI().toString();
        myLogger.setRequestURL(requestURL);

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



// --------------------------------------------------------------------------------------



@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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