1. 스코프는 빈이 존재할 수 있는 범위를 뜻한다.
스프링 빈은 기본적으로 싱글톤 스코프로 생성된다.
2. 스코프 3가지 종류
- 싱글톤
- 프로토타입
- 웹관련 - request
3. 싱글톤 빈
✅싱글톤 빈 예제 코드
public class SingletonTest {
@Test
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);
Assertions.assertThat(singletonBean1).isSameAs(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");
}
}
}
✅싱글톤 빈 테스트 결과
SingletonBean.init //빈을 두개 생성해도 init은 한번!
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@4593ff34
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@4593ff34
08:23:15.847 [Test worker] DEBUG o.s.c.a.AnnotationConfigApplicationContext -- Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@e98770d, started on Fri May 10 08:23:15 KST 2024
SingletonBean.destroy //종료메서드를 호출한다.
> Task :test
4. 프로토타입 빈
✔️생성, 의존관계 주입, 초기화까지만 처리한다.
✔️ 클라이언트에 빈을 반환한 후에는 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
✔️ 클라이언트가 종료메서드를 호출해줘야된다.
✅프로토타입 빈 예제 코드
public class PrototypeTest {
@Test
void prototypeBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2); //isNotSameAs로 테스트
ac.close(); //이렇게 close()를 호출해도 종료메서드가 호출되지 않을 것이다.
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init(){ //두번 호출 되어야한다.
System.out.println("prototypeBean.init");
}
@PreDestroy //호출 되지 않을 것이다.
public void destroy(){
System.out.println("prototypeBean.destroy");
}
}
}
✅프로토타입 빈 테스트 결과
prototypeBean.init
prototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@4593ff34
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@37d3d232
> Task :test
//init이 두번 호출됐다.
//prototypeBean1과 prototypeBean2이 다른 빈이다.
//detroy가 호출되지 않았다.
*참고*
프로토타입을 종료해주려면 직접 종료메서드를 하나씩 호출해줘야한다.
prototypeBean1.destroy();
prototypeBean2.destroy();
5. 프로토타입 빈, 싱글톤 빈 함께 사용할 시 주의점
✔️싱글톤 빈에 의존관계 주입으로 프로토타입 빈을 주입 받는 경우
✔️주입 받은 후, 프로토타입 빈의 clientBean1이 logic()을 호출한다.
✔️ clientBean2가 logic()을 을 다시 호출한다.
결과적으로, 생성자 주입시점에서 프로토타입빈이 새로 생성되지만, 싱글톤 빈과 함께 유지된다.
이전 프로토타입 빈 테스트에서는 두번째 호출 시 cnt가 그대로 1이었지만,
싱글톤과 함께 사용하니 cnt가 2가 된다.
🤔이게 왜 문제일까?
프로토타입 빈을 생성했을 때,의도한 것은 사용할 때마다 새로 생성하고 싶은 것이다.
clientBean1이 생성될 때 PrototypeBean도 딱 한 번 생성되고, 이후에는 그 PrototypeBean 인스턴스가 계속해서 재사용된다.
✅프로토타입 빈과 싱글톤 빈 함께 사용한 예제코드
public class SingletonWithPrototypeTest1 {
@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
//clientBean1
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int cnt1 = clientBean1.logic();
Assertions.assertThat(cnt1).isEqualTo(1);
System.out.println("cnt1 = " + cnt1);
//clientBean2
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int cnt2 = clientBean2.logic();
System.out.println("cnt2 = " + cnt2);
Assertions.assertThat(cnt2).isEqualTo(2);
}
@Scope("singleton")
static class ClientBean{
private final PrototypeBean prototypeBean; //생성시점에 주입
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic(){
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean{
private int cnt = 0;
public void addCount(){
cnt++;
}
public int getCount(){
return cnt;
}
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init"+this);
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.destroy"+this);
}
}
}
✅테스트 결과
cnt1 = 1
cnt2 = 2
//주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되
는 것이 아니다!
프로토타입 스코프 - 싱글톤 빈과 함께 사용할 때, provider로 문제 해결
싱글톤과 함께 사용할 때, 사용할 때마다 항상 새로운 프로토타입 빈 사용하는 법!
- ObjectProvider 의 `getObject()` 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환
한다. -(스프링에 의존적이다. 순수자바가 아니라는 말.)
- java 표준을 사용하는 방법 - (라이브러리를 gradle에 추가해주고 재빌드 해야한다.)
//java 표준 사용하기 위해 build.gradle에 라이브러리 추가(스프링 3.0 이상)
dependencies {
..나머지 생략..
implementation 'jakarta.inject:jakarta.inject-api:2.0.1'
}
✅ObjectProvider 사용한 예제 코드
public class SingletonWithPrototypeTest1 {
@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int cnt1 = clientBean1.logic();
Assertions.assertThat(cnt1).isEqualTo(1); //cnt1 = 1
System.out.println("cnt1 = " + cnt1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int cnt2 = clientBean2.logic();
System.out.println("cnt2 = " + cnt2);
Assertions.assertThat(cnt2).isEqualTo(1); // cnt2 = 1
}
@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;
}
}
@Scope("prototype")
static class PrototypeBean{
private int cnt = 0;
public void addCount(){
cnt++;
}
public int getCount(){
return cnt;
}
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init"+this);
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.destroy"+this);
}
}
}
✅provider 사용, 테스트 결과
PrototypeBean.inithello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@4ced35ed
cnt1 = 1
PrototypeBean.inithello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@7d2a6eac
cnt2 = 1
PrototypeBean이 매번 새롭게 생성됐다!
✅java 표준을 사용하는 방법
import jakarta.inject.Provider; //중요! 여러개 Provider중 jakarta.inject 선택하여 import해야한다.
@Scope("singleton")
@Component
static class ClientBean{
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
//Provider를
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
✅java 표준 Provider 테스트 결과
PrototypeBean.inithello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@79f227a9
cnt1 = 1
PrototypeBean.inithello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@26f1249d
cnt2 = 1
get()을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
get()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
6. 프로토타입 빈을 언제 사용?
매번 사용할 때 마다 새로운 객체가 필요하면 사용하면 된다.
실무에서 싱글톤 빈으로 대부분 해결돼서 프로토타입 빈을 사용하는 경우가 거의 없다.
만약 사용할 일이 있으면, 오늘 소개한 방법을 사용하면 된다.
7. 웹 스코프
✔️웹 환경에서만 동작한다.
✔️스프링이 해당 스코프의 종료시점까지 관리한다. (종료메서드가 호출된다.)
8. 웹 스코프 종류
- request : 각 HTTP 요청마다 별도의 빈 인스턴스를 생성하며, 요청이 끝나면 해당 인스턴스도 소멸
- session : HTTP 세션 동안에만 존재하는 빈 인스턴스를 생성하며, 세션 종료 시 인스턴스도 함께 소멸
- application : 웹 애플리케이션의 전체 생명주기(서블릿 컨텍스트) 동안 유지되는 빈 인스턴스를 생성, 애플리케이션이 종료될 때까지 존재.
- websocket: Websocket 세션 동안 유지되는 빈 인스턴스를 생성하며, Websocket 세션 종료 시 인스턴스도 소멸
* 참고
서블릿 컨텍스트는 웹 애플리케이션이 시작될 때 생성되어 웹 애플리케이션이 종료될 때까지 유지되는 환경
9. request 스코프 예제
✔️web 환경에서 동작하도록 build.gradle에 라이브러리를 추가해준다.(재빌드 필수)
//web 환경이 동작하도록 라이브러리 추가
dependencies {
..나머지 생략..
implementation 'org.springframework.boot:spring-boot-starter-web'
}
✅MyLogger
package hello.core.common;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid; //전세계에 하나뿐인 id가 만들어진다.
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();
//uuid는 HTTP 요청 당 하나가 생성된다. 다른 HTTP요청과 구분할 수 있다.
System.out.println("["+uuid+"]"+"request scope bean create: " + this);
}
@PreDestroy
public void close(){
System.out.println("["+uuid+"]"+"request scope bean close: " + this);
}
}
✅Controller
package hello.core.web;
import hello.core.common.MyLogger;
import jakarta.servlet.http.HttpServletRequest;
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;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo") //url을 매핑한다.
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();//HTTP 요청이 진행중
//request scope 빈의 생성이 정상 처리된다.
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
✅Service
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);
}
}
✅콘솔 로그 확인
[be2250dd-9429-4785-87b5-36bfe3c27366]request scope bean create: hello.core.common.MyLogger@17386f09
[be2250dd-9429-4785-87b5-36bfe3c27366][http://localhost:8080/log-demo] controller test
[be2250dd-9429-4785-87b5-36bfe3c27366][http://localhost:8080/log-demo] service id = testId
11:32:15.900 [http-nio-8080-exec-5] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor -- Using 'text/html', given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8, application/signed-exchange;v=b3;q=0.7] and supported [text/plain, */*, application/json, application/*+json]
11:32:15.900 [http-nio-8080-exec-5] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor -- Writing ["OK"]
[be2250dd-9429-4785-87b5-36bfe3c27366]request scope bean close: hello.core.common.MyLogger@17386f0
-be2250dd-9429-4785-87b5-36bfe3c27366는 임의로 생성된 UUID다.
UUID가 같으면 같은 HTTP요청을 다룬 작업이란 뜻이다.
🤔의문점
ObjectProvider을 사용하면 항상 새로운 빈을 반환한다 했는데? 왜 UUID가 같게 되는 걸까?
💡해결점
ObjectProvider.getObject()를 컨트롤러, 서비스에서 각 각 따로 사용해도, 같은 HTTP요청이면 같은 스프링 빈이 반환된다.
10. 스코프와 프록시
ObjectProvider를 쓰지 않고 코드를 더 간단히 쓸 수 있게 해주는 프록시 사용법을 봐보자.
1️⃣프록시 객체 생성:
- 스프링은 MyLogger의 실제 인스턴스 대신, MyLogger 클래스의 서브클래스를 동적으로 만드는 CGLIB 라이브러리를 사용해 프록시 객체를 생성한다.
- 이 프록시 클래스는 원본 MyLogger 클래스의 모든 메소드를 오버라이드한다.
- 즉, 실제 메소드 호출이 필요할 때마다, 프록시는 현재 HTTP 요청에 맞는 MyLogger 인스턴스를 찾아 그 메소드를 실행한다.
- 이 방식으로 각 요청마다 MyLogger의 독립된 인스턴스가 사용된다.
2️⃣요청 감지 및 빈 제공:
- HTTP 요청이 들어올 때마다, 스프링은 새로운 MyLogger 인스턴스를 생성하고 요청과 관련된 정보를 초기화한다.
3️⃣메소드 호출의 중개:
- MyLogger의 메소드를 호출할 때, 프록시 객체는 실제 요청 스코프에 해당하는 MyLogger 인스턴스를 스프링 컨테이너에서 조회한다. 이렇게 조회된 인스턴스에 대해 호출이 이뤄지고 실행된다.
4️⃣요청 종료와 빈 파괴:
- HTTP 요청이 완료되면, 요청 스코프에 저장된 MyLogger 인스턴스는 스코프가 종료되면서 @PreDestroy 어노테이션이 붙은 메소드를 통해 적절하게 리소스가 정리되고 객체가 소멸된다.
✅프록시를 사용한 MyLogger 코드
@Scope옵션에 proxyMode = ScopedProxyMode.TARGET_CLASS이 추가되었다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) //이 부분이 추가됐다.
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(); //전세계의 유일한 아이디
//uuid는 HTTP 요청 당 하나가 생성된다. 다른 HTTP요청과 구분할 수 있다.
System.out.println("["+uuid+"]"+"request scope bean create: " + this);
}
@PreDestroy
public void close(){
System.out.println("["+uuid+"]"+"request scope bean close: " + this);
}
}
✅프록시를 사용한 Controller 코드
@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";
}
}
✅프록시를 사용한 Service 코드
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
'인프런 김영한 강의 정리 > 스프링 핵심원리 기본편' 카테고리의 다른 글
스프링 컨텍스트 이해와 선택: 애플리케이션 요구에 맞는 올바른 컨텍스트 사용하기 (0) | 2024.05.07 |
---|---|
스프링 핵심 원리 기본편 - 빈 생명주기 콜백 (0) | 2024.05.07 |
스프링 핵심원리 기본편 - 의존관계 자동 주입(5)|조회한 빈이 모두 필요할 때, List, Map (0) | 2024.05.07 |
스프링 핵심 원리 기본편 - 의존관계 자동 주입(4)|조회하는 빈이 2개 이상,해결법, 애노테이션 직접 만들기 (0) | 2024.05.06 |
스프링 핵심원리 기본편 - 의존관계 자동 주입(3)|롬복과 최신 트랜드 (0) | 2024.05.06 |