김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.
앞의 작성한 글에서는 스프링 빈이 컨테이너 생성을 시작으로 생성되어서 컨테이너가 종료될 때, 소멸된다고 이야기 했습니다. 이것은 스프링 빈이 싱글톤 스코프로 생성되기 때문입니다.
스코프란 말 그대로 범위. 즉, 빈이 존재할 수 있는 범위입니다.
스프링은 다양한 빈 스코프를 지원합니다.
다음과 같이 등록할 수 있습니다.
@Scope("prototype")
@Component
public class HelloBean {}
싱글톤 스코프의 경우 늘 같은 인스턴스를 반환하지만, 프로토타입 스코프는 매번 새로운 인스턴스를 생성해서 다른 인스턴스를 반환합니다.
싱글톤 빈의 경우
1. 클라이언트가 요청(MemberService)을 날리면 스프링 컨테이너는 해당하는 스프링 빈을 반환합니다.
2. 또 다른 클라이언트가 요청(MemberService)을 날리면 1번의 빈과 동일한 스프링 인스턴스를 반환해줍니다.
프로토타입 빈의 경우
1. 클라이언트가 요청(prototype Bean)을 날리면 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입하고 반환해줍니다.
2. 또 다른 클라이언트가 요청을 날리면 새로운 프로토타입 빈을 생성, 의존관계 주입하고 새로운 인스턴스를 반환해줍니다.
핵심은 스프링 컨테이너가 프로토타입 빈을 생성하고 의존관계를 주입, 초기화까지만 관여를 한다는것. 그 이후에는 스프링 컨테이너가 관리를 하지 않습니다.
이 말은 즉, @PreDestroy를 수행하지 않는다는 것입니다.
테스트
@Test
public 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);
}
@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
singletonBean1 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd
singletonBean2 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing
SingletonBean.destroy
@Test
public 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);
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
이를 실행해 보면 두개의 인스턴스가 다르다는 것과 destroy()함수 호출이 안되는 것을 알 수 있습니다.
PrototypeBean.init
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing
✓ 프로토타입 빈의 특징 정리
그렇다면 이런 상황일 때는 어떻게 해야 할까요? 🤔
첫번째 상황
A라는 클라이언트가 프로토 타입 빈을 요청하면 스프링 컨테이너에서 새로 생성해서 count필드가 0인 객체를 반환(x01)합니다. 그 이후에, addCount()
를 통해 count 값을 1증가 시킵니다. 결국, 빈(x01)의 count필드 값은 1이 됩니다.
B라는 새로운 클라이언트가 A와 동일한 행동을 하게 된다면, count필드가 1인 객체(x02)를 반환받게 됩니다.
두번째 상황
이번에는 싱글톤 빈 안에서 의존관계 주입을 통해 프로토타입 빈을 주입받는 상황입니다.
싱글톤 빈은 스프링 컨테이너 생성 시점에 생성되어 의존관계 주입을 받게 되므로, 주입 시점에 프로토 타입을 주입 받게 됩니다.
이 때, 프로토 타입 빈의 필드 값인 count값은 0이고 A 클라이언트가 logic()
메서드(프로토 타입 빈의 count 값 1증가)를 호출하면 count값이 1이 됩니다.
또 다른, B 클라이언트가 동일한 메서드 logic()
을 호출하게 된다면 싱글톤이기에 같은 객체를 받게 되고 그 안에 미리 주입 받은 프로토 타입 빈(이전에 증가해서 count값이 1) 내의 count 값은 2가 됩니다.
싱글톤 내부에 있는 프로토 타입 빈은 이전에 의존관계 주입이 끝났기 때문에 새롭게 생성하지 않습니다!!
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;
}
}
하지만 느낌상 원하는 것은 프로토 타입 빈이 싱글톤 내부에 있지만 부를 때마다 새롭게 생성되는 것입니다.
다음과 같은 방법으로 해결할 수 있답니다.
가장 간단한 방법은 프로토타입 빈을 사용할 때마다, 스프링 컨테이너에 요청하는 것.
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
이 부분에서 매번 새로운 프로토 타입 빈이 생성됩니다. 이것은 의존관계 주입(DI)이 아닌 의존관계 조회, 탐색(DL, Dependency Lookup)입니다.
하지만 ApplicationContext을 지금 주입받게 되므로, 스프링 컨테이너에 종속적입니다.
또다른 방법이 없을까요? 🤔
지정한 빈 (< PrototypeBean >)을 컨테이너 대신 찾아주는 것이 ObjectProvider
입니다.
(과거에는 ObjectFactory
)
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
ObjectProvider.getObject()를 통해 매번 새로운 빈이 생성됩니다.
이것은 자바 표준입니다.
라이브러리에 필수적으로 추가해주어야합니다. (gradle)
implementation 'javax.inject:javax.inject:1'
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
웹 스코프는 웹 환경에서 작동하고, 프로토 타입 스코프와 달리 종료 시점까지 스프링이 관리합니다. (-> 이 말은 곧 종료 메서드도 호출 된다는 것)
종류에는 requset, session, application, websocket이 있습니다.
웹 환경을 만들기 위해 build.gradle에 추가해줍니다.
implementation 'org.springframework.boot:spring-boot-starter-web'
이는 내장 톰캣 서버를 활용해서 웹서버와 스프링을 함께 실행시키는 것을 가능하게 해줍니다.
동시에 여러 HTTP 요청이 오면 구분하기 위해 로그를 남기도록 예제를 구현하겠습니다.
[a332fav] requset scope bean created
[a332fav] [localhost:8080] ~~~
[a332fav] requset scope bean close
[UUID][requestURL] {message}의 형식
@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);
}
}
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);
}
}
https://localhost:8080/log-demo 를 웹사이트 주소창에 입력하게 되면 위의 코드가 작동하게 됩니다.
하지만 실제로 입력을 해보면 오류가 나타나게 됩니다.
request빈은 요청이 들어와야 생성할 수 있습니다!
즉, 요청이 들어오지 않은채로 의존관계 주입을 하려 했기 때문에 생긴 문제.
앞서 배운 Provider를 이용하면 해결 가능합니다.
private final ObjectProvider<MyLogger> myLoggerProvider;
public String logDemo(HttpServletRequest request) {
...
MyLogger myLogger = myLoggerProvider.getObject();
...
}
getObject()를 호출하는 시점은 이미 주소창에 url을 입력했을 시기이기 때문에 HTTP 요청이 진행중입니다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
이 때, 클래스면 TARGET_CLASS. 인터페이스라면 TARGET_INTERFACE.
HTTP 요청에 상관 없이 CGLIB
라이브러리를 이용해 가짜 프록시 클래스를 만들고, 빈에 미리 주입해 놓을 수 있습니다.
이전에는 requset 스코프 였기 때문에 요청이 들어오기 전에 주입하는 것이 문제가 됐으므로 이 문제가 해결 된다는 것.
CGLIB는 바이트코드를 조작해 가짜의 무언가를 만듭니다. (여기서는 가짜 프록시 클래스, 즉 가짜 MyLogger 클래스)
진짜 요청이 들어오게 되면 가짜 프록시 클래스 -> 진짜 클래스를 찾게 됩니다.
myLogger.logic()은 가짜 프록시 클래스의 메서드를 호출한 것이고 이 클래스는 진짜 클래스의 logic을 호출합니다.
이상으로 마무리하겠습니다. 이번 편은 길었네요..
저도 틈틈히 보면서 열심히 공부해야 겠어요.
다들 화이팅~!~!