Spring

[Spring] 스프링 핵심 원리 - 기본편 섹션 9 빈 스코프

고쩡이 2024. 4. 5. 18:53

본 내용은 인프런 김영한T 스프링 핵심원리 - 기본편 강의를 정리한 내용입니다:)

 

[Spring] 스프링 핵심 원리 - 기본편 섹션 9 빈 스코프

◼️ 빈 스코프란?

빈은 기본적으로 싱글톤 스코프

🟢 스코프

  • 싱글톤: 기본 스코프, 스프링 컨테이너 시작 ~ 종료까지 유지 (가장 넓은 범위)
  • 프로토타입: 프로토타입 빈 생성 ~ 의존관계주입까지만 관여

🟢 웹 관련 스코프

  • request: 웹 요청이 들어오고 나갈때 까지 유지
  • session: 웹 세션이 생성되고 종료될 때 까지 유지
  • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지

컴포넌트스캔 자동등록

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

수동등록

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

◼️ 프로토타입 스코프

싱글톤 빈과 프로토타입 빈 요청 차이

  • 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리
  • 종료 메서드 호출 X

 

🔘 싱글톤 스코프 빈 테스트해보면, 빈 초기화 메서드 실행->같은 인스턴스 빈 조회-> 종료 메서드 호출되는 것을 알 수 있다.

public class SingletonTest {
    @Test
    public void singletonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);
        assertThat(singletonBean1).isSameAs(singletonBean2);

        ac.close();
    }

    @Scope("singleton")
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("Singleton.init");
        }
        @PreDestroy
        public void destory() {
            System.out.println("SingletonBean.destroy");
        }
    }
}
Singleton.init
singletonBean1 = com.basic.basicproject.scope.SingletonTest$SingletonBean@7905a0b8
singletonBean2 = com.basic.basicproject.scope.SingletonTest$SingletonBean@7905a0b8
SingletonBean.destroy

🔘 프로토타입 스코프 빈 테스트

public class PrototypeTest {
    @Test
    public void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean PrototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean2");
        PrototypeBean PrototypeBean2 = ac.getBean(PrototypeBean.class);
        System.out.println("PrototypeBean1 = " + PrototypeBean1);
        System.out.println("PrototypeBean2 = " + PrototypeBean2);
        assertThat(PrototypeBean1).isNotSameAs(PrototypeBean2);
        ac.close();
    }
    @Scope("prototype")
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }
        @PreDestroy
        public void destory() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
PrototypeBean1 = com.basic.basicproject.scope.PrototypeTest$PrototypeBean@35a3d49f
PrototypeBean2 = com.basic.basicproject.scope.PrototypeTest$PrototypeBean@389b0789

프로토타입은 위 출력을 보면 스프링 컨테이너에서 빈을 조회할 때 생성되고 조회때마다 다른 스프링빈이 생성되었다. 또한 종료메서드가 전혀실행되지않았다.

 

▪️싱글톤 빈: 스프링 컨테이너 생성 시점에 초기화 메서드가 실행

▪️ 프로토타입 스코프 빈: 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행

 

🟢 프로토타입 빈 특징

  • 요청때마다 새로 생성
  • 컨테이너는 빈 생성,의존관계 주입, 초기화까지만 관여
  • 종료 메서드 호출 X

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

싱글톤에서 프로토타입 빈을 사용할 때 :)

아래 코드처럼 싱글톤에서 프로토타입 빈을 사용할때, clientBean이 내부에 가지고 있는 프로토타입 빈은 생성자 주입으로, 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다!

public class SingletonWithPrototypeTest1 {
     @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); // 빈 새로 생성 X
         int count2 = clientBean2.logic();
         assertThat(count2).isEqualTo(2);
		 }
         ....

    static class ClientBean {
        private final PrototypeBean prototypeBean; // 생성자 주입 된다.

        @Autowired
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }
    }

 

🪄 여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다. 예를 들어서 clientA, clientB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.

 

