빈 스코프는 빈이 존재할 수 있는 범위를 말한다.
스프링은 다양한 스코프를 지원한다.
스코프 이름 | 존재 범위 |
---|---|
싱글톤 | 컨테이너 시작 ~ 종료 |
프로토타입 | 컨테이너는 프로토타입 빈의 생성만 관여하고, 그 이후는 관리하지 않음 |
웹(request) | 웹 요청 받음 ~ 웹 요청 나감 |
웹(session) | 웹 세션 생성 ~ 종료 |
빈 스코프는 다음과 같이 지정할 수 있다.
// 컴포넌트 스캔
@Scope("prototype")
@Componenet
public class AutoAppConfig {}
// 수동
@Scope("prototype")
@Bean
public BoardPolicy boardPolicy(){
return new ReadOnlyPolicy();
}
싱글톤 스코프의 빈을 조회하면, 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
하지만, 프로토타입 스코프의 빈을 조회하면 항상 새로운 인스턴스의 빈을 반환한다.
컨테이너: 프로토타입 빈의 생성 및 초기화 담당
클라이언트: 관리 및 종료 담당
싱글톤 빈이 프로토타입 빈에 의존한다면 문제가 발생할 수 있다.
다음과 같은 상황을 보자.
프로토타입 빈은 addOne()
이라는 메소드를 가지고 있다.
@Scope("prototype")
static class PrototypeBean {
private static int num = 0;
public static int getNum() {
return num;
}
public static void addOne() {
num++;
}
}
클라이언트 A가 프로토타입 빈을 컨테이너로부터 받은 뒤, addOne()을 호출한다.
결과는 1이다.
그 다음에 클라이언트 B가 프로토타입 빈을 컨테이너로부터 받은 뒤, addOne()을 호출한다.
프로토타입 빈은 요청받을 때마다 새롭게 생성되기 때문에 결과는 1이다.
SingletonBean 은 PrototypeBean을 주입받는다.
@Scope("singleton") // 생략가능
static class SingletonBean {
private final PrototypeBean prototypeBean;
@Autowired
public SingltonBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
PrototypeBean.addOne();
return PrototypeBean.getNum();
}
}
SingletonBean은 컨테이너가 생성될 때 함께 생성되고 의존관계 주입도 발생한다.
스프링 컨테이너는 프로토타입 빈을 생성하여 SingletonBean에 주입한다.
클라이언트 A가 SingletonBean을 받아서
logic()
메소드를 호출한다.
1이 리턴된다.
그 다음에 클라이언트 B가 SingletonBean을 받아서logic()
메소드를 호출한다.
2가 리턴된다.
SingletonBean 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.
따라서, 클라이언트 A와 B는 동일한 프로토타입 빈의 메소드를 사용하게 된다!
개발자가 의도한 것은 이것이 아닐것이다.
해결방법은 여러가지가 있다.
1. logic()
메소드 안에서 프로토타입 빈 주입
2. ObjectFactory, ObjectProvider
3. JSR-330 Provider
가장 간단한 방법은 싱글톤 빈이 프로토타입 빈을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.
@Autowired
private ApplicationContext ac;
// 컨테이너 자체를 주입받음
public int logic(){
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
// logic 메소드를 실행할 때마다 새로운 빈을 가져옴
prototypeBean.addOne();
return prototypeBean.getNum();
}
의존관계를 외부에서 주입(Dependency Injection) 받는게 아니라, 필요한 의존관계를 직접 찾는 것을 의존관계 조회(Dependency Lookup) 라고 한다.
위의 방법은 간단하지만, 단점도 존재한다.
컨테이너 자체를 DI 하면 스프링 컨테이너에 종속적인 코드가 되어버린다.
DL 정도의 기능만 제공하는 무언가가 필요하다.
ObjectProvider는 지정한 빈을 컨테이너에서 찾아주는 DL 기능을 제공한다.
ObjectFactory는 ObjectProvider에서 편의 기능을 추가한 것이다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
// 스프링이 자동으로 ObjectProvider를 주입해줌
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
// DI
prototypeBean.addOne();
return prototypeBean.getNum();
}
마지막 방법은 JSR-330 자바 표준을 사용하는 방법이다.
JSR-330 Provider는 자바 표준이기 때문에 스프링에 의존적이지 않고, 아주 단순한 기능(DL)만 제공한다.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addOne();
return prototypeBean.getNum();
}
get()
메소드 하나만 가지고 있기 때문에 매우 단순하다.웹 스코프는 웹 환경에서만 동작한다.
웹 스코프의 종류는 다음과 같다.
웹 스코프 | 존재 범위 |
---|---|
request | HTTP 요청 받음 ~ HTTP 요청 나감 |
session | HTTP session와 동일한 생명 주기 |
application | ServletContext와 동일한 생명 주기 |
websocket | 웹 소켓과 동일한 생명 주기 |
대표적인 웹 스코프인 request에 대해서 알아보도록 하자.
클라이언트가 특정 url을 요청하면 LogController
클래스가 해당 요청을 받는다.
LogController
클래스는 MyLog
객체를 활용해서 클라이언트의 로그를 출력하고, LogService
객체를 활용해서 서비스 계층에서 로그를 출력한다.
다음의 로직을 한번 살펴보자.
LogController
가 해당 요청을 받아서 URL을 MyLog
객체에 저장해둔다.LogController
가 로그를 남긴다.LogService
의 로직을 호출해서, 서비스 계층에서도 로그를 출력한다.요청된 URL 저장 및 로그 출력을 하는 MyLog
클래스
@Component
@Scope(value = "request")
public class MyLog {
private String uuid;
private String request;
public void setRequest(String request) {
this.request = request;
}
public void printLog(String message) {
System.out.println("[" + uuid + "]" + "[" + request + "] " + message);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] initiate bean:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] close bean:" + this);
}
}
비즈니스 로직을 실행하는 LogService
클래스
@Service
@RequiredArgsConstructor
public class LogService {
private final MyLog myLog;
// MyLog 타입의 빈이 여러개 있어도, 스프링이 각 클라이언트에 맞는 MyLog 빈을 넣어줌
public void logic(String testId) {
myLog.printLog(testId);
}
}
클라이언트로부터 받은 요청을 처리하는 LogController
클래스
@Controller
@RequiredArgsConstructor
public class LogController {
private final MyLog myLog;
private final LogService logService;
@RequestMapping("server") // server라는 요청이 오면 실행되는 내용
@ResponseBody
public String requestLogic(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLog.setRequest(requestURL);
myLog.printLog("log from LogController class");
logService.logic("testId");
return "Test Pass";
}
}
위의 코드를 실행하면, 오류가 발생한다. 그 이유는 무엇일까?
애플리케이션이 실행되면, 스프링은 컨테이너를 생성하고 스프링 빈을 등록한다.
각 스프링 빈을 등록한 후에는 의존관계 주입을 한다.
LogController
클래스의 의존관계를 한번 보자.
public class LogController {
private final MyLog myLog;
private final LogService logService;
...
}
LogController
클래스는 MyLog 클래스에 의존한다.
하지만, MyLog 클래스는 request 스코프를 가지고 있다.
따라서, 클라이언트의 요청이 오기전까지 빈은 생성되지 않는다!!
해결방안은 2가지가 존재한다.
이전에 배운 ObjectProvider, JSR-330 Provider 등은, 프로토타입 스코프 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
ObjectProvider를 사용해서 request 스코프에서 발생한 문제를 해결해보자.
앞선 문제점은 생성되지 않은 빈을 주입하려 했기 때문에 발생했다.
따라서, 빈 대신에 Provider를 주입한 뒤 클라이언트의 요청이 오면 Provider에서 DL을 통해 request 스코프의 빈을 사용하면 된다.
@Controller
@RequiredArgsConstructor
public class LogController {
private final ObjectProvider<MyLog> objectProvider; // 변경된 부분
private final LogService logService;
@RequestMapping("server")
@ResponseBody
public String requestLogic(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLog myLog = objectProvider.getObject(); // 변경된 부분
myLog.printLog("log from LogController class");
logService.logic("testId");
return "Test Pass";
}
}
@Service
@RequiredArgsConstructor
public class LogService {
private final ObjectProvider<MyLog> objectProvider; // 변경된 부분
public void logic(String testId) {
MyLog myLog = objectProvider.getObject(); // 변경된 부분
myLog.printLog(testId);
}
}
코드를 작성하다가
getObject()
메소드의 파라미터가 없다는게 이상해 보였다.
MyLog
타입의 빈이 여러개가 있다면getObject()
로 클라이언트에 맞는 빈을 꺼내야 할텐데...
이것에 대한 답은 다음 게시물에서 확인할 수 있었다.
인프런 게시물
Request 스코프의 빈은 클라이언트의 요청이 발생할 때 생성된다.
각 요청은 Request 스코프의 빈을 가지고 다닌다고 보면 된다.
해당 요청에서getObject()
메소드를 호출하면 클라이언트 간의 요청을 구분할 필요 없이, 자신의 요청의 Attributes 중에서 MyLog 타입의 빈을 꺼내는 것이다.
위의 코드는 오류없이 잘 동작하는 것을 확인할 수 있다.
getObject()
메소드를 호출하는 시점까지 request scope 빈의 생성을 지연한다.이전의 문제점을 다시 한번 생각해보자.
앞선 문제점은 생성되지 않은 빈을 주입하려 했기 때문에 발생했다.
그럼 가짜 객체를 주입한 후에 요청이 오면 그때 내부에서 진짜 빈을 요청하도록 만들면 되지 않을까?
이 가짜 객체가 바로 프록시이다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
// 프록시 객체의 사용 방법
public class MyLog { ... }
// 원본 코드
@Controller
@RequiredArgsConstructor
public class LogController {
private final MyLog myLog;
private final LogService logService;
@RequestMapping("server")
@ResponseBody
public String requestLogic(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLog.setRequest(requestURL);
myLog.printLog("log from LogController class");
logService.logic("testId");
return "Test Pass";
}
}
myLog.printLog()
메소드를 호출하면 가짜 프록시 객체인 myLog는 request 스코프의 진짜 메소드를 호출한다.Provider나 프록시의 핵심은 객체 조회의 "지연처리"이다.
스프링 사용이유 -> DI(의존성 주입)
빈을 싱글톤으로 유지시켜준다.