본문 바로가기

Spring

Spring- 빈 스코프(웹 스코프)

웹 스코프

 - @Scope("request") 리퀘스트 스코프

 리퀘스트 스코프는 HTTP 요청이 들어온 직후에 생성되어 나갈때까지 유지되는 스코프로써, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성된다.

리퀘스트 스코프는 클라이언트마다 할당되는 객체가 다름

다시말해 클라이언트 A와 클라이언트 B가 각각 HTTP 요청을 줬을 때, A 전용 빈과 B 전용 빈이 생성되어 각각 할당 된다는 것이다. 결국 이는 다시말해 HTTP 요청이 들어오지 않으면 빈이 생성되지 않는다는 말이기도 하다.

 

 그렇다면 만일 싱글톤에 리퀘스트 스코프가 의존관계 주입이 되는 경우 어떤 문제가 발생할까?

 싱글톤에 리퀘스트 스코프 의존관계 주입

 request scope 클래스 MyLogger.java

package hello.core.common;

@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 bean create: " + this);
    }

    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request bean close: " + this);
    }
}

 service 로직 LogDemoService (에러 발생.ver) 

package hello.core.web;

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;  //싱글톤에 리퀘스트 스코프가 주입

    public void logic(String id){
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

controller 로직 LogDemoController.java (에러 발생.ver)

package hello.core.web;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;  //싱글톤에 리퀘스트 스코프가 주입

    @RequestMapping("log-demo")
    @ResponseBody       // HttpServletRequest를 통해서 요청 URL을 받음
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        // myLogger에 URL 저장.
        // myLogger는 request scope이기 때문에 HTTP 요청마다 각각 구분되므로 값이 섞이지않음

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

위 세개의 코드는 웹 "localhost:8080/log-demo"에 접속했을 시 접속 로그를 출력하는 코드이다. 여기서 보면 MyLogger는 리퀘스트 스코프이다. 또한 싱글톤인 LogDemoService와 LogDemoController에 주입되었다. 문제는 싱글톤 빈이 생성될 때 내부의 리퀘스트 빈은 아직 HTTP 요청이 들어오지 않아 빈이 생성되지 않았다는 사실이다. 따라서 싱글톤 빈에 주입해줄 객체가 존재하지 않아서 오류가 발생한다. 실제로 스프링 coreApplication을 실행 시켜보면 이 코드에서는 에러가 발생한다.

 이 문제를 해결하기 위해서 ObjectProvider로 DL을 하는 방법을 사용할 수 있다. 

 싱글톤에 리퀘스트가 주입될 때 주입할 빈이 없는 문제 해결하기

 - 1) ObjectProvider로 DL을 적용하여 정상적으로 작동시키기

사실 별건 없고 LogDemoController.java와 LogDemoService.java에 "ObjectProvider<MyLogger>로 DL"만 해주면 된다.

package hello.core.web;

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerProvider;

    public void logic(String id){
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}
package hello.core.web;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    // ObjectProvider가 없으면 MyLogger는 request scope이기 때문에 logDemoController 빈이
    // 생성될 때 myLogger 빈은 아직 존재하지 않아 의존관계 주입이 불가함.
    // 따라서 ObjectProvider에 MyLogger를 넣어 놓고 HTTP 요청이 들어올 때마다 꺼내어 사용하면 문제 해결.
    private final ObjectProvider<MyLogger> myLoggerProvider;

    @RequestMapping("log-demo")
    @ResponseBody       // HttpServletRequest를 통해서 요청 URL을 받음
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        // myLogger에 URL 저장.
        // myLogger는 request scope이기 때문에 HTTP 요청마다 각각 구분되므로 값이 섞이지않음

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

 주석에서 이미 설명을 다 해 놓았다. 간단히 말해 ObjectProvider에 MyLogger를 넣어놓고 HTTP 요청이

들어올 때마다 ( = 리퀘스트 빈이 생성될 때마다) 꺼내어서 사용하는 것이다. 이런식으로 구현하면 빈이 존재 할 때만 리퀘스트 스코프 빈(myLogger)를 꺼내어서 사용할 수 있으니 에러가 발생하지 않고 정상적으로 동작하게 된다.

 - 2) 프록시를 사용하기 (주로 사용)

package hello.core.common;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;

@Component //프록시 사용
@Scope(value = "request",proxyMode = ScopedProxyMode.TARGET_CLASS )
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 bean create: " + this);
    }

    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request bean close: " + this);
    }
}

  @Scope( proxyMode = ScopedProxyMode.TARGET_CLASS )로 함으로써 해당 클래스 빈을 프록시(임시객체)로 채워놓는다. 이렇게 하면 실제로는 가짜 프록시 객체를 집어 넣어논 후 HTTP 요청이 들어왔을 때 생성된 myLogger 객체를 찾아 넣어준다.

 

 결국 provider든 프록시든 핵심 아이디어는 진짜 객체 조회가 필요한 시점까지 "지연처리"를 한다는 것이다.

'Spring' 카테고리의 다른 글

Spring -Dto, Vo 개념 정리  (0) 2024.01.01
Spring- @xxxToOne  (0) 2023.02.07
Spring- 빈 스코프(싱글톤, 프로토타입)  (0) 2023.02.02
Spring- 빈 생명 주기  (0) 2023.02.02
Spring- 조회한 빈이 모두 필요할 때(Map, List)  (0) 2023.01.31