"김영한 스프링 핵심 원리 기본편" 내용을 기반으로 작성한 내용입니다.
📝스코프
스코프란 스프링 컨테이너에서 빈을 관리하는 영역을 이야기한다. 기본적으로 스프링은 싱글톤 스코프를 가지고 이걸 가장 많이 사용한다. (이번 챕터는 그렇게 핵심은 아니지만 알아둬서 나쁠 거 없는 내용이고 언젠가는 쓰이는 내용이긴 하다) 참고로 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다
📝프로토타입
싱글톤이 아니라 빈의 생성하고 의존관계 주입까지만 관여하고 더는 관리하지 않는 범위이다. 그렇기 때문에 직접 종료를 해줘야한다. 그렇지 않으면 엄청나게 메모리에 쌓일 것이다. 예를 들자면 다양한 클라이언트가 어떤 Bean을 요청했을 때 일반적인 싱글톤의 경우 해당 Bean을 공유해서 사용한다. (때문에 도중에 값을 수정하면 다른 사람에게도 영향이 감) 반면 프로토타입의 경우 요청마다 Bean을 새롭게 생성해서 독자적인 스코프 영역을 만든다.
// prototype 사용예제1
@Scope("prototype")
@Component
public class HelloBean {}
// prototype 사용예제2
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
📝싱글톤에서 프로토타입 빈을 사용 주의점
싱글톤으로 생성된 Client Bean이라는 객체가 있고 의존관계 주입된 Bean이 프로토타입일 경우 ClientBean을 이용해 의존관계 주입된 Prototype으로 만들어진 Bean의 필드값을 바꾸는 경우를 생각해보자
일반적으로 Prototype이라서 서로 다른 클라이언트가 요청해도 필드값은 +1해도 각각 +1씩 가져야하지만 Client Bean이 넓은 범위 싱글톤이기 때문에 Prototype도 공유하게 된다.
📝 Prototype 유지하기 (Provider)
public class PrototypeProviderTest {
@Test
void providerTest() {
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(1);
}
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@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");
}
}
}
위와 같은 문제를 Provider라는 걸 이용해 해결이 가능하다. 일반적인 방법으로는 ClientBean에 있는 logic을 실행시킬때마다 Prototype이 자동으로 의존관계를 주입하게 하는게 아니라 내가 직접 스프링 컨테이너에서 찾아서 넣어주게끔 한다 ac.getBean(PrototypeBean.class); 이러한 과정을 DL(Dependency Lookup)이라고 한다. 위 코드는 DL을 내가 직접해주는게 스프링에서 제공해주는 게 있다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
스프링에서는 ObjectProvider는 빈을 컨테이너에서 대신 찾아주는 DL 서비스 제공해준다.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
또 다른 방법으로는 Java표준인 jakarta.inject.Provider을 사용하는 것이다. 얘는 따로 라이브러리 추가를 해주긴 해야한다.
📝 웹스코프
웹스코프란 웹에서 요청이 들어왔을 떄 언제까지 유지할지에 대한 범위이다.
- request
- HTTP Request와 동일한 생명주기에 요청마다 별도 빈 인스턴스가 생성되고 관리된다.
- session
- HTTP Session과 동일한 생명주기
- application
- 서블릿 컨텍스트와 동일한 생명주기
- websocket
- 웹 소켓과 동일한 생명주기
// 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";
}
}
// Logging Common
@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);
}
}
// Service Area
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
위 예제는 여러사용자가 많이 방문하면 로깅이 많이 찍힐텐데 동시다발적인 경우 어떤 곳에서 에러가 발생했을 경우 어떤 흐름에서 끊겼는지 구분해줄 수 있다. (HTTP Request Bean을 독자적 관리하기 때문에 구분 가능) 위 코드를 사용하면 MyLogger를 의존관계 주입을 하려고했지만 Request Scope라서 Request요청이 들어와야 생성된다. 이럴 경우 MyLogger가 필요한 곳에서 넣지못해서 에러가 나게 된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
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";
}
}
...
이러한 에러는 ObjectProvider로 해결이 가능하다 ObjectProvider는 스프링이 띄워질 때가 아니라 뒤늦게 요청이 들어와야 동적으로 만들어지거나 뒤늦게 조회가 필요한 경우 지연 조회 처리가 가능하다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
더 간단한 방법이 있는데 Proxy를 이용해 가짜 빈을 넣어줘서 있는 것처럼 동작하게끔한다. 그래서 에러가 나지 않는다.