빈 스코프

고동현·2024년 4월 10일
0

Spring 기본

목록 보기
9/10

빈스코프란?

지금까지는 스프링 빈(인스턴스)이 스프링 컨테이너 시작과 함께 생성되어서 스프링 컨테이너가 종료 될때 까지 유지하고, 스프링 컨테이너가 이 빈을 관리한다고 했다.

만약에 근데, 스프링 컨테이너가 싱글톤으로 빈을 관리하는게 아니라, 그냥 계속 생성해서 반환하고 싶다면 어떻게 해야할까?

스프링은 다음과 같이 다양한 스코프를 지원한다.

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

프로토타입 스코프

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

  • 싱글톤 빈 요청

    같은 객체 인스턴스를 반환한다.

  • 프로토타입 빈 요청

    프로토 타입 빈을 생성하고, 필요한 의존관계를 주입한다. 초기화 메서드까지 호출한다.

    반환하면, 새롭게 생성한 빈을 클라이언트에 반환한다.
    이후에 스프링 컨테이너에 같은 요청이 오면, 항상 새로운 프로토 타입 빈을 생성해서 반환한다.

핵심: 스프링 컨테이너는 프로토 타입 빈을 생성하고, 의존관계주입, 초기화까지만 처리한다는 것이다.
고로, 프로토타입 빈을 관리할 책임은 클라이언트에게 있다.
->그러므로 초기화에 해당하는 PostConstruct까지는 되도, PreDestroy는 안된다는 것이다.

싱글톤 스코프 빈 Test

원래 Scope에 "singleton"을 적지 않아도 된다. default가 싱글톤이라, 그러면 뭐 지금까지 이전에 했던것처럼 의존관계 주의 초기화, destroy까지 스프링 컨테이너가 관리하여 잘 해준다.
같은 인스턴스의 빈을 조회하는것을 볼 수 있다.

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

일단 스코프가 prototype이다.

실행결과를 보면

아 맞다. 그리고 저기 @Component가 PrototypeBean class에 안달려있는데 어떻게 빈으로 등록되는지 궁금하다면, 그 AnnotationConfigApplicationContext의 파라미터로 들어오는 자바 클래스는 따로, 컴포넌트 어노테이셔 안붙여도 빈으로 등록이 된다.

그리고 결과를 분석해보면 ac.getBean을 해줄 때 마다. 계쏙 새로운 인스턴스를 새서해서 반환해주는것을 볼 수 있다. 또한 ac.close()를 해도, prototypeBean1,2의 destroy 메서드가 호출이 안되는것을 볼 수 있다.
왜? 결국 스프링 컨테이너가 관리하는 인스턴스가 아니기 때문, 반환하면 끝, 자기 책임 아님

프로토 타입 빈의 특징정리

  1. 스프링 컨테이너에 요청할 때 마다 새로 생성된다.
  2. 스프링 컨테이너는 프로토 타입 빈의 생성과 의존관계 주입 그리고 초기화 까지만 관여한다.
  3. 종료 메서드가 호출되지 않는다.
  4. 그래서 프로토 타입 빈은 프로토 타입 빈을 조회한 클라이언트가 관리해야한다.
    종료 메서드에 대한 호출도 클라이언트가 직접 해야하낟.

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

싱글톤 빈과 프로토 타입 스코프를 같이 사용할때, 문제점이 뭘까?
뭐 문제라고 하면, 큰 문제가 프로토타입은 요청될때 마다 생성해서 반환해야하는데, 계속 똑같은걸 쓴다면, 뭐 이게 문제 아닐까?

그림을 봐보자.

내가 원하는 구성은 프로토타입 빈을 생서해서 반환받고 addCount를 호출할때마다 각각 다른 PrototypeBean인스턴스의 count값이 1씩 증가하는것이다.

그러나, 싱글톤 빈과 같이 사용한다면,

싱글톤 빈으로 관리되는 clientBean에다가 PrototypeBean을 주입받아서 사용한다고 생각해보자.
그러면 당연히 스프링 컨테이너에 올라갈때 prototypeBean을 요청하게 되고, 그러면 생성해서 주입해줄것이다.
그다음에 addCount를 하면 0에서 1로 증가할 것이다.

그런데 문제가 뭐냐면, 클라이언트 B도 clientBean 빈을 같이 공유해서 사용하는 입장이고, 여기서 새로운 PrototypeBean이 생성되어서 addCount를 하기를 원했지만,
결국 prototypeBean을 맨처음에 ClientBean이 빈으로 등록될때, 동시에 의존관계 주입을 했기 때문에, 더이상 새로운 PrototypeBean이 생성되지는 않는다.

