Spring 스터디2 - 10
스프링 빈 스코프
스프링 빈 스코프(Bean Scope)는 스프링 컨테이너가 관리하는 객체인 빈이 생성되고 소멸될 때까지 존재할 수 있는 범위나 생명주기이다. 주요 스코프 종류는 다음과 같다.
- 싱글톤(Singleton) : 기본값으로 스프링 컨테이너 시작부터 종료까지 단 하나의 인스턴스만 생성, 모든 요청에 동일 객체를 반환
- 프로토타입(Prototype) : 빈을 요청할 때마다 새로운 인스턴스 생성. 생성과 의존성 주입까지만 컨테이너가 관리하며 이후는 클라이언트가 직접 관리 필요
- request : HTTP 요청마다 새로운 빈 생성, 요청이 끝나면 소멸한다. 주로 웹 애플리케이션에서 사용
- session : HTTP 세션마다 새로운 빈 생성, 세션 종료 시 소멸한다. 주로 사용자별 데이터 관리에 사용
- application : 서블릿 컨텍스트와 동일한 생명주기를 가지며, 웹 애플리케이션 전체에서 공유
- websocket : 웹소켓 연결마다 빈 생성, 연결 종료 시 소멸한다.
프로토타입 스코프
- 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
- 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다. (3개의 클라이언트 동일)
- 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
- 스프링 컨테이너는 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
- 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
- 이후에 빈을 관리하지 않으며, 3개의 클라이언트는 다른 빈을 받는다.
싱글톤 스코프 빈 테스트
package spring.basic.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.*;
public class SingletonTest {
@Test
void singletonBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class); //최초 호출 시 init 실행
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
assertThat(singletonBean1).isSameAs(singletonBean2); //둘은 같은 객체이다
ac.close(); //종료 시 destroy 실행
}
@Scope("singleton") //기본 Scope 는 singleton 이다
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
프로토타입 스코프 빈 테스트
package spring.basic.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.assertThat;
public class PrototypeTest {
@Test
void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); //호출 시 init 실행
System.out.println("find prototypeBean2");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); //호출 시 init 실행
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2); //둘은 다른 객체이다
ac.close(); //종료 시 destroy 를 실행하지 않는다
}
@Scope("prototype") //Scope 를 prototype 으로 변경
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
프로토타입과 싱글톤을 함께 사용할 때 문제점
일반적으로 프로토타입 스코프를 사용하는 이유는 클라이언트가 요청할 때마다 인스턴스를 생성하기 위해서일 것이다. 하지만 스프링은 싱글톤 빈의 의존성 주입을 생성할 때 한 번만 주입한다. 따라서 싱글톤 빈에 프로토타입 스코프를 가진 빈을 주입 받아서 사용할 경우 주입 시점에 프로토타입 빈이 생성되어 주입되며 이후에 같은 인스턴스를 사용하게 된다(프로토타입 빈의 의미가 사라진다). 이 문제점을 해결하기 위해서는 ObjectFactory 또는 Provider를 사용할 수 있다.
package spring.basic.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Provider;
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 static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@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(1);
}
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
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");
}
}
}
웹 스코프
웹 스코프는 웹 환경에만 동작하며, 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 종류는 위에서 말했던 것 중 request, session, application, websocket이 있다. 이번에는 request 스코프를 통해 로그를 남기는 예제를 작성했다.
MyLogger
package spring.basic.common;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import java.util.UUID;
@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();
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")
로 request 스코프로 지정하였기 때문에 이 빈은 HTTP 요청 당 하나의 인스턴스가 생성되고, HTTP 요청 종료 시 소멸된다. 빈 생성 시 @PostConstructor
를 통해 UUID를 만들어서 저장하고, 빈 소멸 시 @PreDestroy
를 통해 소멸 메시지를 남긴다. requestURL
은 빈 생성 당시에는 알 수 없기 때문에 setter로 입력받는다.
LogDemoController
package spring.basic.web;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import spring.basic.common.MyLogger;
@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 myLogger = myLoggerProvider.getObject();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
MyLogger가 제대로 동작하는지 확인하기 위한 컨트롤러로, log-demo
로 요청한다.
LogDemoService
package spring.basic.web;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import spring.basic.common.MyLogger;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
// MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
요청에 따른 로그가 정상적으로 출력된다.
MyLogger에서 @Scope
의 proxyMode를 ScopedProxyMode.TARGET_CLASS를 설정해놨는데, 이는 MyLogger를 프록시 객체로 생성하고, 내부에서는 진짜 myLogger를 호출하게 된다. 이전에 나왔던 프록시 처럼 CGLIB을 통해 처리된다. Provider나 프록시나 진짜 객체 조회를 필요한 시점까지 지연처리 한다는 점이 중요하다.