[SpringBoot] [3] 9. 빈 스코프

윤경·2021년 8월 29일
0

Spring Boot

목록 보기
32/79
post-thumbnail

1️⃣ 빈 스코프란?

스프링 빈은 스프링 컨테이너의 시작과 함께 생성돼 스프링 컨테이너가 종료될 때까지 유지된다고 배웠다.

이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. (스코프: 빈이 존재할 수 있는 범위)

📌 스프링이 지원하는 다양한 스코프

싱글톤: 기본 스코프. 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프 (생명 주기가 가장 긴. 스프링 컨테이너와 생명 주기가 같은.)
프로토타입: 스프링 컨테이너가 프로토타입 빈의 생성(요청을 하면 딱 만들어줌)과 의존관계 주입까지만 관여하고 더는 관여하지 않는 매우 짧은 범위의 스코프 (만들어 던져주고 끝. 그래서 종료 메소드가 호출되지 않음.)

웹 관련 스코프:

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

싱글톤, 프로토타입, request 정도만 알면 됨.

그럼 등록은 어떻게 하면 될까?

컴포넌트 스캔 자동 등록

수동 등록


2️⃣ 프로토타입 스코프

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

싱글톤 빈 요청

프로토타입 빈 요청

즉,

스프링 컨테이너는 프로토타입 빈을 생성하고 의존관계 주입, 초기화까지만 처리하지 이후 빈을 관리하지 않는다.

프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에게 있다. 그래서 @PreDstroy같은 종료 메소드가 호출되지 않는다.

✔️ test/scope 패키지 생성 후 SingletonTest.java

package hello.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class SingletonTest {

    @Test
    void singletonBeanFind() {
        // 이렇게 파라미터로 (컴포넌트 클래스) SingletonBean.class를 넣어주면 아래 19줄이 알아서 컴포넌트 스캔되어 등록됨
        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);

        Assertions.assertThat(singletonBean1).isEqualTo(singletonBean2);

        ac.close();
    }

    @Scope("singleton") // 사실 싱글톤은 디폴트
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }
    }
}

✔️ 같은 패키지에 PrototypeTest.java

package hello.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.Bean;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import static org.assertj.core.api.Assertions.assertThat;

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).isEqualTo(prototypeBean2);

        ac.close();
    }

    @Scope("prototype")
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

➡️ 스프링 컨테이너가 종료될 때 빈 종료 메소드가 실행되는 싱글톤 빈과 달리 프로토타입 빈은 스프링 컨테이너가 생성, 의존관계 주입, 초기화까지밖에 관여하지 않아 @PreDestroy와 같은 종료 메소드가 실행되지 않는다.

📌 프로토타입 빈 특징

  • 스프링 컨테이너에 요청 시 새로 생성
  • 스프링 컨테이너가 프로토타입 빈 생성, 의존관계 주입, 초기화까지만 관여
  • 종료 메소드 호출 X

즉, 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메소드에 대한 호출 또한 클라이언트가 직접한다.


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

보통 싱글톤 빈을 사용하고 프로토타입 빈을 가끔 같이 쓸 때가 있는데 이때 중대한 문제가 발생할 수 있다. 이에 대해 알아보자.

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해 반환한다.
하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의하자.

스프링 컨테이너에 프로토타입 빈 직접 요청

✔️ 스프링 컨테이너에 프로토타입 빈 직접 요청1

✔️ 스프링 컨테이너에 프로토타입 빈 직접 요청2

코드로 직접 확인해봅시다.
✔️ test/scope SingletonWithPrototypeTest1.java

package hello.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }

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

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

😺: 아니, 왜 굳이 이런걸,, 확인하지 ,,

이제 나올 내용을 위해 굳~이 굳이 테스트를 해봤다.

싱글톤 빈에서 프로토타입 빈 사용

clientBean이라는 싱글톤 빈이 DI를 통해 프로토타입 빈을 주입받아 사용하는 예를 살펴보자.



✔️ SingletonWithPrototypeTest1.java에 다음 코드를 추가

    @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);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }

    @Scope("singleton")
    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;
        }
    }

➡️ 생성 시점에 주입된 빈을 계속 사용. 그래서 원하는 결과 count == 2를 얻을 수 있었던 것.