🟢 정리!

  • 싱글톤 스코프의 빈을 조회 → 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환
  • 프로토타입 스코프를 스프링 컨테이너에 조회  →  스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환

💨 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 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

  • 의존관계 조회(Dependency LookUp,DL) : 의존관계 외부 주입(DI) X, 필요한 의존관계를 찾는 것
  • ObjectProvider : 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공 / ObjectFactory 상속한 것
  • 스프링에 의존
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
     PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); // 항상 새로운 프로토타입 빈 생성
     prototypeBean.addCount();
     int count = prototypeBean.getCount();
     return count;
}

🟢 JSR-330 Provider

  • javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법
  • jakarta.inject:jakarta.inject-api:2.0.1 라이브러리를 gradle에 추가해야 한다.
  • 별도 라이브러리 필요, 스프링 비의존
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
     PrototypeBean prototypeBean = provider.get(); // 항상 새로운 프로토타입 빈 반환
     prototypeBean.addCount();
     int count = prototypeBean.getCount();
     return count;
}
🪄 자바표준을 사용할지, 스프링이 제공하는 것을 사용할지?
→ 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다

 

◼️ 웹 스코프

  • 웹 환경에서만 동작
  • 스프링이 스코프 종료시점까지 관리. 종료 메서드 호출됨

🟢 웹 스코프 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

HTTP request 요청 당 각각 할당되는 request 스코프

 

아래 웹 라이브러리를 추가한다. 그러면 스프링 부트는 내장 톰켓 서버를 활용해 웹서버+스프링을 함께 실행시킨다.

implementation 'org.springframework.boot:spring-boot-starter-web'
🪄스프링 부트는 웹 라이브러리 없으면, AnnotationConfigApplicationContext 을 기반으로 애플리케이션을 구동.
웹 라이브러리가 추가되면, 웹과 관련된 추가 설정, 환경 필요하므로 AnnotationConfigServletWebServerApplicationContext 를 기반으로 구동.

⬜ request 스코프 예제 개발

각 HTTP 요청이 오면 어떤 요청이 남긴 로그인지 HTTP마다 UUID, requestURL 정보가 표시되도록 추가 기능을 개발해보자.

 

MyLogger 클래스

@Component
@Scope(value = "request") // request 스코프로 지정. HTTP 요청 당 하나씩 생성
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);
    }
}
참고: requestURL을 MyLogger에 저장하는 부분은 컨트롤러 보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다. 여기서는 예제를 단순화하고, 아직 스프링 인터셉터를 학습하지 않은 분들을 위해서 컨트롤러를 사용했다. 스프링 웹에 익숙하다면 인터셉터를 사용해서 구현해보자.

LogDemoController 클래스

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

LogDemoService 클래스

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;

    public void logic(String id){
        myLogger.log("service id = " + id);
    }
}
Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;

request 스코프 빈이 아직 생성되지않아 발생하는 오류이다. 이 빈은 실제 고객의 요청이 와야 생성할 수 있다!

⬜ 스코프와 provider

ObjectProvider를 사용해보자.

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    //private final MyLogger myLogger;
    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";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    //private final MyLogger myLogger;
    private final ObjectProvider<MyLogger> myLoggerProvider;

    public void logic(String id){
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}
[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
  • ObjectProvider 로 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연했다.
  • ObjectProvider.getObject() 를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.
  • ObjectProvider.getObject() 를 LogDemoController , LogDemoService 에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다.

⬜ 스코프와 프록시

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
  • 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS 를 선택
  • 적용 대상이 인터페이스면 INTERFACES 를 선택
  • MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

위와같이 설정 후 다시 코드를 원래대로 되돌려놓고 실행하면 잘 동작한다.

 

🟢 동작 원리

myLogger 로그를 찍어보면 아래와 같다. CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d

  • 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
  • 클라이언트가 myLogger.logic() 을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다. 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic() 를 호출한다.
  • 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하도록 한다. (유지보수 관점)
  • Provider와 프록시의 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 것이다.