빈 스코프라고는 하지만, 뭔가 뒤로 갈 수록 잘 안쓰이지만 이론적으로는 알아야 하는 내용들을 설명해주는 듯 하다.
사실상 쓰는건 request정도고 prototype 아주 가끔 쓰는 정도인듯. 그래서 request scope를 배우기 위한 과정 같음.
일단 기존의 싱글톤을 보자.
싱글톤은 지금까지 써왔던 것으로, 컨테이너가 생성되고 빈으로써 컨테이너에 들어가서 스프링이 종료되어 컨테이너가 없어질 때 까지 계속 남아 혼자 일을 한다.
그런데 프로토타입 스코프는 평소엔 생성되지 않다가, 요청이 들어오면 빈을 새로 만들어서 수행하고 종료됨. 만들어질 때 스프링 컨테이너가 빈을 생성하고, 의존관계 주입, 초기화 까지만 해주고 뒤로는 신경 안써서 종료할때인 @PreDestroy를 사용하지 못한다.
싱글톤 안에 프로토타입 스코프를 쓰는 예시가 있는데, 사실 거의 안쓴다.
뒤에 request 스코프에서 쓰일 provider를 소개해주기 위한 예시 정도.
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
Assertions.assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
Assertions.assertThat(count2).isEqualTo(2);
}
@Scope("singleton")
static class ClientBean {
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
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);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
내용인 즉슨 원래 프로토타입 스코프는 요청할 때 생기고 컨테이너가 신경도 안쓰지만 싱글톤인 경우 싱글톤이 관리해줘서 다음 클라이언트가 진행되도 아까 생성한 객체를 다시 불러와서 누적된다는 것.
해결 방법은 안에 컨테이너가 객체를 다시 만들도록 하는 코드도 안에 넣는 것.
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");
}
}
}
logic 안에 getBean으로 새로 만들어준다.
원래 지금까지는 스프링이 알아서 만들어줘서 주입 해줬으므로 의존관계 주입(DI)를 해줬지만, 이 경우는 내가 직접 만들라도 지시한 경우이므로 Dependency Lookup (DL)이라고 한다.
코드가 너무 복잡해지므로 스프링에서 DL 정도만 지원해주는 ObjectFactory, ObjectProvider가 있다. Factory는 너무 옛날거라 더 상위버전인 Provider를 사용하자.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
다음은 request 스코프인데 다른 session, application, websocket 스코프도 있지만 request가 제일 많이 쓰이고 비슷하기 때문에 이걸 보자.
일단 이게 필요한 이유는 위에서 prototype이 lazy하게 실행되서 진짜 필요한 순간에만 사용한 것과 같이 request도 요청이 들어왔을 때에만 prototype마냥 새로 인스턴스를 생성해서 사용하고 요청이 끝나면 반환한다. 중요한 건 각각의 http request마다 1:1로 생성되서 독자적이 되어 관리가 매우 편하다는 것.
로그 단위를 요청 스코프로 해놓고 설정한다.
이렇게만 하고 시작하면 오류가 발생하는데, 이유는 MyLogger는 스코프가 request여서 request가 생성될 때 만들어지기 때문에, 처음부터 모든것을 주입해서 만들려고 하는 스프링에서 아직 생성되지 않은 MyLogger를 주입하려 하기 때문.
여기서 아까 위에서 봤던 provider를 사용하는 것이다. prototype도 스프링 시작할 때 만들어지는게 아니라 실행될 때 그때 생성되는 것인데 잘 됐잖아? 그래서 여기서 써먹으려고 배운거임.
provider로 바꿔서 하면 잘 된다.
사실 provider안쓰고 매우 간단하게 하는게 있는데, 가짜 프록시 객체를 사용하는 방법임.
@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 = java.util.UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean created: " + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean closed: " + this);
}
}
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다. 위 그림에서 클라이언트가 logic을 호출하면 가짜가 받아서 진짜로 호출하는 것. 가짜이므로 새로 요청받을 때 때 새로 만들어서 호출할 수 있도록 기능을 만들었다.
근데 사실 이런 특별한 scope들은 꼭 필요한 곳에서만 사용하자. 안그러면 유지보수가 어려워짐.
'CS > 김영한 스프링 강의' 카테고리의 다른 글
HTTP 웹 기본 지식 - ~ 섹션8. 캐시와 조건부 요청 (0) | 2023.07.11 |
---|---|
HTTP 웹 기본 지식 - ~ 섹션5. HTTP 메서드 활용 (0) | 2023.07.09 |
스프링 기본 - 섹션8. 빈 생명주기 콜백 (0) | 2023.07.06 |
스프링 기본 - 섹션7. 의존관계 자동 주입 (0) | 2023.07.05 |
스프링 기본 - 섹션6. 컴포넌트 스캔 (0) | 2023.07.03 |