월루를 꿈꾸는 대학생

[핵심원리 - 기본] 빈스코프 본문

Programing/Spring Boot

[핵심원리 - 기본] 빈스코프

하즈시 2023. 7. 25. 22:54
728x90

보통 디폴트로 사용한 거는 싱글톤 스코프
스프링 컨테이너 시작과 종료까지 함께한 친구

스코프 : 빈이 존재할 수 있는 범위

  1. 싱글톤 : 디폴트 , 시작과 종료를 함께하는 가장 넓은 범위
  2. 프로토 타입 : 빈 생성과 의존관계 주입까지만 하는 가장 짧은 범위
  3. 웹 관련
     1. request : 웹 요청이 들어오고 나갈때 까지 유지 
     2. session : 웹 세션이 생성되고 종료될 때 까지 유지
     3. application : 웹 서블릿 컨텍스트 같은 범위로 유지 

프로토타입 스코프

  • 프로토타입 스코프는 싱글톤과 다르게 조회할 때마다 새로운 인스턴스를 만들어서 반환해줌

  • 클라이언트 요청 시점에 새로 프로토타입 빈 생성하고 의존관계 주입한다 그 후 클라이언트에 객체 주고 컨테이너에서는 지워버림 관리 x

핵심 : 컨테이너는 프로토타입 빈을 생성하고 , 의존관계 주입, 초기화까지만 처리한다!
프로토타입 빈 관리는 컨터이너가 하는게 아니라 클라이언트가 함 그래서 @Predestroy 같은 메서드 안 함


public class PrototypeTest {  

    @Test  
    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("prototypeBean2 = " + prototypeBean2);  
        System.out.println("prototypeBean1 = " + prototypeBean1);  
        Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);  
        ac.close();  
    }  
    // 여기에 @Componet 없어도 new AnnotationConfigApplicationContext(PrototypeBean.class) 이부분 때문에 자동 등록 되버림  
    @Scope("prototype")  
    static class PrototypeBean{  
        @PostConstruct  
        public void init(){  
            System.out.println("prototype init");  
        }  
        @PreDestroy  
        public void destroy(){  
            System.out.println("prototype destory");  
        }  
    }  
}

프로토타입은 빈을 조회할때 생성되고 초기화 메서드도 그 때 실행됨
즉 조회할 때마다 다 다른 인스턴스임
컨테이너는 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여함
종료 메서드도 알아서 호출 안 됨 : 관리 안하니까 클라이언트가 직접해야함
또 컨테이너가 관리 안하다보니까 책임은 해당 객체를 받은 클라이언트에게 있음


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

싱글톤 코드 안에 프로토타입 빈을 같이 넣으면 싱글톤처럼 움직이고
프로토타입 처럼 움직이지 않는다 .

프로토타입의 움직임

  • 클라이언트a가 프로토타입 빈을 요청하고 count를 증가시키면 1이됨

  • 클라이언트 b가 요청할 때도 새로 만들기 때문에 count는 2가 아니라 1이 된다
싱글톤 빈에서 프로토타입 빈을 사용

  • clientBean은 싱글톤 , 컨테이너에서 생성 및 의존관계 주입을 받음
  • 의존 관계 주입을 받을 때 변수 PrototypeBean이 있는데 이 때 컨테이너에서 생성받은 객체를 참조해서 내부에 보관

클라이언트a가 싱글톤 빈에 메서드 addcount()로 프로토타입 빈 객체의 변수 조작 가능


해당 객체는 싱글톤이니까 클라이언트B가 clientBean 요청해도 같은 객체를 참조하고 이미 프로토타입은 컨테이너에서 관리하지 않고 주입이 끝난 빈이라 이미 생성된 걸 사용
이때 프로토타입의 변수값은 이미 1인 상태 여기서 b가 2로 변경할 수가 있음
즉 프로토타입이지만 싱글톤타입처럼 조작을 하게 됨
프로토타입이 아무런 의미가 없어짐


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

스프링 컨테이너에 요청
  • 싱글톤빈이 프로토타입 사용할 때마다 요청하기
    @Autowired
    private ApplicationContext ac;
    

public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}

- ac.getBean() 메서드로 늘 새로운 프로토타입 빈이 생성 
- **DL(Dependency Lookup) 의존관계 조회(탐색)** : 직접 필요한 의존관계를 찾음 
- ApplicationContext을 주입하면 스프링 컨테이너에 종속적인 코드가 되고 테스트도 어려워짐
> 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 게 필요!!


