빈 스코프가 뭐에요?
👧 빈이 존재할 수 있는 범위
✔ 스프링이 지원하는 다양한 스코프
싱글톤 | 기본 스코프로 스프링 컨테이너의 시작과 종료까지 유지되는 넓은 범위의 스코프 |
프로토타입 | 매우 짧은 범위의 스코프, 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입가지만 관여 |
웹 관련 스코프 | - request : 웹 요청이 들어오고 나갈 때 까지 유지 - session : 웹 세션이 생성되고 종료될 때 까지 유지 - application : ;웹의 서블릿 컨텍스트와 같은 범위로 유지 |
✔ 싱글톤 스코프
기본인 싱글톤 스코프의 경우 스프링 조회 시 항상 같은 인스턴스를 공유해서 사용한다.
package com.example.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 SingletonTest {
@Test
void singletonBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean = " + singletonBean);
System.out.println("singletonBean2 = " + singletonBean2);
assertThat(singletonBean).isSameAs(singletonBean2);
ac.close();
}
@Scope("singleton")
static class SingletonBean{
@PostConstruct
public void init(){
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destory(){
System.out.println("SingletonBean.destory");
}
}
}
- singletonBean, singletonBean2는 같은 객체다.
- init과 distory를 한번 실행한 것을 확인할 수 있다.
✔ 프로토타입 스코프
- 프로토타입 스코프는 스프링을 조회하면 항상 새로운 인스턴스를 생성해서 반환한다.
- 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다.
- 프로토타입 빈 관리는 클라이언트가 해야 하기 때문에, @PreDestory 같은 종료 메서드가 호출되지 않는다.
package com.example.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 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("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.destory");
}
}
}
- prototypeBean1과 prototypeBean2는 다른 객체다
- close()를 했음에도 destory()가 실행되지 않는다.
✔ 프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 문제점
스프링 컨테이너에서 프로토타입의 스프링 빈과, 싱글톤 빈을 함께 사용하면 의도대로 동작되지 않는다
package com.example.core.scope;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import static org.assertj.core.api.Assertions.*;
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);
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();
return prototypeBean.getCount();
}
}
@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);
}
public void destroy(){
System.out.println("PrototypeBean.destroy"+this);
}
}
}
- 싱글톤 빈인 ClientBean에서 프로토타입의 빈을 생성했다.
- 프로토타입의 빈이면 새로운 객체를 계속해서 만들기 때문에 assertThat(count2).isEqualTo(2); 에서 오류가 발생해야하지만, 해당 코드에서 오류가 발생하지 않는다.
싱글톤 빈은 생성 시점에서 의존관계를 주입 받고, 주입 시점에 프로토 타입이 빈이 새로 생성되긴 하지만 싱글톤 빈과 함께 계속 유지된다.
📌 프로토타입 스코프 + 싱글톤 빈 Provider로 문제 해결하자
✔ ObjectFactory,ObjectProvider
- 지정한 빈을 컨테이너에서 대신 찾아 주는 기능을 제공한다.
- ObjectProvider가 편의 기능이 추가돼서 제공해줌
- 싱글톤 빈에서 프로토타입의 빈을 생성할 때 ObjectProvider를 사용하면
- ObjectProvider에서 프로토타입의 빈을 찾아줘서 프로토타입의 기능을 수행한다.
✔ JSR-330 Provider
Gradle 추가 : implementation 'javax.inject:javax.inject:1'
- provider의 get()을 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
📌 웹 스코프
- 웹 환경에서만 동작하며, 프로토타입과는 다른게 종료 시점까지 관리된다.
- request : HTTP 요청 하나가 들어오고 나갈때까지 유지, 각각 HTTP 요청 마다 빈이 별도로 관리됌
- session : HTTP Session과 동일한 생명주기를 가지는 스코프
- application : 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
- websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프
✔ request 스코프 예제
Gradle 추가 (내장된 톰캣 서버 사용)
implementation 'org.springframework.boot:spring-boot-starter-web'
✔ request 스코프 Logger을 만들어 보자
- UUID를 사용해 HTTP 요청 구분
- requestURL 추가로 어떤 URL 요청인지 로그 확인
Logger
package com.example.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();
System.out.println("[" + uuid+"] request scope bean create : "+this);
}
@PreDestroy
public void close(){
System.out.println("[" + uuid+"] request scope bean close : "+this);
}
}
- request 스코프로 지정, 해당 빈은 HTTP 요청 당 하나 씩 생성된다.
- @PostConstruct 초기화 메서드를 사용해 uuid를 저장해 놓았다가 다른 HTTP 요청과 구분한다.
Controller
package com.example.core.web;
import com.example.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;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String reuquestURL = request.getRequestURI().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(reuquestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- 요청 URL을 받는다.
- request 스코프는 스프링 애플리케이션 실행 시점에 생기지않는다.(실제 고객이 요청이 와야 생성)
- ObjectProvider를 사용해 HTTP 요청이 왔을 때 request 스코프 빈을 생성한다.
Service
package com.example.core.web;
import com.example.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);
}
}
- 파라미터로 모든 정보를 서비스 계층에 넘길 수 있지만, 지저분해 진다. requestURL 과 같은 웹 관련 정보는 서비스 계층까지 안넘가는게 좋다.
✔ 스코프와 프록시
-ObjectProvider를 사용하지 않고 프록시를 사용해서 프로바이더처럼 사용할 수 있다.
@Component
@Scope(value = "request",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
private String uuid;
private String requestURL;
}
- CGLIB 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.
- 스프링 실행 시점에 가짜 플록시 객체를 만들어 주입만들어 문제가 발생하지 않는다.
- 실제 요청이 오는 시점에 내부에서 진짜 빈을 요청한다.
REFERENCE
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
'공부 > Spring 핵심 원리' 카테고리의 다른 글
웹 서버와 웹 애플리케이션, 서블릿, 쓰레드 알아보기 (0) | 2021.09.25 |
---|---|
빈 생명주기 콜백을 알아보자 (0) | 2021.09.02 |
다양한 의존 관계 자동 주입과 사용법들을 알아보자 (0) | 2021.09.01 |
컴포넌트 스캔으로 의존 관계를 자동 주입해보자. (0) | 2021.08.31 |
싱글톤 패턴을 보장해주는 스프링 컨테이너와 @Configuration (0) | 2021.08.29 |