From 71d206a5f89e2e8f57393c9917b99fd46a0fdf15 Mon Sep 17 00:00:00 2001 From: hyxrxn Date: Thu, 4 Apr 2024 15:55:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=95=84=ED=86=A0=20=EC=84=B9=EC=85=98=209?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "section9/\354\225\204\355\206\240.md" | 494 +++++++++++++++++++++++++ 1 file changed, 494 insertions(+) create mode 100644 "section9/\354\225\204\355\206\240.md" diff --git "a/section9/\354\225\204\355\206\240.md" "b/section9/\354\225\204\355\206\240.md" new file mode 100644 index 0000000..76c65c2 --- /dev/null +++ "b/section9/\354\225\204\355\206\240.md" @@ -0,0 +1,494 @@ +# 빈 스코프 +- 빈이 존재할 수 있는 범위 +## 예시 +### 싱글톤 + - 기본 스코프 + - 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프 +### 프로토타입 + - 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여 + - 매우 짧은 범위의 스코프 +### 웹 관련 + - request + - 웹 요청이 들어오고 나갈때 까지 유지되는 스코프 + - session + - 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프 + - application + - 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프 + +## 지정 방법 +### 자동 +```java +@Scope("prototype") +@Component +public class HelloBean {} +``` +### 수동 +```java +@Scope("prototype") +@Bean +PrototypeBean HelloBean() { + return new HelloBean(); +} +``` + +## 프로토타입 스코프 +### 설명 +- 싱글톤은 항상 같은 인스턴스의 스프링 빈을 반환했었음 +- 프로토타입이면 항상 새로운 인스턴스를 생성해 반환함 +- 프로토타입 빈을 요청하면 그 시점에 생성하고 의존관계 주입 후 반환 +- 스프링 컨테이너는 그 이후는 관리하지 않음 +- 빈을 받은 클라이언트가 책임져야 함 +- 사용할 때마다 매번 의존관계 주입이 완료된 새로운 객체가 필요하면 사용 +- 거의 사용할 일 없긴 함... +### 싱글톤인 경우 +```java +@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); + + assertThat(singletonBean1).isSameAs(singletonBean2); + ac.close(); +} + +@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 +``` +- 컨테이너 생성 시점에 초기화 +- `Bean1`과 `Bean2`가 같음 +- 정상적으로 `destroy` 실행됨 +### 프로토타입인 경우 +```java +@Test +public 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 destroy() { + System.out.println("PrototypeBean.destroy"); + } +} +``` +``` +find prototypeBean1 +PrototypeBean.init +find prototypeBean2 +PrototypeBean.init +prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d +prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971 +org.springframework.context.annotation.AnnotationConfigApplicationContext - +Closing +``` +- 빈을 조회하는 시점에 초기화 +- `Bean1`과 `Bean2`가 다름 +- `destroy` 실행되지 않음 + +## 프로토타입 빈과 싱글톤 빈 동시 사용 +### 프로토타입 빈을 직접 요청하는 경우 +```java +@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); +} + +@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"); + } +} +``` +### 싱글톤 빈에서 프로토타입 빈을 사용하는 경우 +```java +@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); +} + +static class ClientBean { + + private final PrototypeBean prototypeBean; + + @Autowired + public ClientBean(PrototypeBean prototypeBean) { + this.prototypeBean = prototypeBean; + } + + public int logic() { + 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"); + } +} +``` +- 클라이언트1과 2는 같은 `ClientBean`을 사용 +- `ClientBean`의 생성자에서 `PrototypeBean` 주입받을 때 `PrototypeBean` 생성 +- 그래서 계속 같은 `PrototypeBean`을 사용하게 됨 +- 의도한 바와 다른 결과... +### 간단한 해결책 +```java +@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 { + + ... + +} +``` +- `ac.getBean(PrototypeBean.class)`을 통해 매번 새로운 빈 생성 +- 의존관계를 직접 찾으면(DL, Dependency Lookup) 해결 가능 +- 그런데 이렇게 하면 스프링 컨테이너에 종속적인 코드가 됨 +- 단위 테스트도 힘들어짐 +- 그래서 스프링이 뭔가 해줌! +### `ObjectProvider`를 이용한 해결책 +```java +@Autowired +private ObjectProvider prototypeBeanProvider; + +public int logic() { + PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); + prototypeBean.addCount(); + int count = prototypeBean.getCount(); + return count; +} +``` +- `ac.getBean` 대신 `prototypeBeanProvider.getObject` 사용 +- 대신 조회해주는 대리자 역할을 함 +- 스프링에 의존적이지만 기능이 단순해 단위테스트나 mock 만들기 쉬워짐 +- 옵션, 스트림 처리등 편의 기능이 많음 +- 별도의 라이브러리 필요 없음 +### `Provider`를 이용한 해결책 +```java +@Autowired +private Provider provider; + +public int logic() { + PrototypeBean prototypeBean = provider.get(); + prototypeBean.addCount(); + int count = prototypeBean.getCount(); + return count; +} +``` +- `javax.inject.Provider`(스프링 3.0은 `jakarta.inject.Provider`)라는 자바 표준 이용 +- 스프링부트 3.0 미만이면 `javax.inject:javax.inject:1` 라이브러리를 gradle에 추가해야 함 +- 스프링부트 3.0 이상이면 `jakarta.inject:jakarta.inject-api:2.0.1` 라이브러리를 gradle에 추가해야 함 +- `get()` 메서드 하나로 기능이 매우 단순 +- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있음! +- 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 이걸 이용해라 + +## 웹 스코프 +### 특징 +- 웹 환경에서만 동작 +- 스프링이 해당 스코프의 종료시점까지 관리 +### 종류 +- request + - HTTP 요청 하나가 들어오고 나갈 때 까지 유지 + - 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성, 관리됨 +- session + - HTTP Session과 동일한 생명주기를 가지는 스코프 +- application + - `ServletContext`와 동일한 생명주기를 가지는 스코프 +- websocket + - 웹 소켓과 동일한 생명주기를 가지는 스코프 +### `request` 스코프 예제 +1. 준비 +```java +implementation 'org.springframework.boot:spring-boot-starter-web' +``` +- build.gradle에 라이브러리 추가 +- `AnnotationConfigServletWebServerApplicationContext` 기반으로 구동됨 +2. 이용하는 경우 +- 동시에 여러 HTTP 요청이 왔을 때, 정확히 어떤 요청이 남긴 로그인지 구분하기 위해 사용 +- request scope를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘긴다면, 파라미터가 많아서 지저분해짐 +- 또한 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게 됨 +- 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋음 +3. 구현 기능 +- 기대하는 공통 포멧: [UUID][requestURL] {message} +- UUID를 사용해서 HTTP 요청을 구분하자. +- requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 남은 로그인지 확인하자. +4. 코드 +```java +@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); + } +} +``` +- `@Scope(value = "request")` 를 사용해서 request 스코프로 지정 +- 이 빈은 HTTP 요청 당 하나씩 생성, HTTP 요청이 끝나는 시점에 소멸 +- 생성되는 시점에 `@PostConstruct`를 사용해 uuid를 생성해서 저장 +- 소멸되는 시점에 `@PreDestroy` 를 사용해서 종료 메시지 남김 +- `requestURL`은 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받음 +```java +@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"; + } +} +``` +- 테스트용 컨트롤러 +- `HttpServletRequest`를 통해서 요청 URL을 받음 +- `requestURL` 값을 `myLogger`에 저장해둠 +- 컨트롤러에서 `controller test`라는 로그를 남김 +``` +Error creating bean with name 'myLogger': Scope 'request' is not active for the + current thread; consider defining a scoped proxy for this bean if you intend to + refer to it from a singleton; + +``` +- 리퀘스트가 오고 나서부터 나갈 때까지가 request 스코프 빈의 생존 범위인데, 리퀘스트가 없으니 오류 나옴 +- Provider 사용하자! +```java +@Controller +@RequiredArgsConstructor +public class LogDemoController { + + private final LogDemoService logDemoService; + private final ObjectProvider 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"; + } +} +``` +```java +@Service +@RequiredArgsConstructor +public class LogDemoService { + + private final ObjectProvider myLoggerProvider; + + public void logic(String id) { + MyLogger myLogger = myLoggerProvider.getObject(); + myLogger.log("service id = " + id); + } +} +``` +- `main()` 메서드로 스프링을 실행, 웹 브라우저에 `http://localhost:8080/log-demo` 를 입력 +``` +[d06b992f...] request scope bean create +[d06b992f...][http://localhost:8080/log-demo] controller test +[d06b992f...][http://localhost:8080/log-demo] service id = testId +[d06b992f...] request scope bean close +``` +- `ObjectProvider` 덕분에 `ObjectProvider.getObject()`를 호출하는 시점까지 request scope 빈 생성 요청 지연 +- `ObjectProvider.getObject()`를 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope 빈 의 생성이 정상 처리 +- `ObjectProvider.getObject()`를 `LogDemoController`, `LogDemoService`에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈 반환 + +## 프록시 +### 코드 +```java +@Component +@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) +public class MyLogger { +} +``` +- 적용 대상이 인터페이스가 아닌 클래스면 `TARGET_CLASS`를, 인터페이스면 `INTERFACES`를 선택 +```java +@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"; + } +} +``` +```java +@Service +@RequiredArgsConstructor +public class LogDemoService { + + private final MyLogger myLogger; + + public void logic(String id) { + myLogger.log("service id = " + id); + } +} +``` +- `Provider`를 사용하지 않았는데 잘 동작함 +- `CGLIB`라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한 것! +- 스프링 컨테이너가 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용 +- MyLogger를 상속받은 가짜 프록시 객체를 생성해 대신 등록 +- `ac.getBean("myLogger", MyLogger.class)` 로 조회해도 프록시 객체가 조회됨 +- 의존관계에도 이 가짜 프록시 객체가 주입됨 +- 가짜 프록시 객체에 요청이 오면 내부에서 진짜 빈을 요청하는 위임 로직이 들어있음 +- 클라이언트가 `myLogger.log()`을 호출하면 사실은 가짜 프록시 객체의 메서드가 호출됨 +- 가짜 프록시 객체가 request 스코프의 진짜 `myLogger.log()` 호출 +- 아무튼 지연이 중요하다! From aa23cd5da70ada3fb01ed55cce1f41e960762adc Mon Sep 17 00:00:00 2001 From: hyxrxn Date: Thu, 4 Apr 2024 16:47:47 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=95=84=ED=86=A0=20=EC=84=B9=EC=85=98=209?= =?UTF-8?q?=20=EA=B0=9C=ED=96=89=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "section9/\354\225\204\355\206\240.md" | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git "a/section9/\354\225\204\355\206\240.md" "b/section9/\354\225\204\355\206\240.md" index 76c65c2..33e0fbc 100644 --- "a/section9/\354\225\204\355\206\240.md" +++ "b/section9/\354\225\204\355\206\240.md" @@ -101,6 +101,7 @@ public void prototypeBeanFind() { @Scope("prototype") static class PrototypeBean { + @PostConstruct public void init() { System.out.println("PrototypeBean.init"); @@ -345,9 +346,7 @@ public class MyLogger { } public void log(String message) { - System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + - - message); + System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message); } @PostConstruct @@ -394,7 +393,6 @@ public class LogDemoController { Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; - ``` - 리퀘스트가 오고 나서부터 나갈 때까지가 request 스코프 빈의 생존 범위인데, 리퀘스트가 없으니 오류 나옴 - Provider 사용하자!