[스프링] 빈 스코프

June Lee·2021년 6월 14일
0

Spring

목록 보기
6/9

🌱 김영한님의 스프링 핵심 원리 - 기본편을 수강한 후 학습한 내용을 정리하고 기록하기 위해 작성하는 포스팅입니다.



스프링 빈에는 다양한 스코프가 존재한다.

스프링 빈 스코프

  1. 싱글톤: 스프링에서는 싱글톤 빈이 디폴트다. 해당 빈은 스프링 컨테이너 시작부터 끝까지 함께한다.
  2. 프로토타입: 요청마다 새로운 빈을 생성하여 반환한다. 또한 스프링 컨테이너는 빈의 생성과 의존관계 주입까지만 관여한다.
  3. 웹 관련 스코프
    1) Request: 요청이 들어오고 나갈 때까지 빈이 유지된다. 각각의 HTTP Request마다 별도의 빈이 생성되고 유지된다.
    2) Session: HTTP Session과 같은 생명 주기를 가지는 스코프
    3) Application: ServletContext와 같은 생명 주기를 가지는 스코프

2번 프로토타입 빈의 경우, 빈이 생성된 후 빈에 대한 관리 책임은 온전히 클라이언트에 있다. 따라서 @PreDestroy와 같은 어노테이션도 적용되지 않고, 클라이언트가 알아서 생명주기에 맞게 관리하고 없애기 전에 필요한 작업들을 필요한 시점에 호출해서 처리해줘야한다.

싱글톤 빈에서 프로토타입 빈을 멤버변수로 갖고 싶은 경우

문제는 위와 같은 상황에 발생한다. 싱글톤 빈과 프로토타입 빈은 서로 생명 주기가 다른데, 싱글톤 빈이 프로토타입 빈을 들고 있다면, 서로 다른 요청에 대해서 싱글톤 빈이 같은 프로토타입 빈을 공유하는 상황이 발생할 수 있다.
이와 같은 상황을 개발자가 의도했다면 문제가 되지 않지만, 그게 아니라 프로토타입 빈의 원래 특성처럼 매 요청마다 새로운 빈이 생성되어 반환되기를 원하는 것이라면 특별한 기법을 적용해줘야한다.

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

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertEquals(count1, 1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertEquals(count2, 1);
    }

    @Scope("singleton") // 생략 가능
    @RequiredArgsConstructor
    static class ClientBean {
//        private final ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
        private final Provider<PrototypeBean> prototypeBeanObjectProvider;

        public int logic(){
//            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.get();
            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 " + this);
        }

        public void destroy(){
            System.out.println("PrototypeBean.destroy " + this);
        }
    }
  1. ObjectProvider
private final ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
...
PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();

getObject()를 호출했을 때 스프링 컨테이너에 요청해서 해당 빈을 새로 받아온다. 즉, 빈을 생성하는 시점에는 임시로 ObjectProvider타입의 무언가만 넣어두고, 이후에 실제로 해당 빈이 필요할 때 Dependency Lookup 을 통해 객체를 받아온다.
옛날에는 ObjectFactory를 사용했는데, 거기에 여러 편의기능을 추가한 것이 ObjectProvider이다.

  1. Provider : javax 라이브러리
private final Provider<PrototypeBean> prototypeBeanObjectProvider;
...
PrototypeBean prototypeBean = prototypeBeanObjectProvider.get();

스프링이 아닌 다른 컨테이너에서도 사용 가능하고 심플하다.

cf)
위의 방법들은 꼭 프로토타입 빈이 아니더라도 Dependency Lookup이 필요한 경우 언제든 사용할 수 있다. 예를 들어, 순환참조의 경우 Dependency Injection이 안되는데(A를 생성하려면 B가 필요하고 B를 생성하려면 A가 필요하니까), DL로 하면 실제 필요한 시점 전까지 생성을 미룰 수 있기 때문에 이럴 때 사용하기도 한다.(Breaking Circular Dependencies)

그렇다면, 둘 중 어떤 방법이 더 많이 쓰일까? 결론부터 말하자면 1번 방법이 더 많이 사용된다. JPA와 Hibernate의 경우에는 자바 표준인 JPA가 승리했지만, 스프링은 스프링 자체적으로 제공하는 기능들이 de facto이기 때문에 표준을 쓰라고 권고하는 것들 외에는 굳이 자바 표준을 쓸 필요는 없다.(현재까지는..)


웹 스코프

웹 스코프는 프로토타입 빈과는 다르게 스프링에서 생성부터 소멸까지 관리해준다. 웹 스코프, 그중에서도 request scope를 활용할 수 있는 대표적인 예시는 로깅이 있다. 웹에서는 다양한 요청들이 한번에 들어오는데, 이런 로그들을 request scope를 활용해 request에 맞는 uuid와 함께 다음과 같은 형식으로 남겨주면 효율적으로 로그를 필터링해서 확인할 수 있다.

[uuid][requestURL]{message}

그런데, 예를 들어 다음과 같은 컨트롤러, 서비스 그리고 logger 클래스가 있다고 했을 때

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoSerivce;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) throws InterruptedException {
        String requestURL = request.getRequestURL().toString();
        // 실제 logger 인스턴스 생성 시점 
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        Thread.sleep(1000);
        logDemoSerivce.logic("testId");
        return "OK";
    }

}

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

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

proxyMode 설정이 없다고 생각하면, 스프링 컨테이너에서 Service와 Controller 빈을 생성하는 시점에는 MyLogger 빈이 존재하지 않기 때문에 문제가 발생한다. (MyLogger 빈은 request scope이기 때문에 요청이 들어왔을 때부터 요청이 반환될 때까지만 존재한다.)

이 문제를 해결하기 위한 방법이 2가지가 있는데, 그 중 한 가지는 위에서 배운 ObjectProvider(혹은 Provider)를 사용하는 방식이다. Controller와 Service에서 둘다 ObjectProvider를 사용해서 코드를 수정해줄 수 있다.

한 가지 다른 방법은 Proxy 객체를 사용하는 방식이다. 위와 같이 ProxyMode 설정을 해주면, DI 시에는 가짜 MyLogger 클래스(CGLIB라는 바이트코드조작 라이브러리를 이용해서 만든 MyLogger를 상속받은 가짜 프록시 객체)를 주입해준다. 그리고 실제 기능을 호출하는 시점에 진짜 MyLogger를 생성해서 호출한다. 이것이 가능한 이유는 이 가짜 프록시 객체가 내부적으로 진짜 myLogger를 찾는 방법을 알고 있기 때문이다.

특별한 스코프는 테스트하기도 힘들고, 유지보수도 힘들기 때문에 꼭 필요한 곳에서 최소한으로 사용해야한다! 보통은 백그라운드에서 사용하는 경우가 많다.


Provider든 Proxy든 그 핵심은 진짜 객체의 조회(및 생성)을 꼭 필요한 시점까지 지연처리한다는 점 이다.
스프링 컨테이너는 앱 실행 시점에 빈을 생성하고 의존관계를 주입해주는 작업을 해주기 때문에 이와 같이 잠시 가짜 객체를 만들어놓고, 이후 진짜 로직이 실행될 때에 진짜 객체로 바꿔치기 해줄 수 있는 기법들이 필수적이다.

cf.
추후에 다시 다루겠지만, 스프링의 또 다른 핵심 개념인 AOP도 이와 같은 방식으로 동작한다.

profile
📝 dev wiki

0개의 댓글