Spring/Spring 핵심 원리

#8 빈 스코프

히포파타마스 2021. 6. 14. 20:52

빈 스코프

 

1. 빈 스코프란?

빈 스코프란 빈이 존재할 수 있는 범위를 뜻한다.

 

스프링은 싱글톤, 프로토타입, 웹 관련 등 다양한 스코프를 지원한다.

 

빈 스코프는 다음과 같이 지정할 수 있다.

 

 

[컴포넌트 스캔 자동 등록]

@Scope("prototype")
@Component
public class HelloBean {}

[수동 등록]

@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}

 

 

2. 프로토타입 스코프

싱글톤 스코프의 빈은 조회하면 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.

 

반면에 프로토타입 스코프를 스프링 컨테이너에서 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.

 

프로토타입 빈 조회 과정은 다음과 같다.

 

 

[프로토타입 빈 요청1]

프로토타입 스코프의 빈을 스프링 컨테이너에 요청.

 

스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.

 

 

[프로토타입 빈 요청2]

스프링은 컨테이너에서 생성한 프로토타입 빈을 클라이언트에게 반환.

 

위와 같은 방식으로 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환 한다.

 

위의 과정에서 핵심 사항은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것이다.

 

프로토타입 빈을 관리할 책임은 빈을 받은 클라이언트에 있다.

 

때문에 종료 메서드에 대한 호출도 클라이언트가 직접해야만 한다.

 

 

3. ObjectProvider, JSR-330 Provider

 

스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.

 

그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제다.

 

이런 문제점을 해결하기 위해 지정한 빈을 컨테이너에서 대신 찾아주는 DL서비스를 제공하는 것이 바로 ObjectProvider이다.

 

 

[ObjectProvider 사용 예제]

    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;
    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }

ObjectProvider<클래스>로 객채를 생성 후 getObject() 메서드로 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다(DL).

 

또한, ObjectProvider 대신에 JSR-330자바 표준을 사용하는 방법도 있다.

 

이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle(dependencies 부분)에 추가해야 한다.

ex) implementation 'javax.inject:javax.inject:1'

 

사용법은 ObjectProvider와 유사하다.

 

 

[javax.inject.Provider 사용 예제]

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

Provider<클래스> 인스턴스를 생성 후, get()메서드로 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다(DL).

 

이 방법은 자바 표준이기 때문에 스프링이 아닌 다른 컨테이너에서도 사용 가능하다.

 

 

4. 웹 스코프

■ 웹 스코프의 특징

웹 스코프는 웹 환경에서만 동작한다.

웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 때문에 종료 메서드가 호출 된다.

 

■ 웹 스코프 종류

request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.

seesion: HTTP Session과 동일한 생명주기를 가지는 스코프

application: 서블릿 컨텍스트와 동일한 생명주기를 갖는 스코프

websocket: 웹 소켓과 동일한 생명주기를 갖는 스코프

 

 

5. 웹 스코프(request) 사용과 프록시

웹 스코프를 싱글톤 빈에서 사용하기 위해 아래와 같은 코드를 작성했다고 하자.

 

 

[웹 스코프(request) 사용 예제]

package hello.core.common;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;

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

클라이언트의 요청에 따라 UUID와 메세지, URL을 남기는 객체를 request 스코프로 작성한다.

 

 

[웹 스코프 컨트롤러]

package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;

@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.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

"log-demo"를 통해 클라이언트가 HTTP 요청을 보내면 웹 스코프 빈인 MYLogger에 의해 특정 log가 작성될 것이다.

 

그러나 위의 코드는 실제로는 정상적으로 작동하지 않는다.

 

LogDemoController는 싱글톤 빈이라 스프링을 실행하는 시점에 생성되지만 request 스코프 빈인 MyLogger는 생성되지 않아 주입 될 수 없기 때문이다.

 

MyLogger는 실제 고객의 요청이 들어올 때 생성이 된다.

 

이를 해결 하기 위해 프록시 방식을 사용 할 수 있다.

 

 

[프록시 적용]

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

request 스코프 빈에 proxyMode = ScopedProxyMode.TARGET_CLASS를 추가한다.

 

적용 대상이 인터페이스이면 TARGET_CLASS대신 INTERFACES를 선택하면 된다.

 

프록시를 추가하면 앞서 작성된 웹 스코프 컨트롤러가 정상적으로 작동된다.

 

프록시의 원리는 다음과 같다.

 

프록시는 CGLIB라는 라이브러리로 내 클래스(request 스코프)를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.

 

이 때문에 아직 request 스코프 빈이 생성되지 않아도 웹 스코프 컨트롤러는 정상적으로 생성된다.

 

 

[프록시 객체 사용 예시]

가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.

 

프록시의 핵심 아이디어는 진짜 객체조회를 꼭 필요한 시점까지 지연시킨다는 것이다.

 

이 때문에 Provider를 사용해서 request 스코프를 처리할 수도 있다.

 

※ 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.

 

■ 프록시 사용시 주의점

마치 싱글톤을 사용하는 것 같지만 실상 다르게 동작하기 때문에 주의해서 사용해야 한다.

이런 특별한 스코프는 꼭 필요한 곳에 최소한으로 사용해야한다. 무분별하게 사용하면 유지보수가 어렵기 때문.