포스트

15. 빈 스코프

15. 빈 스코프

빈 스코프

빈 스코프란?

지금까지 스프링 컨테이너 라이프 사이클 > 빈 라이프 사이클 포함 관계였다.

하지만 스프링은 다양한 스코프 또한 지원한다. (스코프란 생명 수명이다.)

스프링이 지원하는 빈 스코프

  • 싱글톤 : 기본 스코프, 스프링 컨테이너와 함께한다.
  • 프로토 타입 : 스프링 컨테이너가 생성과 의존관계 주입까지만 관여한다. 추후에는 관리하지 않는다.

웹 관련 스코프

  • request : 하나의 요청에 관해서 들어오고 나올떄까지의 스코프
  • session : 웹 세션이 생성되고 종료 될 때까지의 스코프
  • application : 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프

프로토타입 스코프

  • 기본 스코프가 아니다.
  • 즉 싱글톤이 아니다
    • 즉 스프링 컨테이너는 요청 할떄마다 새로운 Bean 인스턴스를 생성해서 반환한다.

      스크린샷 2024-05-01 오후 6.31.45.png

  • 즉 객체만 생성하고 전혀 관리하지 않는다.
  • 핵심 : 즉 생성에만 관리한다. → 즉 추후 빈 관리는 클라이언트에게 있다.
    • @PreDestroy 같은 메서드가 실행되지 않는다.
    • 종료처리를 스프링이 해주지 않고 클라이언트가 직접 해야한다.
  • 즉 각 bean을 두번 실행하면 다른 bean 두개가 실행된다.

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

  • 스프링에서는 프로토타입시에 항상 새로운 객체를 인스턴스로 생성해서 반환한다.
  • 만약에 싱글톤 빈과 함께 사용한다면?
    • 즉 싱글톤 빈에서 의존관계 주입으로 프로토타입 빈을 받는 것
      1. 싱글톤 빈이 필요하다면 스프링은 의존관계 주입을 한다.
      2. 프로토타입 빈도 주입을 해준다.
      3. 싱글톤빈은 프로토타입 빈을 내부 필드에 관리한다.
      1. 즉 더이상 프로토타입 빈은 스프링 내부에서 관리하지 않는다. 4. 추후 누군가가 싱글톤빈을 호출하면 같은 싱글톤빈이 호출되어진다. 5. 그러면 프로토타입 빈은 싱글톤 빈 내부에 있기에 여전히 같은 객체가 사용자에게 전달된다…

      프로토타입의 성질을 잃음

이러한 부수효과를 언제나 프로그래밍에서 조심할것…

Provider로 문제 해결

  • 의존관계를 외부에서 주입 받는게 아니라 의존관계를 찾는 것을 dl이라한다.
  • 스프링에는 dl을 해주는 것이 준비되어 있다.

ObjcectFactory, ObjectProvider

  • Provider가 자식이다. → 기능 확장
  • 본질적으로는 같다.
  • prototypeBeanProvider.getObject() 을 통해 항상 새로운 타입의 프로토타입 빈을 제공한다.
    • 즉 스프링 컨테이너가 해당 빈을 찾아서 반환한다.
  • 별도의 라이브러리는 필요 없지만 스프링에 의존한다.
1
2
3
4
5
6
  private final ObjectProvider<MyLogger> myLoggerProvider;

    public String logDemo(HttpServletRequest request) throws Exception {
        String requestURL = request.getRequestURL().toString();
				MyLogger myLogger = myLoggerProvider.getObject();
		)

javax.inject:javax.inject:1을 gradle에 추가시 DL 스프링을 사용하지 않고도 기능을 사용할 수 있다. 하지만… 대부분의 이런 기능이나 다른 라이브러리와 비슷한 기능을 제공할 경우 스프링 컨테이너에서 사용하는 기능으로 통일하는 것이 좋다.

왜 프로토타입 Bean을 사용할까?

  • 매번 필요한 빈을 생성해야할 때
  • 하지만 대부분 상황에서는 Bean을 싱글톤으로 사용할 일이 더 많다.
  • 즉… 대부분의 경우는 싱글톤을 사용하고 필요하면 new 메서드를 사용해 프로토타입을 제어해도 충분하다.