즉, clientBean이 내부에 갖고 있는 프로토 타입 빈은 이미 과거에 주입이 끝난 빈이다.
그러므로, 주입 시점에 스프링 컨테이너에서 요청해서 프로토 타입 빈이 새로 생성된것이지 사용할때마다 새로 생성되는것이 아니다.

TestCode



여기서도 Component없이 파라미터에 넣으면 빈으로 등록되는것을 볼 수 있따.
ac.getBean으로 Client 인스턴스를 가져와서 logic()으로 count값을 증가시키는데,

우리가 원하는건 결국 Client가 싱글톤으로 관리 되더라도, Client에 있는 내부 필드에 해당하는 프로토 타입 빈은 새로 생성되서, count가 따로 각각 증가하길 원하지만,
결국 DI를 통해서 한번 주입이 되서 사용할때마다 새로 생성 되지 않는다.

만약에, 여러빈에서 같은 프로토 타입 빈을 주입받으면, 주입받는 시점에 각각 새로운 프로토 타입 빈이 생성된다.
이건 너무 당연하게, 만약 ClientA와 ClientB가 각각 만들어져서 의존관계를 주입받으면, prototype이므로 각각 다른 prototype을 주입받게된다. 물론 사용할때마다 새로 생성되지는 않는다.

ClientA=>prototypeBean@x01
ClientB=>prototypeBean@x02

Provider로 문제해결

결국 싱글톤 빈과 프로토타입 빈을 함께 사용할때는, Provider를 사용하여서 해결하면 된다.

앞에서 문제점들을 보면 프로토 타입 빈을 DI하기 때문에 문제가 생겼다.
이제는 DI를 하지 않고 사용한다.

  • ObjectFactory


보면 PrototypeBean을 ObjectFactory로 받고있다. 이름만 봐도 factory.. 먼가 만들어 줄것 같지 않은가?

기존의 prototypeBean은 바꾸지 않는다.

TestCode

일단 컴포넌트 애노테이션 안달아도 어노테이션콘피그어플리케이션 컨텍스트 메서드 파라미터에 넣으면 빈등록 자동으로 되고,
거기서 getBean을 해주고, logic()메서드를 부른다.

logic메서드를 봐보자.
logic메서드에서, Factory에서 getBean을 한다.
그러면 getBean을 할때마다. PrototypeBean이 생성되서 반환된다.

  • JSR-330 Provider
    JSR-330 자바 표준을 사용하는것이다.
    이방법을 사용하려면 gradle에 javax.inject:javax.inject:1 라이브러리를 추가해야한다.

방식은 동일하다.


Provider로 바꾸고, get()으로 변경하면된다.

provider.get으로 항상 새로운 프로토타입 빈이 생성된다.
의존관계 주입후, 초기화 메서드 호출후 클라이언트에게 반환후, 스프링 컨테이너에서는 전혀 관리하지 않는다.
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

정리
이글을 여기까지 읽으면서, 근데 이거 왜쓰지? 라는 생각이 들어야만한다.
드는 사람은 이어서 읽고, 아닌사람은 다시한번 글을 읽어보자.

결국 내가 원하는건 스프링 빈 컨테이너에 등록은 하되, 이걸 가져다가 매번 사용할때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 된다.

근데 생각해보면, 굳이 이렇게 프로토타입 빈을 사용하지 않고, 그냥 new객체로 만들어서 쓰면 안되나? 그러니까 필요하면 new해서 쓰면 되는거 아니냐 이말이다.

맞다... 그래서 이 Provider를 실제로 많이 사용하지 않는다고 한다. 왜? 그냥 new해서 새롭게 만들면되지 굳이 Provider를 쓸 이유가 없기 때문이다.

참고로, 그럼 JSR-330과 ObjectProvider,ObjectFactory 둘중 뭘 써야하냐?
ObjectProvider->편의 기능이 많음, 스프링 외에 별도의 의존관계 추가 없어서 편리
JSR-330 -> 혹시 진짜 만약에 스프링이 아닌 다른 컨테이너에서 사용한다면, JSR-330사용 왜냐? 이건 자바 표준기술이므로 스프링에 의존적이지 않음, 그러나 ObjectProvider은 애초에 패키지 부터 보면

스프링 프레임워크에 의존적임 즉 다른 스프링이 아닌 컨테이너에서는 사용 불가.

웹스코프

지금까지 배운 스코프는

  • 싱글톤 스코프: 스프링 컨테이너의 시작과 끝까지 함께하는 매우 긴 스코프
  • 프로토타입 스코프: 생성과 의존관계 주입, 그리고 초기화 까지 진행하는 특별한 스코프

이제는 웹스코프에 대해서 배워 볼 것이다.
웹스코프는 웹 환경에서만 동작하고, 스프링이 해당 스코프의 종료 시점까지 관리한다->종료메서드가 호출된다.

