빈 스코프
- 김영한님의 스프링 핵심 원리 강의에서 빈 스코프의 종류와 특징, 프로토타입 스코프와 싱글톤 스코프의 차이, 웹 스코프의 활용 방법,
Provider와 프록시를 이용한 문제 해결 방법을 정리함
빈 스코프 개념
스코프(Scope)
- 빈이 존재할 수 있는 범위
스코프 종류
- 기본 스코프
- 싱글톤(
Singleton)- 스프링 컨테이너의 시작부터 종료까지 유지되는 가장 넓은 범위
- 프로토타입(
Prototype)- 생성과 의존관계 주입까지만 관여하는 짧은 범위
- 싱글톤(
- 웹 스코프
Request- 웹 요청이 들어오고 나갈 때까지 유지
Session- 웹 세션 생성부터 종료까지 유지
Application- 서블릿 컨텍스트와 같은 범위로 유지
WebSocket- 웹 소켓과 동일한 생명주기
스코프별 사용 시기
| 스코프 | 사용 시기 |
|---|---|
| 싱글톤 | 대부분의 경우 (기본값) |
| 프로토타입 | 매번 새로운 객체가 필요할 때 (실무에서 드묾) |
Request |
HTTP 요청별로 다른 인스턴스가 필요할 때 |
Session |
사용자 세션별로 다른 인스턴스가 필요할 때 |
스코프 지정 방법
컴포넌트 스캔 자동 등록
1
2
3
@Scope("prototype")
@Component
public class HelloBean {}
수동 등록
1
2
3
4
5
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
프로토타입 스코프
싱글톤과 프로토타입 동작 비교
- 싱글톤 빈
- 스프링 컨테이너에 요청 시 항상 같은 인스턴스 반환
- 컨테이너 생성 시점에 초기화
- 컨테이너 종료 시
@PreDestroy호출됨
- 프로토타입 빈
- 스프링 컨테이너에 요청 시 항상 새로운 인스턴스 생성 및 반환
- 빈 조회 시점에 생성 및 초기화
- 컨테이너가 생성, 의존관계 주입, 초기화까지만 관여
@PreDestroy같은 종료 메서드 호출되지 않음- 클라이언트가 직접 관리 책임
싱글톤 스코프 예제
코드
1
2
3
4
5
6
7
8
9
10
11
12
@Scope("singleton")
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
실행 결과
1
2
3
4
SingletonBean.init
singletonBean1 = hello.core.scope.SingletonBean@54504ecd
singletonBean2 = hello.core.scope.SingletonBean@54504ecd
SingletonBean.destroy
- 같은 인스턴스 반환
-
종료 메서드 호출됨
- 전체 코드 보기
프로토타입 스코프 예제
코드
1
2
3
4
5
6
7
8
9
10
11
12
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
실행 결과
1
2
3
4
5
6
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeBean@302f7971
- 다른 인스턴스 반환
-
destroy호출 안됨 - 전체 코드 보기
프로토타입과 싱글톤 함께 사용 시 문제점
문제 상황
- 싱글톤 빈이 프로토타입 빈을 의존관계 주입으로 사용하면
- 싱글톤 빈 생성 시점에 프로토타입 빈이 주입됨
- 이후 같은 프로토타입 빈 인스턴스를 계속 사용
- 매번 새로운 인스턴스를 사용하려는 의도와 다름
문제 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
static class ClientBean {
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
ClientBean은 싱글톤이므로 생성 시점에 한 번만 주입받음- 이후
logic()호출 시 같은prototypeBean사용 -
count가 계속 증가하는 문제 발생 - 전체 코드 보기
Provider 패턴으로 해결
ObjectProvider 사용
1
2
3
4
5
6
7
8
9
10
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
- 특징
getObject()호출 시점에 스프링 컨테이너에서 빈 조회 (DL: Dependency Lookup)- 매번 새로운 프로토타입 빈 생성
- 스프링에 의존적이지만 기능이 단순하고 편리
JSR-330 Provider 사용
-
의존성 추가 필요
1
implementation 'javax.inject:javax.inject:1'
1 2 3 4 5 6 7 8 9 10
static class ClientBean { @Autowired private Provider<PrototypeBean> provider; public int logic() { PrototypeBean prototypeBean = provider.get(); prototypeBean.addCount(); return prototypeBean.getCount(); } }
-
특징
- 자바 표준 (JSR-330)
get()메서드 하나로 단순- 스프링이 아닌 다른 컨테이너에서도 사용 가능
- 별도 라이브러리 필요
웹 스코프
Request 스코프 특징
- HTTP 요청 당 하나씩 생성
- HTTP 요청이 끝나는 시점에 소멸
- 각 요청마다 별도의 빈 인스턴스 생성 및 관리
- 종료 메서드 호출됨
Request 스코프 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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);
}
}
문제점
1
Error creating bean with name 'myLogger': Scope 'request' is not active
Request스코프 빈은 실제 HTTP 요청이 와야 생성 가능-
애플리케이션 실행 시점에 오류 발생
- 전체 코드 보기
Provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> 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";
}
}
1
2
3
4
[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
프록시 모드
-
프록시 설정
1 2 3 4 5
@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class MyLogger { // 코드 동일 }
-
프록시 모드 옵션
TARGET_CLASS- 적용 대상이 클래스인 경우
INTERFACES- 적용 대상이 인터페이스인 경우
-
프록시 사용 시
Controller와Service1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
@Controller @RequiredArgsConstructor public class LogDemoController { private final LogDemoService logDemoService; private final MyLogger myLogger; // Provider 없이 직접 주입 @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"; } }
1 2 3 4 5 6 7 8 9
@Service @RequiredArgsConstructor public class LogDemoService { private final MyLogger myLogger; // Provider 없이 직접 주입 public void logic(String id) { myLogger.log("service id = " + id); } }
프록시 동작 원리
프록시 객체 확인
1
System.out.println("myLogger = " + myLogger.getClass());
1
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
동작 메커니즘
CGLIB라이브러리가MyLogger를 상속받은 가짜 프록시 객체 생성- 스프링 컨테이너에 가짜 프록시 객체 등록
- 의존관계 주입 시 가짜 프록시 객체 주입
- 클라이언트가
myLogger.log()호출 시- 가짜 프록시 객체의 메서드 호출
- 프록시 객체가 실제
Request스코프 빈의log()호출 (위임)
- 가짜 프록시 객체는 싱글톤처럼 동작하지만, 실제 빈은 요청마다 새로 생성
특징
- 클라이언트는 싱글톤 빈처럼 편리하게 사용
- 진짜 객체 조회를 필요한 시점까지 지연 처리
- 다형성과
DI컨테이너의 강점 활용 - 애노테이션 설정만으로 프록시 객체로 대체 가능
Provider와 프록시
| 구분 | ObjectProvider / JSR-330 Provider | 프록시 모드 |
|---|---|---|
| 코드 수정 | Provider 주입 필요 | 기존 코드 그대로 사용 |
| 편의성 | getObject()/get() 호출 필요 |
직접 사용 가능 |
| 설정 | 코드 수정 | 애노테이션만 추가 |
| 권장 | DL이 명시적으로 필요할 때 |
일반적인 경우 |
DI와 DL
DI (Dependency Injection)
- 의존관계를 외부에서 주입
DL (Dependency Lookup)
- 필요한 의존관계를 직접 찾아서 사용
연습 문제
-
프로토타입 스코프 빈의 파괴(destruction)는 누가 담당할까요?
a. 해당 빈을 요청한 클라이언트
- 프로토타입 빈은 컨테이너가 생성,
DI, 초기화까지만 담당하고 이후 관리를 클라이언트에게 넘기므로@PreDestroy같은 컨테이너의 파괴 메서드는 호출되지 않음
- 프로토타입 빈은 컨테이너가 생성,
-
여러 HTTP 요청이 동시에 들어올 때,
Request스코프 빈은 어떻게 동작할까요?a. 각 요청마다 별도의 인스턴스가 만들어짐
Request스코프는 HTTP 요청 사이클마다 완전히 별개의 인스턴스를 생성하고 관리하며, 요청 데이터를 분리할 수 있음
-
싱글톤 빈에서 프로토타입 빈을 의존성 주입(
DI)하면 어떤 문제가 발생할 수 있나요?a. 싱글톤 빈 안에서 항상 같은 프로토타입 인스턴스가 사용됨
- 싱글톤 빈은 컨테이너 시작 시점에 계속되므로
DI단 한 번만 이뤄지고, 이때 주입받은 프로토타입 빈 인스턴스를 싱글톤 빈이 계속 재사용하여 문제 발생
- 싱글톤 빈은 컨테이너 시작 시점에 계속되므로
-
싱글톤 빈에서 스코프가 짧은 빈(프로토타입,
Request등)을 새롭게 사용하기 위한Provider나Proxy방식의 핵심 원리는 무엇인가요?a. 필요한 시점까지 빈 조회/생성 지연
Provider나Proxy는 싱글톤 빈 생성 시 실제 스코프 빈 주입 대신 조회를 지연시켜 스코프 활성화 시점에 새로운 인스턴스를 얻거나 프록시를 통해 위임받아 사용할 수 있음
-
싱글톤 빈에서
Request스코프 빈을 사용할 때,Proxy방식을 통해 얻는 중요한 이점은 무엇인가요?a. 해당 빈을 사용하는 클라이언트 코드가 간결해짐
Proxy방식을 사용하면 싱글톤 빈은 마치 일반 빈을 사용하는 것처럼 편리하게Request스코프 빈의 메서드를 호출할 수 있고,Provider처럼getObject()를 명시적으로 호출할 필요가 없어 코드가 간결해짐
요약 정리
- 빈 스코프
- 빈이 존재할 수 있는 범위
- 싱글톤, 프로토타입,
Request,Session,Application,WebSocket
- 싱글톤
- 항상 같은 인스턴스, 컨테이너 종료 시까지 관리
- 프로토타입
- 매번 새로운 인스턴스, 생성/
DI/초기화까지만 관여
- 매번 새로운 인스턴스, 생성/
- 프로토타입 + 싱글톤 문제
- 싱글톤 빈이 프로토타입 빈 주입받으면 같은 인스턴스 계속 사용
Provider또는 프록시로 해결
Provider패턴ObjectProvider- 스프링 의존, 간편
JSR-330 Provider- 자바 표준, 라이브러리 필요
- 웹 스코프
Request- HTTP 요청마다 생성
Session- 웹 세션마다 생성
- 프록시 모드
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)CGLIB로 가짜 프록시 객체 생성- 싱글톤처럼 편리하게 사용 가능
- 실제 빈은 필요한 시점에 생성 (지연 처리)
- 주의사항
- 특별한 스코프는 꼭 필요한 곳에만 최소화해서 사용
- 프록시 모드는 싱글톤처럼 보이지만 실제로는 다르게 동작
- 무분별한 사용은 유지보수를 어렵게 만듦