스프링은 일반적으로 싱글톤 빈을 사용한다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받아 프로토타입 빈이 새로 생성되어도 싱글톤 빈과 함께 계속 유지되는 문제가 있다.

📌 (참고): 여러 빈에서 같은 프로토타입 빈을 주입 받으면 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.

(clientA, clientB 각각 의존관계 주입 받을 시 각각 다른 인스턴스 프로토타입 빈 주입 받음.)
clientA → prototypeBean@x01
clientB → prototypeBean@x02

단, 사용할 때마다 새로 생성되는 것은 ❌

그런데,

👽 악덕 기획자: 순개씨, 그런데 이건 우리가 원하던 것이 아닙니다. 이럴거면 싱글톤을 쓰지 왜 굳이굳이 프로토타입을 사용하겠어요? ;;

😺 순진한 개발자:

불쌍한 순개씨😺를 위해 같이 문제를 해결해보자.


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

프로토타입 빈을 싱글톤 빈과 함께 사용할 때 어떻게 해야 악덕 기획자👽가 원하는대로 사용시 늘 새로운 프로토타입 빈을 생성하게 될까?

스프링 컨테이너에 요청

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

그래서 굳이굳이 실습을 하진 않았고 핵심 코드만 보자면

  • ac.getBean()을 통해 항상 새로운 프로토타입 빈이 생성된다.
  • 의존관계를 외부에서 주입(DI)받는 것이 아닌 직접 필요한 의존관계를 찾는 것Dependency Lookup(DL) 의존관계 조회(탐색) 이라고 한다.
  • 이렇게 스프링 애플리케이션 컨텍스트 전체를 주입 받으면 스프링 컨테이너에 종속적인 코드가 되고 단위테스트가 어려워진다.

단지 딱, DL 정도의 기능만 제공하는 어떤 것이 필요하다.

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것: ObjectFactory
(과거에는 ObjectFactory가 있었는데 +편의기능이 되어 ObjectProvider가 생김)