웹스코프의 종류

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

음.. 죄송하지만, request와 session까지는 들어보고 사용했지만, application과 websocket은 저도 잘 모르겠습니다... 공부하고 다른 포스트로 채워 넣겠습니다.

쨋든 request스코프를 예제로 웹 스코프를 설명해보도록 하겠다.

자 만약에 ClientA와 ClientB가 있다고 치자.
그리고 각 클라이언트가 요청을 보낼때마다. userid,message 등을 로그로 찍고 싶다고 치자.

그러면 당연히 이 해당 로그 class를 컴포넌트로 빈에 등록해서 사용할 것이고, 문제는 뭐냐면, 만약 이게 빈으로 등록해서 싱글톤으로 관리한다면,

우리가 앞에서 봤던 문제들이 발생할 것이다. 분명 앞에서 addcount를 B클라이언트가 했는데 1이 아니라 2가 됬던것처럼 말이다.

고로 그러면, B클라이언트가 요청을 했는데 A클라이언트의 요청까지 보이는것이다.

그러므로, 각 Client의 HTTP요청마다 각각 다르게 할당되는 request 스코프가 필요하다.(사실 그래서 앞에서 Provider를 학습한것이다.)

Request 스코프 예제 만들기
웹 환경 추가
build.gradle에 org.springframework.boot:spring-boot-starter-web을 추가하자.

만약 로그를 찍으면
[uuid][requestURL][message] 순서로 로그가 찍히게 해보겠다.

MyLogger

우선 빈으로 등록해주고, Scope를 request로 설정한다.

우선 PostConstruct를 이용해 초기화 메서드를 불러주어, Request를 클라이언트가 요청했을때, Mylogger가 새롭게 만들어지고, init 초기화 메서드를 통해서 uuid를 할당한다.

그다음에 이제 나중에 setRequestURL로 url을 할당한다. 왜? 현재 MYLogger가 만들어질때 파라미터를 넣어줄 수 가 없으니까. 이건 자동적으로 Request를 보내면 Mylogger가 만들어지는 것이다.

MyLogger 빈은 생성되는 동시에 초기화 메서드를 통해서 uuid를 생성해서 저장해두고, 이 빈은 HTTP요청당 하나씩 생성되므로, uuid를 통해서 다른 HTTP요청과 구분하는것이다.

약간 앞에 있던 그림과 비교해서 오해할 수있는게,
CLientA가 요청을 보내면 Mylogger생성후 관리, if uuid-1
그리고 다시 요청을 보내면 Mylogger가 다시 생성 후 uuid-2라고 가정하면,
동일한 CLientA임에도 uuid가 다름, why? http request마다 Mylogger가 생성되니까.
고로, uuid를 통해서 HTTP요청을 구분되는것이지 user를 구분하기 위해 쓰는것은 아니다.

LogDemoController

URL을 set하고
log메서드를 호출하고, 서비스의 logic도 호출한다.
requestURL의 값을 mylogger에다 저장해두는데 중간에 다른 user의httpRequest가 들어와서 로그가 찍히더라도 uuid로 Http 요청당 uuid로 구분가능하다.

LogDemoService

로직메서드로 그냥 출력을 하였다.
여기서 중요한점은 파라미터가 없다는 것이다.
이게 무슨말이냐면, 만약에 굳이 mylogger이런걸 안만들고 URL을 httpRequest로 받아서 사실, 이 URL을 서비스에다가 파라미터로 넘겨줘도 된다.
그러면 뭐 이 파라미터로 로그를 찍던, URL로 뭐 다른걸 하던, 굳이 mylogger를 만들어서 뭘 할 필요가 없다.

그러나 웹과 관련된 부분은 컨트롤러까지만 사용하는 것이 좋다.
서비스 계층은 웹 기술에 종속되지 않고, 그냥 딱 비즈니스 로직까지 관리하는것이 유지보수하기 좋다.
즉 RequestURL같은 웹과 관련된 정보가 웹과 전혀 관련없는 비즈니스 계층인 서비스 계층까지 넘어가지 않는게 중요하다.

그러나 딱 여기까지만 보면, 이게 실행이 될거같은가?

여기서 실행이 안되고, 안되는 이유를 맞춘다면 지금까지 학습을 잘한것이다.

이유는 우리는 스프링 컨테이너를 띄울때, Autowired를 통해서 DI까지 다 마친후 스프링컨테이너를 준비한후에 실행한다.

그러나 MyLogger mylogger를 private final로 설정하여, 이게 LogDemoController를 빈에 등록할때 DI를 위해서 스프링 컨테이너를 뒤지는데 MyLogger가 없는것이다.

