본문 바로가기

SPRING/영한님 강의 - 스프링 핵심원리 기본편

빈 스코프(Singleton, Prototype, 웹 관련), 빈 스코프 활용하여 로그

현재 포스팅은 영한님의 스프링의 핵심원리 - 기본편을 바탕으로 작성한 포스팅입니다 :)

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

 

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

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢

www.inflearn.com


빈 스코프(Bean Scope)

스프링은 빈이라는 개념으로 객체를 만들고 싱글톤화하여 관리합니다. 이렇게 빈으로 생성된 객체들은 스프링 컨테이너와 함께 시작되어서 종료될 때까지 스프링이 관리해 주는데, 이 이유는 스프링 빈들은 기본적으로 싱글톤 스코프로 관리되기 때문입니다.

 

그렇다면 빈 스코프란 무엇일까요?? 빈 스코프란 빈이 존재할 수 있는 범위를 뜻합니다 :)

 

스프링은 다음과 같은 다양항 스코프를 지원합니다.

  • Singleton : 기본 스코프로, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프입니다.
  • ProtoType : 프로토 타입의 빈의 경우, 스프링 컨테이너가 빈의 생성과 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프입니다.(따라서 소멸 콜백이 호출 안됨)
  • 웹 관련 스코프
    • Request : HTTP 요청이 하나 들어오고 나갈때 까지 유지되는 스코프입니다. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
    • Session : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프입니다(Http Session과 동일한 생명주기를 가지는 스코프).
    • Application : 웹의 ServletContext와 같은 범위로 유지되는 스코프입니다
    • WebSocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

스코프 등록 방법

@Component
public class HelloBean{
	...
}

싱글톤 스코프의 경우 위의 예시처럼 @Component로 등록하면 됩니다.(디폴트 옵션이 싱글톤 스코프로 적용)

 

// 컴포넌트 스캔에 의해 자동 등록
@Scope("prototype")
@Component
public class HelloBean{
	...
}

// 수동으로 빈 등록 시
@Scope("prototype")
@Bean
PrototypeBean HelloBean(){
	return new HelloBean();
}

다른 스코프의 경우 위의 예시 코드처럼 @Scope 어노테이션을 통해 적용하시면 됩니다!


프로토 타입 스코프

싱글톤 스코프의 빈을 스프링 컨테이너에 요청을 하면, 항상 같은 인스턴스의 스프링 빈을 반환합니다.

 

반면 프로토타입 스코프의 빈을 요청하게 되면, 스프링은 매 요청마다 항상 새로운 프로토 타입의 빈을 생성해서 반환하게 됩니다.

 

여기서 핵심은 스프링 컨테이너는 프로토타입의 빈을 생성(메모리에 할당되는 과정)하고, 의존관계 주입 및 초기화(빈 만들어 진후, 추가적인 설정이나 준비)까지만 처리한다는 것입니다!

컨테이너는 클라이언트에 빈을 반환하고, 이후 해당 빈을 관리하지 않습니다. 즉 해당 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에게 있습니다! 따라서 스프링 컨테이너가 사용하는 @PreDestroy와 같은 소멸 전 콜백은 호출되지 않습니다!

 

싱글톤 빈에서 프로토타입 빈 사용 시

싱글톤 빈이 의존 관계 주입을 통해 프로토타입의 빈을 주입받아서 사용한다고 가정해 보겠습니다!

 

위 그림에서 clientBean은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생합니다. 이때 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청합니다. 이때 스프링 컨테이너는 프로토타입 빈을 생성해서 ClientBean에 반환합니다. 의존관계 주입을 통해 clientBean은 프로토타입 빈을 내부 필드에 보관하겠죠??

 

 

이후 클라이언트 A가 clientBean을 스프링 컨테이너에 요청해서 받았다고 해보겠습니다. 그러면 clientBean 자체는 싱글톤이기에 항상 객체가 반환이 되겠죠??

그리고 클라이언트 A가 addCount라는 로직을 호출해서 프로토타입 빈의 count를 1로 증가시켰다고 가정해 보겠습니다.

 

클라이언트 A가 아닌 클라이언트 B가 ClientBean을 요청하고 addCount() 로직을 호출하면 count를 한 개 증가한 2가 됩니다.

 

지금까지 제가 들어본 예시의 중요한 점은, clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이라는 것입니다. 즉 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지,사용할 때마다 새로 생성되는 것이 아닌 것이죠. 따라서  상태를 유지하고 있는 것입니다

 

그렇다면 어떻게 싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때마다 항상 새로운 프로토타입 빈을 생성할 수 있을까요??

 

간단하게 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 빈을 요청할 수도 있겠지만 보통은 Provider를 사용합니다.

여기서 Provider란 지정할 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 주체입니다!

 