✔️ SingletonWithPrototypeTest1.java 일부

    @Scope("singleton")
    static class ClientBean {

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

➡️ 결과가 각각 1이며 다른 빈이 생성된 것을 확인

  • prototypeBeanProvider.getObject()항상 새로운 프로토타입 빈이 생성되고 있다. (대리자)
  • ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아 반환(DL)
  • 스프링 제공 기능을 사용하지만, 기능이 단순하므로 단위 테스트를 만들거나 mock 코드를 만들기 훨 쉬워짐
  • objectProvider는 우리가 원하던 어떤 것을 충족한다.(딱 !! DL 정도의 기능만 제공)

📌 특징

ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 X, 스프링에 의존
ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리 등 편의 기능이 많고 별도의 라이브러리 필요 X, 스프링에 의존적

JSR-330 Provider

: javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법

사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가하기.

✔️ build.gradle
➡️ 그리고 반드시 코끼리🐘 눌러주기 !! (적용)

✔️ SingletonWithPrototypeTest1.java 일부

    @Scope("singleton")
    static class ClientBean {

        @Autowired
        private Provider<PrototypeBean> prototypeBeanProvider;

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

📌 특징

  • get() 메소드 하나로 기능이 매우 단순 (장점도 단점도 심플)
  • 별도의 라이브러리 필요
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능

😺: 그렇다면, 프로토타입 빈은 언제 사용하지 ??

🙋🏻‍♀️ 정답은 매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용 하면 된다.

그런데, 실무에서 웹 애플리케이션을 개발하다보면 싱글톤 빈으로 대부분 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다고 할 수 있다.

ObjectProvider, JSR330 Provider 등은 프로토타입 뿐 아니라 DL이 필요한 경우에 언제든 사용 가능

(스프링이 제공하는 메소드에 @Lookup 애노테이션을 사용할 수도 있지만 이전 방법들로 충분하며 이 방법은 고려할 내용이 많아 생략)

😺: JSR-330 Provider(자바 표준)와 ObjectProvider(스프링 제공) 중 뭘 사용할지 고민 돼,,

ObjectProviderDL을 위한 편의 기능을 많이 제공, 스프링 이외 별도 의존관계 추가가 필요 없어 편리.

혹시라도 (그럴일 진짜 거~의 없다) 코드를 스프링 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider 사용

스프링을 쓰다보면 이 기능 뿐 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 많이 겹친다. 대부분 스프링이 더 다양하고, 편리한 기능을 제공하기 때문에 특별히 다른 컨테이너를 사용할 일이 없다면 스프링 제공 기능 사용


5️⃣ 웹 스코프

📌 특징

  • 웹 환경에서만 동작
  • 프로토타입과는 다르게 스프링이 해당 스코프 종료시점까지 관리. 따라서 종료 메소드 호출.

📌 종류

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

우리는 request 스코프 예제만 살펴볼 것 !!


6️⃣ request 스코프 예제 만들기

웹 환경 추가

우선, 웹 스코프는 웹 환경에서만 동작하므로 웹 환경을 추가하자 ➡️ 라이브러리 추가

✔️ build.gradle 코드 추가 후 잊지말자 코끼리 🐘!!

CoreApplication을 실행시켜보면 톰캣이 생김. (원랜 없었음)

📌 (참고): spring-boot-starter-web 라이브러리를 추가하면 스프링 부트는 내장 톰켓 서버를 활용해 웹 서버와 스프링을 함께 실행

📌 (참고): 스프링부트는 웹 라이브러리가 없다면 AnnotationConfigApplicationContext를 기반으로 애플리케이션을 구동.
웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServerApplicationContext를 기반으로 애플리케이션을 구동.

잠깐, 8080 기본 포트를 다른 곳에서 사용해 오류가 발생한다면 포트를 9090으로 바꾸어주자.

✔️ main/resources/application.properties

server.port=9090

request 스코프 예제 개발

😺: 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 어떻게 구분하면 좋을까?

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

  [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

➡️ 이런 로그가 남도록 request 스코프를 활용해 추가 기능을 개발해보자.

  • 기대하는 공통 포멧: [UUID][requestURL]{message}
  • UUID(오직 하나만 생성되는 ID)를 사용해 HTTP 요청을 구분

requestURL 정보를 추가로 넣어 어떤 URL을 요청해 남은 로그인지 확인해보자

✔️ hello.core에 common 패키지 생성 후 MyLogger.java

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();
        // this 이렇게 해주면 주소까지 나올 수 있도록.
        System.out.println("[" + uuid + "] request scope bean create: " + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close: " + this);
    }
}

➡️ 로그를 출력하기 위한 MyLooger 클래스

@Scope(value = "request")를 사용해 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청이 발생할 때마다(스프링 컨테이너 요청 시점) 하나씩 생성되며 HTTP 요청이 끝날 때 소멸된다.

이 빈이 생성되는 시점 자동으로 @PostConstruct초기화 메소드를 사용해 uuid를 생성해 저장됨.
HTTP 요청 당 하나씩 생성되므로 uuid를 저장해두면 다른 HTTP 요청과 구분 가능.

이 빈이 소멸되는 시점 @PreDestroy를 사용해 종료 메시지 남김.

requestURL은 빈 생성 시점엔 알 수 없어 외부 setter로 입력 받음.

✔️ web 패키지 생성 후 LogDemoController.java

package hello.core.web;

import hello.core.common.MyLogger;
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;
import java.lang.annotation.Retention;

@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.java (mac: option+enter)

package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

➡️ 로거가 잘 작동하는지 테스트용 컨트롤러

HttpServletRequest를 통해 요청 URL을 받음.
(requestURL 값: http://localhost:8080/log-demo)

이 requestURL 값을 myLogger에 저장. myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청으로 값이 섞이진 않는다.

컨트롤러에서 controller test라는 로그가 남는다.

📌 (참고): 사실 requestURL을 MyLogger에 저장하는 부분은 컨트롤러보단 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다.

비즈니스 로직이 있는 서비스 계층에서도 로그를 출력해보자.

그런데 이때 ⭐️ 주의하자 !! request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘길 수 있다. 그런데 이 방법은 파라미터가 많아 지저분해진다.
또한, requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어간다.
웹 관련 부분은 컨트롤러까지만 사용해야한다.
서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.

(장점): request scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고도 MyLogger 멤버변수에 저장해 코드와 계층을 깔끔하게 유지할 수 있다.

그 런 데 실행시키면 ...

당연한 결과이다. 스프링 컨테이너가 이제 의존관계를 주입하는데 어라 얘는 request 스코프였지 않나. 이놈은 고객 요청이 들어와야 request 스코프 빈이 생성된다 !!
(스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해 주입이 가능)


7️⃣ 스코프와 Provider

😺: 이걸 어떻게 해결한담,,

우선, ObjectProvider를 사용해보자.

✔️ LogDemoController.java

package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Retention;

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

✔️ LogDemoService.java

package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

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

➡️ localhost:8080/log-demo
⬇️ 그리고 이렇게 두번 새로고침을 하면 다른 uuid를 확인해볼 수 있다.

ObjectProvider덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연시킬 수 있다. ( 정확하게는 스프링 컨테이너에게 요청하는 것을 지연 )
ObjectProvider.getObject() 호출 시점엔 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리.
ObjectProvider.getObject()LogDemoController, LogDemoService에서 각각 한 번씩 따로 호출한다고 해도 같은 HTTP 요청이면 같은 스프링 빈이 반환.

👽👽👽(개발자 인간들): .. provider도 귀찮다,, 더 줄이고싶다,,
😺:


8️⃣ 스코프와 프록시

이상한 개발자들을 위해 프록시 방식을 사용해보자 ..

✔️ MyLogger.java 아래와 같이 수정

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

➡️ proxyMode = ScopedProxyMode.TARGET_CLASS
(적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS 선택
적용 대상이 인터페이스면 INTERFACES 선택)

MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

MyLogger.java 코드를 바꾸어 놓았으면 이제 LogDemoController.java랑 LogDemoService.java 코드도 원래대로 돌려놓자.

✔️ LogDemoService.java

package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

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

✔️ LogDemoController.java

package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Retention;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
//    private final MyLogger myLogger;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    // 파라미터로 고객 요청 정보를 받음
    public String logDemo(HttpServletRequest request) throws InterruptedException {
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

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

실행 결과는 ??

provider를 사용한 것과 같이 자알~ 동작한다.
LogDemoController , LogDemoServices는 Provider 사용 전과 완전 동일.

이게 어떻게 된 일이지 ??

웹 스코프와 프록시 동작 원리

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

➡️ 출력결과

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

@Scope(value = "", proxyMode = ScopedProxyMode.TARGET_CLASS)
이렇게 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해 MyLogger를 상속받은 가짜 프록시 객체를 생성한다.

이는 결과를 보면 알 수 있다. 우리가 등록한 순수 MyLogger 클래스가 아닌 ...CGLIB 이렇게 만들어진 객체가 대신 등록되어 있다.

ac.getBean("myLogger", MyLogger.class)로 조회해도 프록시 객체가 조회된다.
그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.

➡️ 가짜 프록시 객체는 원본 클래스를 상속받아 만들어져, 이 객체를 사용하는 클라이언트 입장에선 사실 원본이든 뭐든 모르고 그냥 동일하게 사용 (다형성)

즉,

📌 동작

  • CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어 주입.
  • 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈 요청하는 위임 로직을 가짐.
  • 가짜 프록시 객체는 request scope와 관계 없는 그냥 가짜일 뿐이며 내부 단순 위임 로직만 존재하며 싱글톤처럼 동작.

📌 특징

  • 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈 사용하듯 편하게 request scope 사용 가능.
  • 사실 provider, 프록시.. 뭘 사용하든 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 것.
  • 단지 애노테이션 변경만으로 원본 객체를 프록시 객체로 대체 가능
    ➡️ 다형과 DI 컨테이너가 가진 가장 큰 강점
  • 꼭 웹 스코프가 아니어도 프록시 사용 가능.

📌 주의

  • 마치 싱글톤 사용하듯 사용하지만 결국 다르게 동작 → 이 점 주의해 사용
  • 이런 특별한 scope는 꼭 필요한 곳에서만 !! 최소화하여 !! 사용하자. 무분별하게 사용하면 유지보수가 힘들다.

profile
개발 바보 이사 중

0개의 댓글