왜없을까? 당연히 MyLogger의 스코프는 request이므로, HTTP Request가 들어와야 Mylogger가 생긴다. 지금 요청이 들어와 있지 않은 스프링 컨테이너를 올리는 상황이라 아무리 찾아도 없는것이다.

그러면 어떻게 해야할까?
맞다. 이전에 배운 Provider를 사용해야한다.

일단 Controller를 빈에다가 등록을하고 그다음에 필요할때 생성해서 DI를 해주는 지연 처리가 필요하다는 말이다.

ObjectProvider를 사용


여기서 ObjectProvider를 사용하면 의존성 주입이 스프링 컨테이너를 띄울때 LogDemoController를 등록할때 되는게 아니라,
ObjectProvider.getObject()를 호출하는 시점까지 지연해서 request scope빈의 생성을 지연할 수 있다.

ObjectoProvider.getObject()를 호출하는 시점에는 HTTP 요청이 현재 진행중이니까, request scope빈의 생성이 정상 처리된다.->MyLogger가 정상적으로 생성되고 등록된다.


여기서도 ObjectProvider를 통해서 지연생성이가능하다.
그런데 중요한게 어? logic메서드에서 myLogger.getObject를 하면 myLogger가 새로 생성되는거 아닌가? 싶을 수 있는데
이게, 같은 HTTP요청이면 같은 스프링 빈이 반환된다. 고로, 내가 직접 구분하지 않아도된다.
아까 위에서 컨트롤러에서 생성된 Mylogger 인스턴스가 스프링 빈에 등록->이걸 가져온다.

정상적으로 작동된다.
만약에 request요청을 여러번 보내면

각 request마다 uuid가 구분되어서 로그가 찍히고, 각 MyLogger마다, init과 destory메서드 호출이 정상적으로 이루어 짐을 확인 할 수 있다.

앞에서 말한 그, user마다 구분하는게 아니라는것이 말이다.
지금 내가 request를 여러번 보내서, clientA가 동일한 상황인데,
그래도 각 Request마다 uuid가 생성되므로, 고객을 구분 x -> request 별로 구분 이다.

프록시 사용

이번에는 프록시 방법을 사용 해볼 것이다.

우선 MyLogger의 scope에서 proxyMode를 추가해줍니다. class면 Target.CLass, 인터페이스면 Interface.Class이런식으로 만들면 됩니다.

이렇게하면 MyLogger의 가짜 프록시 클래스를 만들어두고, HTTP request와 관계없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있습니다.

LogDemoController

여기서 원래 DI가 필요한 MyLogger는 프록시 MyLogger를 만들고 주입해줍니다.
당연히 이러면, 싱글톤 빈으로 프록시 MyLogger가 등록이 됩니다.

실행을 시켜보면 정상적으로 작동함을 알 수 있다.
왜그런걸까?

우선
sout(myLogger.getClass());이걸 해보자
class hello.core.common.MyLoggerEnhanceBySpringCGLIBEnhanceBySpringCGLIBb68b7726d 이런식으로
갑자기? CGLIB가 나타남을 알수 있다.

바로 CGLIB라는 라이브러리를 통해 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 싱글톤 빈으로 등록하는 것이다.

그래서 ac.getBean("myLogger",MyLogger.class)로 가져와도 프록시 객체가 조회됨을 확인 할 수 있다.

그런데, 중요한게
가짜 프록시객체에는 실제 MYLOGGER가 들어있는게 아니라, 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.

가짜 프록시 객체는 내부에 진짜 MyLogger를 찾는 방법을 알고 있다.

그래서 클라이언트가 컨트롤러 코드에서 설명을 해보자면,
일단 스프링 컨테이너 뜨고, DI가 필요할때는 가짜 프록시 MyLogger를 생성해서 주입해준다.

그다음에 Rest APi요청이 들어오면, 그때 이제 HTTP Request가 들어오므로, 이 가짜 프록시 객체가 실제 요청이 오면 그때 내부에서 실제 빈 MyLogger를 요청하는 위임 로직이 들어있다.

그리고, myLogger.setRequestURL(requestURL); 이런식으로 로직이 진행되는데 여기서 myLogger는 당연히 프록시이다.

그러나 앞에서 말했듯 가짜 프록시 객체는 내부에 진짜 MyLogger를 찾는 방법을 알고 있으므로, 여기서 가짜 프록시한테 메서드 요청이 오면,
아까 만들어놨던 진짜 MyLogger를 찾아서 진짜 MyLogger의 setURL을 호출한다.

고로, 가짜 프록시 객체는 실제 request scope랑은 아에 관련이 없고, 그냥 들어온 요청을 진짜를 찾아 전달해주는 로직만있다. 고로 싱글톤으로 관리한다 한들 아에 상관이 없다.

사실 Provider를 사용하던, 프록시를 사용하던 핵심아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연 처리한다는것이다.

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글