@Scope("singleton")
static class ClientBean{

    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic(){
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

@Scope("singleton")
static class ClientBean{

    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;

    public int logic(){
        PrototypeBean prototypeBean = prototypeBeanProvider.get();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

첫 번째 코드는 ObjectProvider, 두 번째 코드는 javax.inject.Provider라는 자바 표준 프로바이더를 사용한 코드입니다.

위 코드의 getObject()와 get() 메서드를 통해 항상 새로운 프로토타입 빈이 생성됩니다.


웹 스코프

웹 스코프는 웹 환경에서만 동작하며, 프로토타입과 다르게 스프링이 해당 스코프의 종료 시점까지 관리를 합니다.

이러한 웹 스코프에는 앞서 살펴 보신 것처럼 request, session, application, websocket이 있습니다. 

 

 

request scope bean의 경우 위 그림처럼 HTTP request 요청 당 각각 할당됩니다.(각 클라이언트 별로 빈이 할당되는 것을 볼 수 있다)

만약 동시에 여러 HTTP 요청이 오면 어떤 요청이 남긴 로그인지 구분하기 어려울 수 있습니다. 이럴때 사용하기 딱 좋은 것이 바로 request 스코프입니다.

 

위와 같은 형식으로 로그를 남겨 보기 위해 코드를 짜보겠습니다!(포맷 : [UUID][requestURL]{message})

 

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

위 코드는 로그를 출력하기 위한 MyLogger 클래스로, @Scope(value = "request")를 통해 request 스코프로 지정하였습니다.

 

빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장하고, 소멸되는 시점에 @PreDestroy를 사용해서 종료 메시지를 남깁니다. RequestURL은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받습니다.

 

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

위 코드는 로거가 잘 작동하는지 확인하기 위한 테스트용 컨트롤러입니다. HttpServletRequest에서 Request URL을 받고, 이렇게 받은 값을 MyLogger에 저장하고 있습니다. 이때 myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 됩니다 :)

 

그런데 이때 requestURL을 MyLogger에 저장하는 부분은 컨트롤러보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋습니다. 혹시 스프링에 익숙하시다면 인터셉터를 사용해서 구현하는 것을 추천드립니다!

 

2번째 코드는 비즈니스 로직이 있는 서비스 계층에서도 로그를 출력하고 있습니다. 만약 request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면, 파라미터가 너무 많아 지저분해질 뿐만 아니라, requestURL과 같이 웹과 관련된 정보가 웹과 관련 없는 서비스 계층까지 넘어가게 됩니다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋습니다!

 

현재까지의 코드를 실행시켜 보면

다음과 같은 오류가 뜹니다... 왜 그럴까요???

LogDemoController 코드에서 private final MyLogger myLogger로 의존관계를 주입받는 상황에 대해 생각해 보겠습니다!

 

스프링이 실행되면서 LogDemoController 클래스의 빈이 생성됩니다(@Controller). 생성자 주입으로 인해 생성 시 의존관계를 맺게 되는데, 의존 관계를 맺어줄 때 과연 myLogger가 존재할까요??? 

 

myLogger의 경우 request scope, 즉 요청이 들어올 때 생성되는 빈인 것입니다! 따라서 스프링 실행 시에는 고객의 요청이 들어오지 않았으니 컨테이너에 존재하지 않고, 위와 같은 오류가 뜨는 것이죠!

 

이러한 오류는 아까 잠시 살펴보았던 Provider를 통해 해결할 수 있습니다 :)

 

Provider를 통해 빈 의존관계 주입 단계를 실제 고객의 요청이 왔을 때 진행하도록 지연시킬 수 있습니다

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    // 이 경우 DL(Dependency LookUp)을 할 수 있는 놈이 주입이 된다.
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.setRequestURL(requestURL);

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

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerObjectProvider;
    public void logic(String id){
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

위의 ObjectProvider 덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연시킬 수 있습니다. ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행 중이므로 request 빈의 생성이 정상 처리되는 것이죠!

 

이때 ObjectProvider.getObject()를 LogDemoController, LogDemoService에서 각각 한 번씩 따로 호출해도, 같은 HTTP 요청이면 같은 스프링 빈이 반환됩니다 :)

 

위와 같이 Provider를 사용하는 것 외에 프록시 방식을 사용할 수도 있습니다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger{

}

프록시 방식 사용시 MyLogger의 가짜 프록시 클래스를 만들어 두고 HTTP Request와 상관 없이 가짜 프록시 클래스를 다른 빈에 주입할 수 있습니다.

이때 핵심은 Controller에서 MyLogger에 대한 의존관계를 주입할때는 가짜 MyLogger를 주입해두고, 실제 개발자가 이 기능을 호출하는 시점에 진짜를 찾아서 동작한다는 것입니다!

 

가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있습니다