WebScope
웹 환경에서만 동작하는 특별한 스코프인 웹 스코프의 특징과 활용법을 정리했습니다. 특히 request 스코프와 프록시 모드를 중심으로 알아보겠습니다.
1. 웹 스코프란? 📌
정의 💡 웹 환경에서만 동작하는 스코프로, HTTP 요청이나 세션 등의 생명주기와 연동되는 빈 스코프입니다.
웹 스코프 특징 ⚡
웹 환경에서만 동작 (웹 애플리케이션 컨텍스트 필요)
스프링이 해당 스코프의 종료 시점까지 관리 (프로토타입과 다름)
종료 시점에 @PreDestroy 같은 소멸 메서드가 실행됨
2. 웹 스코프 종류 🗂️
request 📨
HTTP 요청 하나가 들어오고 나갈 때까지 유지
각 HTTP 요청마다 별도의 빈 인스턴스 생성
session 🔑
HTTP 세션과 동일한 생명주기
사용자 단위로 빈이 관리됨
application 📱
서블릿 컨텍스트(ServletContext)와 동일한 생명주기
웹 애플리케이션당 하나의 빈이 생성됨
websocket 🔌
웹소켓 연결이 생성되고 종료될 때까지 유지
양방향 통신에서 사용
3. 웹 스코프 활용 예제 - request 스코프 📝
3.1 웹 환경 설정 ⚙️
의존성 추가 (Gradle) 📦
implementation 'org.springframework.boot:spring-boot-starter-web'
포트 변경 (선택 사항) 🔢
# application.properties
server.port=9090
3.2 request 스코프 활용 시나리오 🎯
문제 상황 🔍
여러 HTTP 요청이 동시에 들어올 때 각 요청을 구분해야 함
로그에 요청 정보를 포함하고 싶음
서비스 계층에 웹 관련 정보를 직접 전달하지 않고 싶음
기대하는 로그 형식 📋
[UUID][요청URL] 메시지
3.3 웹 스코프 예제 코드 구현 💻
MyLogger 클래스 (request 스코프 빈)
@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")
: 빈을 HTTP 요청당 하나씩 생성uuid
: 각 요청을 구분하는 고유 IDrequestURL
: 현재 요청한 URL 정보@PostConstruct
: 빈 생성 시 UUID 생성@PreDestroy
: 요청 종료 시 로그 출력
컨트롤러와 서비스
@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";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
3.4 실행 시 발생하는 문제점 ⚠️
에러 메시지 ❌
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 스코프는 HTTP 요청이 있어야 생성됨
애플리케이션 시작 시점에는 HTTP 요청이 없어 빈을 생성할 수 없음
싱글톤 빈(컨트롤러, 서비스)에 주입할 수 없음
4. 웹 스코프 문제 해결 방법 🛠️
4.1 Provider 사용 🔄
컨트롤러 수정
@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";
}
}
서비스 수정
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
Provider 방식 특징 💡
실제 빈 조회를 필요한 시점까지 지연(Lazy)
getObject()
호출 시점에는 HTTP 요청이 진행 중같은 HTTP 요청이면 같은 스프링 빈 반환
구현이 복잡해짐 (매번
getObject()
호출 필요)
4.2 프록시 방식 사용 🧩
MyLogger 수정 (프록시 모드 적용)
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
// 기존 코드와 동일
}
핵심 코드 📝
proxyMode = ScopedProxyMode.TARGET_CLASS
: 클래스 기반 프록시 생성인터페이스 기반이라면
ScopedProxyMode.INTERFACES
사용
컨트롤러와 서비스는 원래 코드로 복귀
@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";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger; // 프록시 주입
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
5. 프록시 동작 원리 🧠
5.1 CGLIB 프록시 객체
주입된 myLogger 클래스 확인
System.out.println("myLogger = " + myLogger.getClass());
출력 결과
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
5.2 프록시 작동 과정 🔄
스프링 컨테이너는 CGLIB 라이브러리를 사용해 MyLogger를 상속받은 가짜 프록시 객체 생성
스프링 컨테이너에 "myLogger"라는 이름으로 프록시 객체를 등록
의존관계 주입 시 가짜 프록시 객체가 주입됨
프록시 객체는 요청이 올 때까지 대기
실제 요청 시 프록시 객체의 메서드가 호출되면 내부에서 진짜 request 스코프 빈을 요청
프록시 객체는 원본 클래스를 상속받았기 때문에 클라이언트는 원본과 동일하게 사용 가능
Client -> Proxy Object -(위임)-> 실제 MyLogger 빈 (request 스코프)
5.3 특징 요약 📊
프록시 객체 🎭
싱글톤처럼 동작하는 가짜 객체
내부에 진짜 객체를 찾는 위임 로직만 존재
실제 요청이 오면 그때 진짜 빈을 요청
지연 로딩 ⏳
Provider와 프록시 모두 실제 객체 조회를 필요한 시점까지 지연
프록시는 설정만으로 지연 로딩 구현 가능
다형성 활용 🧩
애노테이션 설정만으로 원본 객체를 프록시로 대체
DI 컨테이너의 강점
6. 실무 활용 가이드 💼
6.1 주의 사항 ⚠️
마치 싱글톤처럼 보이지만 다르게 동작
특별한 스코프는 꼭 필요한 곳에만 최소화해서 사용
무분별한 사용은 유지보수를 어렵게 함
6.2 활용 사례 🎯
로그 추적기 (각 HTTP 요청별 처리 과정 추적)
사용자별 설정 정보 (세션 스코프)
공통 웹 애플리케이션 설정 (application 스코프)
실시간 통신 상태 관리 (websocket 스코프)
6.3 선택 가이드 🧭
Provider 방식
명시적인 코드를 선호하는 경우
빈을 지연해서 조회하는 시점을 정확히 제어하고 싶을 때
프록시 방식
코드를 간결하게 유지하고 싶을 때
기존 코드를 수정하지 않고 스코프 동작을 변경하고 싶을 때
7. 핵심 요약 📚
웹 스코프
웹 환경에서만 동작하는 특별한 스코프
스프링이 생성부터 소멸까지 관리
종료 시점에 @PreDestroy 호출됨
request 스코프
HTTP 요청마다 별도의 빈 인스턴스 생성
웹 관련 정보를 모든 계층에 파라미터로 전달하지 않아도 됨
코드 계층 분리와 유지보수에 유리
활용 전략
Provider로 지연 로딩
프록시로 간결한 코드 유지
필요한 곳에만 최소화해서 사용
Last updated