##### ObjectFactory , ObjectProvider 
- 지정한 bean을 컨테이너에서 대신 찾아주는 DL서비스 **ObjectProvider**
- ObjectFactory가 상위 클래스 , ObjectProvder가 자식 클래스 

```java
        @Autowired  
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;  


        public int logic(){  
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();  
            prototypeBean.addCount();  
            int count = prototypeBean.getCount();  
            System.out.println("count = " + count);  
            return count;  
        }
  • prototypeBeanProvider.getObject()를 통해서 새로운 빈 타입 생성

  • 스프링 컨테이너에 조회를 대신 해주는 친구 !!!

    DL : 스프링 컨테이너에 getObject()메서드로 호출하면 스프링 컨테이너에서 해당 빈 찾아서 반환

  • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존

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

이젠 스프링에 의존하지 않는 기술이 나옴

JSR-330 Provider

  • javax : 자바 표준
  • 따로 gradle 추가 필요
@Autowired  
private Provider<PrototypeBean> prototypeBeanProvider;  

public int logic(){  
    PrototypeBean prototypeBean = prototypeBeanProvider.get();  
    prototypeBean.addCount();  
    int count = prototypeBean.getCount();  
    System.out.println("count = " + count);  
    return count;  
}
  • provider.get()을 통해서 항상 새로운 프로토타입 빈을 생성하는 거 확인
  • provider.get() 호출 시 스프링 컨테이너에서 해당 빈을 찾아서 반환 DL
  • 자바 표준 , 단위 테스트 , mock코드 만들기 쉬움

get() 메서드 하나 단순
별도 라이브러리 필요
자바 표준 스프링 의존X

하아.. 결국 포로토타입은 잘 안 쓰는 거임
대부분 싱글톤으로 해결 가능함..
DL필요한 경우 ObjectProvider , JSR330 Provider 사용 가능

정리!!!

일단 싱글톤을 쓰자
간혹 싱글톤이랑 프로토타입 같이 쓰려고 하면 ObjectProvider , JSR330 Provider 을 사용해서 DL을 확보하자!!


웹 스코프

특징

  • 웹에서만 동작
  • 스프링이 해당 스코프 종료 시점까지 관리 = 종료 메서드 호출

종류

  1. request : http요청 하나 (= 각각 따로) 가 들어오고 나갈 때 까지 유지 되는 스코프 / 각 http 요청마다 별도 빈 인스턴스가 생성 , 관리
  2. session : http session 과 동일한 생명주기를 가지는 스코프
  3. application : 서블릿 컨텍스트 ('ServletContext')와 동일한 생명주기를 가지는 스코프
  4. websocet : 웹 소켓과 동일한 생명주기를 가지는 스코프

  • 동시에 요청해도 컨트롤러에서 각각 따로 인스턴스를 할당해서 처리함

request 스코프 예제 만들기

동시에 여러 http 요청이 오면 로그가 섞임
그랠 때 사용하는 것이 request 스코프

uuid를 남겨서 http요청 구분

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

}
  • 로그를 출력하기 위한 코드 작성
  • @Scope(value="request") 를 만들어서 스코프 지정 / 빈은 http요청 하나당 생성되고 끝나는 시점에 소멸
  • @PostConstruct 초기화 메서드 사용해서 uuid 생성후 저장
@Controller  
@RequiredArgsConstructor  
public class LogDemoCOntroller {  
    private final LogDemoService logDemoService;  
    private final MyLogger myLogger;  

    // 해당 생성자가 @RequiredArgsConstructor이랑 동일함  
//    @Autowired  
//    public LogDemoCOntroller(LogDemoService logDemoService, MyLogger myLogger) {  
//        this.logDemoService = logDemoService;  
//        this.myLogger = myLogger;  
//    }  


    @RequestMapping("log-demo")  
    @ResponseBody  
    public String logDemo(HttpServletRequest request){  
        String requestURL = request.getRequestURI().toString();  
        myLogger.setRequestURL(requestURL);  
        myLogger.log("controller test");  
        logDemoService.logic("testId");  
        return "OK";  
    }  

}

근데 이거 만들어서 실행하면 에러가 뜸

에러 내용 보면 MyLogger를 주입하지 못해서 나는 에러

MyLogger의 스코프는 request
request의 생존 범위는 각 http request가 들어오고 날갈 때 까지 즉 실행시점에선 존재하지 않음
그래서 에러가 났다

서비스나 컨트롤러 코드를 보면 웹과 관련된 정보가 거의 없는데 이는 MyLogger클래스가 대부분 해당 역할을 가지고 있기 때문이다 어지간하면 웹 관련 정보는 비즈니스 로직까지 가지 않는편이 좋다


Scope와 Provider

ObjectProvider를 사용해보기

    private final ObjectProvider<MyLogger> myLoggerProvider; // MyLogger를 주입하는 게 아니라 찾아서 주입해주는 DL친구를 넣어줌  
    // 이거는 주입 시점에서 알아서 주입시켜주니까 에러가 안 나는 건가?  

    @RequestMapping("log-demo")  
    @ResponseBody  
    public String logDemo(HttpServletRequest request){  
        MyLogger myLogger = myLoggerProvider.getObject();  // 요렇게 넣어주기 
        String requestURL = request.getRequestURI().toString();  
        myLogger.setRequestURL(requestURL);  
        myLogger.log("controller test");  
        logDemoService.logic("testId");  
        return "OK";  
    }

에러 해결의 해석

  1. 스프링 애플리케이션이 시작하면서 싱글톤 빈들에 대한 생성과 의존성 주입이 발생한다.
  2. LogDemoController와 LogDemoService는 싱글톤 빈들이기 때문에 스프링 애플리케이션이 시작할 때 의존성을 주입받아야 한다.
  3. MyLogger는 Request Scope 빈이기 때문에 스프링 애플리케이션 시작 시에는 빈 객체가 없다.
  4. LogDemoController와 LogDemoService는 없는 빈을 받으려고 한 것이기 때문에 예외가 발생한다.
  5. ObjectProvider를 이용하면 실제 objectProvider.getObject()를 할 때까지 해당 빈 객체가 필요없다. 실제 getObject()하는 시점에 해당 빈 객체가 필요하다.
  6. LogDemoService나 LogDemoController에서 myLoggerProvider.getObject()를 포함하는 메서드를 실행할 때, 정확히는 메서드 안에서 myLoggerProvider.getObject()를 실행할 때 해당 빈 객체를 DL한다.
  7. 이 때는 HTTP 요청이 온 상태이기 때문에 MyLogger 빈 생성이 가능하고 해당 빈을 정상적으로 불러올 수 있기 때문에 프로세스가 정상적으로 처리된다.

[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

다음과 같이 하나의 요청에 대해서 미리 만들어지고 출력되고 종료되는 거 확인 가능
uuid를 통해 여러 요청에도 구분이 가능하다

정리

  • 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 {
    }
  • ScopedProxyMode.TARGET_CLASS : 적용 대상이 클래스일 때
  • ScopedProxyMode.INTERFACES : 적용 대상이 인터페이스일 때
  • 해당 프록시 모드를 사용하면 가짝 프록시 클래스를 만들어서 리퀘스트에 상관없이 가짜 클래스를 빈에 주입을 해서 에러가 안나도록 함
  • 이 가짜 프록시 보면 지난번 싱글톤처럼 또 CGLIB 클래스임

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

@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)로 설정시 CGLIB바이트 코드 조작 라이브러리르 사용해서 MyLogger를 상속받은 가짜 프록시 객체를 만든 다음 이걸 스프링 실행할 때 일단 대충 다른 빈에 주입을 시켜둠 그 다음 진짜 실행할 때 찐퉁 MyLogger를 조작해서 코드를 실행
일단 실행할 때는 가짜라도 컨테이너에 주입해두었으니까 에러가 안 난거다 !

  • 사실 클라이언트가 호출한 myLogger.logic()은 프록시 모드에 가짜 프록시 객체의 메서드를 호출한 것
  • 해당 가짜 클래스는 찐퉁을 아니까 클라이언트에서 요청 받으면 상속받은 찐퉁 코드를 실행시킴
  • 가짜는 찐퉁을 상속받았으니까 동일한 동작을 보장하고 클라이언트는 가짜이든 진짜든 동작은 하니까 동일하게 사용 가능

동작 정리

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

특징 정리

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

정리
웹 스코프인 경우에는 실행시 에러가 나니까 여러가지 방법을 사용해서 에러를 안나게 하는데 그 방법 중에는 Provider도 있고 Proxy도 있음

끄읕!

출처
https://inf.run/wFfL

728x90