웹 스코프

  • 웹 환경에서만 동작한다.
  • 스프링이 프로토타입과 달리 종료시점까지는 관리 해준다. → 종료 메서드 호출

웹 스코프의 종류

  • request
    • HTTP 요청 하나가 들어오고 나갈 떄까지 유지되는 스코프, 각 HTTP 요청보다 별도의 bean 이 생성된다.

    Untitled

  • session
    • HTTP Session과 동일한 생명주기를 가지는 스코프
  • application
    • servletcontext 와 동일한 생명주기를 가지는 스코프
  • websocket
    • 웹 소켓과 동일한 생명 주기를 가지는 스코프

request 스코프 실습

gradle 설정

1
	implementation 'org.springframework.boot:spring-boot-starter-web'
  • 이걸 하면 스프링 부트는 내장 톰캣 서버를 통해 웹 서버를 실행한다.

로그를 사용한 예제 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
@Scope(value = "request")
public class MyLogger {

    private String uuid;
    private String requestURL;

    public MyLogger() {
    }

    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 + "] 초기화 " + this );
    }
    
    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] 종료 " + this );
    }

}

  • 로그를 출력하기 위한 myLogger 클래스
  • 이 빈은 HTTP 요청당 하나씩 생성된다.
  • 절대 중복되지 않는 uuid를 사용한다.
  • requestURL 은 이 빈이 생성되는 시점에는 알 수 없기에 setter를 사용한다.

컨트롤러

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
public class LogDemoController {

    private final LogDemoController logDemoController;
    private final MyLogger myLogger;

    public LogDemoController(LogDemoController logDemoController, MyLogger myLogger) {
        this.logDemoController = logDemoController;
        this.myLogger = myLogger;
    }

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

        myLogger.log("controllerrrr");
        return "ok";
    }

}
  • 하지만 이코드는 오류가 뜬다. 왜?
    • Spring 컨테이너가 Bean 객체를 만들려고 하는데 request 요청이 없기에 객체를 생성할 수가 없다.

Provider를 사용한 해결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
public class LogDemoController {

    private final LogDemoController logDemoController;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    public LogDemoController(LogDemoController logDemoController, ObjectProvider<MyLogger> myLoggerObjectProvider) {
        this.logDemoController = logDemoController;
        this.myLoggerObjectProvider = myLoggerObjectProvider;
    }

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

        myLogger.log("controllerrrr");
        return "ok";
    }
}

  • provider를 통해 적절한 시기에 Bean을 주입받을 수 있다!
  • .getObjcet() 를 사용하기에 service, controller 등 어떠한 곳에서도 같은 HTTP 요청임을 구분할 수 있다!

코드를 더 줄이는 방법 - Proxy

1
2
3
4
5
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
	...
}
  • proxyMode = ScopedProxyMode.TARGET_CLASS
    • 적용 대상이 인터페이스면 INTERFACES 를 선택
    • 클래스라면 위 처럼 적는다.
  • 이런 식으로 MyLogger의 가짜 프록시 클래스(CGLIB)를 만들어 놓고 HTTP request와 상관 없이 가짜 프록시 빈을 주입시켜놓는다!
    • 스프링 컨테이너가 CGLIB라는 바이트 코드 조작 라이브러리를 사용한다.
    • 의존관계에 미리 주입시켜놓기에 getBean() 에서도 프록시 객체가 조회된다.

Untitled

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

  • 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기에 클라이언트 입장에선 동일하게 사용가능 → 다형성
  • 마찬가지로 싱글톤처럼 동작한다.

특징 정리

  • 이거 덕분에 편리하게 request scope을 사용할 수 있다.
  • Provider나 프록시를 사용하면 쉽게 지연 처리를 제공한다.
  • 어노테이션 설정 변경만으로 프록시 객체를 대체 가능 → 다형성과 DI컨테이너의 큰 강점
  • 웹 스코프 뿐만 아니라 다른 곳에서 응용 가능

주의점 : 싱글톤처럼 보이지만 아니다. 즉 이런 scope는 주의해서 사용한다. 유지보수에 어려움 을 겪을 수 있다…

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.