본문 바로가기
개발 관련 공부/스프링 김영한 로드맵

[스프링 기본] 9. 빈 스코프

by 슴새 2022. 10. 17.
반응형

빈 스코프란?

말 그대로 빈이 존재할 수 있는 범위

스프링은 아래 스코프들을 지원한다.

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
  • 웹 관련 스코프
    • request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
    • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다

싱글톤 프로토타입 리퀘스트 정도 알면 된다.

스프링 빈은 기본적으로 싱글톤 스코프로 생성된다.

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

이렇게 빈 등록할때 @Scope로 지정 가능

수동 등록의 경우 @Bean 위에 @Scope를 붙이면 됨.

프로토타입 스코프

싱글톤 스코프는 스프링 컨테이너가 생성될 때 초기화 메서드가 실행되어 만든 인스턴스를 돌려쓴다.

반면 프로토타입 스코프의 빈을 조회하면, 싱글톤과는 달리 항상 새로운 인스턴스를 생성해서 반환한다.

요청이 들어오면(빈을 조회하려고 하면) 그때 빈을 만들고 DI를 주입한다.

그렇게 만들어진 빈을 반환하고, 더이상 관리하지 않는다.

@PreDestroy같은 종료메서드는 호출되지 않는다. 이런 관리는 클라가 해야 한다.

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

싱글톤 빈과 프로토타입 빈을 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.

static class ClientBean {
    private final PrototypeBean prototypeBean;
     @Autowired
     public ClientBean(PrototypeBean prototypeBean) {
         this.prototypeBean = prototypeBean;
     }
     public int logic() {
         prototypeBean.addCount();
         int count = prototypeBean.getCount();
         return count;
     }
 }

예를 들어 다음과 같은 상황이 있다고 하자.

  • 싱글톤 스코프인 clientBean
  • clientBean은 내부에 프로토타입 스코프인 PrototypeBean을 가짐
  • PrototypeBean은 내부 변수 count를 1 증가시키는 addCount()함수를 가짐

이 상황에서 clientBean-prototypeBean-addCount()를 두 번 호출하면 count=2가 된다.

clientBean이 내부에 가지고 있는 프로토타입 빈은, 주입 시점에 요청 1회가 일어나 생성된 것이다. 주입할때 요청이 일어나므로...사용할 때마다 프로토타입 빈이 생성되는 것이 아니다. 그래서 프로토타입 빈이 유지되었기 때문에 count=2가 된것이다.

 

하지만 우리는 clientBean을 같은 인스턴스로 유지하면서 내부의 prototypeBean만 새로 생성되기를 원한다.(count=1이 되기를 바람) 솔직히 count=2로 만들거면 안의 프로토타입빈을 프로토타입으로 만들 이유가 없다. 

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

스프링 컨테이너에 요청

static class ClientBean {
    @Autowired
     private ApplicationContext ac; //여기가 별로임
     public int logic() {
         PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
         prototypeBean.addCount();
         int count = prototypeBean.getCount();
         return count;
     }
 }

가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.

그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.

 

ObjectFactory, ObjectProvider

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider; //변경된 부분
public int logic() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); //변경된부분2
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

필요한 건 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 Dependency Lookup (DL) 의존관계 조회(탐색)기능이다. 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider이다.

위 코드를 실행하면 prototypeBeanProvider.getObject()를 통해 항상 새로운 프로토타입 빈이 생성된다.
(ObjectProvider 의 getObject()가 스프링 컨테이너를 통해 해당 빈을 찾아서 반환함)

 

JSR-330 Provide

먼저 gradle에 가서 설정을 추가해준다.

static class ClientBean {
     
    @Autowired
    private Provider<PrototypeBean> provider;
    public int logic() {
        PrototypeBean prototypeBean = provider.get(); //변경된부분2
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

이후 코드 작성.

 

ObjectProvider는 스프링에 의존하기 때문에 스프링이 아닌 다른 컨테이너에서 사용할 수 없다.

하지만 Provider는, 별도의 라이브러리가 필요한 대신 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

 

그러니까 스프링 컨테이너만 사용하면 ObjectProvider를 쓰고, 특별히 다른 컨테이너를 쓰는 경우에만 JSR-330 Provider를 쓰면 된다. 사실 프로토타입 빈 문제가 아니더라도 dl이 필요한 경우 이 둘을 사용하면 된다. 

 

웹 스코프

웹 스코프 중 request 스코프에 대해 알아보자.

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

 a 전용 인스턴스는 a요청이 끝나면 파괴가 된다.

 

request 스코프 예제 만들기

동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.

이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.

@Component
@Scope(value = "request")
public class MyLogger {
    private String uuid;
    ....
    @PostConstruct //uuid를 생성해서 저장하는 기능 가진 어노테이션
    public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);
    }
   
}

로그를 출력하기 위한 MyLogger 클래스를 request 스코프로 작성한다.

uuid라는 스트링 값을 이용해 요청들을 구분할 것이다.

 

@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와 logic는  uuid와 메세지를 출력하는 함수.

 

실행하면 오류가 발생한다.

스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하다.

하지만 request 스코프 빈은 요청이 오지 않으면 생성되지 않아서 생성된 오류이다.

 

 

스코프와 Provider

첫번째 해결방안은 Provider를 사용하는 것이다.

아까 배운 ObjectProvider를 사용해보자.

ObjectProvider는 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다. 

private final ObjectProvider<MyLogger> myLoggerProvider; //변경된 부분
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
    String requestURL = request.getRequestURL().toString();
    MyLogger myLogger = myLoggerProvider.getObject();
    myLogger.setRequestURL(requestURL);
    myLogger.log("controller test");
    logDemoService.logic("testId");
    return "OK";
}

그러면 잘 실행되며 로그가 찍히는 것을 볼 수 있다. 

 

 

스코프와 프록시

또다른 방법은 프록시 방식이다.

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

Provider보다 훨씬 간단하다.

@Scope 안에 추가로 proxyMode를 명시해주면 된다.

실행해보면, Provider와 똑같이 정상 작동한다.

 

 

이렇게 하면 MyLogger의 가짜 프록시 클래스(싱글톤처럼 동작)를 만들어두고, 요청이 오기 전에 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다 

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

가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다(다형성)

 

사용하기 편리해 보이지만, 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용해야 한다.

무분별하게 사용하면 유지보수하기 어려워진다.

 

이 포스팅은 인프런 김영한님의 '스프링 핵심 원리 기본편'을 듣고 정리한 것입니다.
강의 링크: https://url.kr/udopsk  

 

반응형

댓글