본문 바로가기
BackEnd/Spring

빈 스코프

by 규난 2022. 12. 18.
728x90

빈 스코프 란?

  • 지금까지 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어 스프링 컨테이너가 종료될 때 까지 유지된다고 알고있었다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스프링은 싱글톤 스코프 말고도 다양한 스코프를 지원한다.
  • 빈 스코프 종류
    • 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작부터 종료까지 유지되는 가장 넓은 범위의 스코프
    • 프로토타입 : 스프링 컨테이너 프로토타입 빈의 생성과 의존 관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
    • 웹 관련 스코프
      • request : 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
      • session : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
      • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

프로토타입 스코프

  • 스프링 컨테이너가 항상 새로운 인스턴스를 생성해서 반환한다.
  • 스프링 컨테이너는 프로토타입 빈을 생성하고 의존 관계 주입과 초기화까지만 처리하고 이후에는 프로토타입 빈을 관리하지 않는다(@PreDestroy와 같은 종료 메서드가 호출되지 않음). 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다.
package com.example.core.scope;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import static org.assertj.core.api.Assertions.assertThat;

public class PrototypeTest {

    @Test
    @DisplayName("프로토타입 빈 테스트")
    public void prototypeBeanFind() throws Exception {
        AnnotationConfigApplicationContext ac =
               new AnnotationConfigApplicationContext(PrototypeBean.class);

        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);

        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);

        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close(); //종료
    }

    @Scope("prototype")
    static class PrototypeBean {

        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean destroy");
        }
    }
}

/*
출력 결과
**find prototypeBean1
PrototypeBean init
find prototypeBean2
PrototypeBean init
prototypeBean1 = com.example.core.scope.PrototypeTest$PrototypeBean@5bf0fe62
prototypeBean2 = com.example.core.scope.PrototypeTest$PrototypeBean@1e097d5
*/**

  • 프로토타입 스코프는 싱글톤 스코프와 다르게 빈을 조회할 때 생성되고 초기화 메서드도 실행
  • 위 코드에서 프로토타입 빈을 2번 조회했으므로 다른 빈 2개가 생성되고 초기화도 2번 실행이 된다.
  • 하지만 프로토타입 빈은 스프링 컨테이너가 빈 생성과 초기화 까지만 관리하기 때문에 스프링 컨테이너가 종료될 때 소멸 작업이 자동으로 되지 않는다. 만약 소멸 작업을 하고 싶으면 직접 이 코드를 사용하여 prototypeBean2.destroy(); 소멸을 시켜줘야한다.

웹 스코프

  • 웹 환경에서만 동작한다.
  • 프로토타입 스코프와는 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.
  • 웹 스코프의 종류
    • request : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
    • session : HTTP Session과 동일한 생명주기를 가지는 스코프
    • application : ServletContext와 동일한 생명주기를 가지는 스코프
    • websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

request scope 실습

  • 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어려운데 이럴때 쓰기 좋은것이 request scope이다.
  • request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘기면 파라미터가 많아져서 코드가 지저분해진다. 서비스 계층은 가급적이면 순수하게 유지하는 것이 유지보수 관점에서 좋다.
  • request scope을 사용하면 파라미터로 넘기지 않고 서비스 계층에서 멤버변수에 저장해서 웹과 관련된 정보를 사용 할 수 있어지므로 서비스 계층을 순수하게 유지가 가능하다.
// scope
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
// proxyMode = ScopedProxyMode.TARGET_CLASS
// 위 코드를 추가해 MyLogger의 가짜 프록시 클래스를 만들어 두고 
// 실제 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입 가능
// 위 부분을 적용해주지 않으면 서버 실행시 에러가 난다.
@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("[" + this.uuid + "]" + " [" + this.requestURL + "]"
        + message);
    }

    @PostConstruct
    public void init() {
        this.uuid = UUID.randomUUID().toString();
        System.out.println("[" + this.uuid + "] request scope bean create" + this);
    }

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

// controller 
import com.example.core.common.MyLogger;
import com.example.core.logdemo.service.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @GetMapping("/log-demo")
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

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

}

// service
import com.example.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);
    }
}
728x90

'BackEnd > Spring' 카테고리의 다른 글

@ControllerAdvice와 @ExceptionHandler  (0) 2023.01.08
DispatcherServlet과 Spring MVC 아키텍처  (0) 2022.12.18
빈 생명주기 콜백  (0) 2022.12.18
의존 관계 자동 주입  (0) 2022.12.18
싱글톤 컨테이너  (0) 2022.12.18