반응형
# 스프링 핵심 원리 기본편
# 프로토타입 스코프_싱글톤 빈과 함께 사용 시 Provider로 문제 해결
- 싱글톤 빈과 프로토타입 빈을 함께 사용할 때 마다 항상 새로운 프로토타입 빈읋 생선하는 방법 중 가장 간단한 방법으로 '스프링 컨테이너에 요청'하는 방법이 있다.
- 의존 관계를 외부에서 주입(DI) 받는게 아닌, 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색) 이라고 한다.
- 스프링의 애플리케이션 컨텍스트 전체를 주입받게 된다면, 스프링 컨테이너에 종속적인 코드가 되고, 그로 인해 단위 테스트도 어려워지게 된다.
## ObjectFactory, ObjectProvider
- ObjectProvider : 지정한 빈을 컨테이너에 대신 찾아주는 DL 서비스를 제공하는 것.
- 과거에 ObjectFactory 존재했는데, ObjectFactory에 편의 기능이 추가된 것이ObjectProvider 이다.
- SingletonWithPrototypeTest1
package hello.core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
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.getCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.getCount();
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 ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
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 destory() {
System.out.println("PrototypeBean.destory");
}
}
}
- ObjectProvider의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반 환한다.(DL)
- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
- ObjectProvider 는 지금 딱 필요한 DL 정도의 기능만 제공한다
- 특징
ObjectFactory : 기능 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
ObjectProvider : ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
## JSR-330 Provider
- build.gradle
plugins {
id 'org.springframework.boot' version '2.4.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
//lombok 설정 추가 시작
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
//lombok 설정 추가 끝
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'javax.inject:javax.inject:1'
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
- SingletonWithPrototypeTest1
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
- 특징
get() 메서드 하나로 기능이 매우 단순하다.
별도의 라이브러리가 필요하다.
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
## 정리
- 실무에서 웹 애플리케이션 개발 시, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
- ObjectProvider, JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우 언제든지 사용할수 있다.
- 참고 : 실무에서 자바 표준인 JSR-330 Provider를 사용할 것인지, 아니면 스프링이 제공하는 ObjectProvider를 사용할 것인지 고민.
- ObjectProvider는 DL을 위한 편의 기능을 많이 제 공해주고 스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 편리하다. 만약 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야한다.
- 스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다. 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용 할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다.
# 웹 스코프
- 싱글톤 : 스프링 컨테이너의 시작과 끝
- 프로토타입 : 생성과 의존관계를 주입, 초기화까지만 진행하는 특별한 스코프
## 웹 스코프의 특징
- 웹 환경에서만 동작한다.
- 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.
## 웹 스코프의 종류
- request : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
- session : HTTP Session과 동일한 생명주기를 가지는 스코프
- application : 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
- websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프
## HTTP request 요청 당 각각 할당되는 request 스코프
# request 스코프 예제 만들기
- 웹 스코프는 웹 환경에서 동작, web 환경이 동작할 수 있도록 라이브러리를 추가해야 한다.
- build.gradle에 추가
//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
- 참고 : spring-boot-starter-web 라이브러리를 추가하면 스프링 부트는 내장 톰켓 서버를 활용해서 웹 서버와 스프링을 함께 실행시킨다.
- 스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext를 기반으로 애플리케이션을 구동한다. 웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServerApplicationContext를 기반으로 애플리케이션을 구동한다.
- 기본 포트인 8080 포트가 오류가 발생하면(다른곳에서 사용할 경우 등) 포트를 변경해야 한다. 9090으로 변경하기위해 main/resources/application.properties 내에 아래와 같이 작성한다.
server.port=9090
## request 스코프 예제 개발
- 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴때 사용하기 딱 좋은것이 바로 request 스코프이다
- MyLogger
package hello.core.common;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;
@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);
}
}
- 로그를 출력하기 위한 MyLogger 클래스
- @Scope(value = "request") 를 사용 request 스코프로 지정, 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
- 해당 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용 uuid를 생성해서 저장해 둔다. 이 빈은 HTTP 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다. 이 빈이 소멸되는 시점에 @PreDestroy 를 사용해서 종료 메시지를 남긴다. requestURL은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.
- LogDemoController
package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.web.LogService.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@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.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- 로거가 잘 작동하는지 확인하는 테스트용 컨트롤러
- HttpServletRequest를 통해 요청 URL을 받는다. requestURL의 값은 http://localhost:8080/log-demo
- 받은 requstURL 값을 myLogger에 저장. myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 된다.
- LogDemoService
package hello.core.web.LogService;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
- 비즈니스 로직이 있는 서비스 계층 로그 출력
- request scope를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘긴다면, 파라미터가 많아서 지저분해진다. 더 큰 문제는 requestURL 같은 웹 관련 정보가 웹과 관련없는 서비스 계층까지 넘어가게 된다는 것이다. 웹과 관련된 부분은 컨트롤러까지만 사용해야 한다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
- requst scope의 MyLogger를 이용해 파라미터로 넘기지 않고, MyLogger의 멤버변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.
- 현재 위 코드들은 오류 발생.
# 스코프와 Provider
- LogDemoController
package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.web.LogService.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerObjectProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
MyLogger myLogger = myLoggerObjectProvider.getObject();
String requestURL = request.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- LogDemoService
package hello.core.web.LogService;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
- ObjectProvider 덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
- ObjectProvider.getObject()를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.
- ObjectProvider.getObject()를 LogDemoController, LogDemoService에서 각각 한번씩 다로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다.
출처 : 인프런 스프링 핵심 원리 기본편
반응형
'인프런 강의 학습 > 스프링 핵심 원리(기본편)' 카테고리의 다른 글
재학습_1일차 객체 지향 설계와 스프링 (0) | 2022.02.06 |
---|---|
스프링 핵심 원리 기본편 24일차 (0) | 2021.03.03 |
스프링 핵심 원리 기본편 22일차 (0) | 2021.02.24 |
스프링 핵심 원리 기본편 21일차 (0) | 2021.02.20 |
스프링 핵심 원리 기본편 20일차 (0) | 2021.02.18 |