Back-End/Spring

[Spring][스프링 기본편] - 27. 웹 스코프

얄루몬 2022. 2. 7. 14:14

💻본 포스팅은 '스프링 핵심 원리 - 기본편 김영한'님의 강의를 듣고 작성되었습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com


1. 웹 스코프란?

지금까지는 싱글톤 스코프, 프로토타입 스코프를 학습했다.

싱글톤은 스프링 컨테이너의 시작과 끝까지 함께하는 매우 긴 스코프이다.

프로토타입 스코프는 생성과 의존관계 주입, 그리고 초기화까지만 진행하는 특별한 스코프이다.

 

싱글톤 스코프 프로토타입 스코프 웹 스코프
- 스프링 컨테이너의 생성부터 종료까지 함께하는 긴 스코프이다.

-하나의 빈을 유지하며 싱글톤 상태를 유지한다.
- 빈의 생성, 의존관계 주입, 그리고 초기화까지만 진행하는 짧은 주기의 스코프이다.

- 매번 요청마다 새로운 빈을 제공한다.
- 웹 환경에서만 동작한다.

- 해당 스코프의 종료시점까지 관리를 해준다.

- Http 요청마다 새로운 빈을 제공하지만 같은 Http의 사용이라면 만들어 놓은 빈을 계속해서 사용한다. 

request scope

 

 

 

2. 웹 스코프의 특징

  • 웹 스코프는 웹 환경에서만 동작한다.

 

  • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리를 해준다. 
    • 따라서 종료 메서드가 호출이 된다.

 

 

 

3. 웹 스코프의 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

 

 

 

4. 웹 스코프 예제

[웹 환경 추가]

웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가하자.

build.gradle에 추가 //web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web

이후 mina 메서드를 실행하면 웹 애플리케이션이 실행되는 것을 확인할 수 있다.

 

 

[웹 스코프를 사용하는 경우는?]

  • 동시에 여러 HTTP 요청이 올 때 정확하게 어디서 남긴 로그인지를 구분하기 어렵다. 
    • 그럴 때 그 로그를 구분하기 위해서는 request 스코프를 사용해서 확인하면 좋다.

 

 

[로그 출력을 위한 클래스]

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;


//로그 출력을 위한 클래스 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를 사용해서 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸될 것이다.
  • requestURL은 이 빈이 생성되는 시점에는 알 수 없기 때문에, 외부에서 setter로 입력 받는다.

 

[스프링 웹 계층]

스프링 웹 계층

 

 

[Controller]

MVC 패턴에서 C에 해당하며 모델과 뷰를 업데이트하는 로직을 가지고 있는 클래스를 말한다.(클라이언트 입력에 응답하는 로직)

package hello.core.web;


import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
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();

        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL);

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

 

[Service]

package hello.core.web;


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);
    }
}
  • 비지니스 로직이 있는 서비스 계층에서도 로그를 출력하기 위해 만든 클래스

 

[오류 발생]

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 스코프 빈은 이와 달리 실제 고객의 요청이 와야 생성이 가능하다!!

 

 

5. 스코프와 Provider - 해결 방안(1)

이전 포스팅에서 살펴본 DL(Dependency LookUp)을 사용해서 문제를 해결해보자

package hello.core.web;


import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
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> myLoggerProvider;//

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURI().toString();
        MyLogger myLogger = myLoggerProvider.getObject();//

        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}
  • 이런식으로 사용하면 된다.

 

 

 

6. 스코프와 프록시 - 해결 방안(2)

우리의 개발자들은 Provider을 사용하면 매우 귀찮다고 생각해서 이것보다 짧고 간결하게 같은 효과를 내는 방법을 고안해 냈다. 그것이 바로 프록시다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
package hello.core.web;


import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
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();

        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}
package hello.core.web;


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);
    }
}

다시 전의 코드로 돌려서 사용하면 된다.

 

[CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.]

  • @Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면 스프링 컨테이너는 CGLIB 라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
  • 결과를 확인해보면 우리가 등록한 순수한 MyLogger 클래스가 아니라 MyLogger$ $EnhancerBySpringCGLIB 이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다.
  • 그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다. ac.getBean("myLogger", MyLogger.class) 로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다.
  • 그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다

 

[주의점]

싱글톤 사용하는 것처럼 보이지만 다르게 동작하기 때문에 주의해서 사용해야 하고, 이런 특별한 스코프는 꼭 필요한 곳에만 최소화해서 사용해야 한다. 무분별하게 사용하면 유지보수가 매우 어려워지기 때문이다.