빈 스코프

Sangkyeong Lee·2021년 5월 19일

인프런 강의 < 스프링 핵심 원리 - 기본편 > 정리


빈 스코프

  • 스프링 빈은 스프링 컨테이너의 시작과 함께 생성되고 스프링 컨테이너가 종료될 때 까지 유지된다.
  • 스프링 빈이 싱글톤 스코프로 생성되기 때문
    • 스코프는 빈이 존재할 수 있는 범위를 말한다.

스프링이 지원하는 스코프

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
  • 웹 관련 스코프
    • request: 웹 요청이 들어오고 나갈때까지 유지
    • session: 웹 세션이 생성되고 종료될때까지 유지되는 스코프
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

프로토타입 스코프

  • 싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
  • 반면에 프로토타입 스코프를 스프링 컨테이너에 조회하면 항상 새로운 인스턴스를 생성해서 반환한다.

싱글톤 빈 요청


1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
3. 이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환한다.

프로토타입 빈 요청1


1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.

프로토타입 빈 요청2


3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.

정리

  • 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리.
  • 클라이언트에 빈을 반환하고, 스프링 컨테이너는 생선된 프로토타입 빈을 관리하지 않는다.
  • 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다.
  • @PreDestory같은 종료메서드는 호출되지 않는다.

싱글톤 스코프 테스트

public class SingletonTest {

    @Test
    void singletonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);
        Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);

        ac.close();
    }

    @Scope("singleton")
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

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

}

결과

SingletonBean.init
singletonBean1 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd
singletonBean2 = hello.core.scope.PrototypeTest$SingletonBean@54504ecd
org.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing SingletonBean.destroy

프로토타입 스코프 테스트

public class PrototypeTest {

    @Test
    void prototypeBeanFind() {
        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);
    }

    @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 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing 
  • 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드 실행, 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행됨
  • 프로토타입 빈을 2번 조회했으므로 다른 스프링 빈이 생성되고, 초기화도 2번 실행됨
  • 프로토타입은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하므로 종료될때 PreDestory 같은 종료 메서드가 실행 되지 않음!!

프로토타입 빈 정리

  • 스프링 컨테이너에 요청할 때 마다 새로 생성된다.
  • 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.
  • 종료 메서드가 호출되지 않는다.
  • 그래서 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

스프링 컨테이너에 프로로타입 스코프의 빈을 요청하면 항상 새로운 객체를 반한한다.

  • 하지만 싱글톤 빈과 함께 사용할 때는 원하는대로 동작하지 않는다.

  • clientBean은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생
  • clientBean은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청
  • 스프링 컨테이너는 프로토타입 빈을 생성해서 clienBean에 반환한다. 프로토타입 빈의 conunt 필드 값은 0이다.
  • 그 후에, clientBean은 프로토타입 빈을 내부 필드에 보관한다.( 정확히 말하면 참조값을 보관한다.)

  • 클라이언트 A는 clientbean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
  • 클라이언트 A는 clientBean.logic()을 호출한다.
  • clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가시킨다.

  • 클라이언트 B는 clientbean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
  • 여기서 중요한 점은, clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이고, 사용할때마다 새로 생성되는 것은 아니다.
  • 클라이언트 B는 clientBean.logic()을 호출한다.
  • 프로토타입 빈의 count를 증가시키고 count의 값은 2가 된다.

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

  • 가장 간단한 방법은 싱글톤 빈이 프로토타입 스코프를 사용할때마다 스프링 컨테이너에 새로 요청하는 것
@Autowired
private ApplicationContext ac;
public int logic() {
    PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

ObjectFactory, ObjectProvider

  • 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider이다. 참고로 과거에는 ObjectFactory가 있었고 여기에 편의 기능을 추가한것이 ObjectProvider이다.
static class ClientBean {

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

        public int logic(){
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }
  • ObjectProvidergetObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
  • Dependency Lookup 정도의 기능만 제공

JSR-330 Provider

  • javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법
    • 사용하기 위해서 gradle에 javax.inject:javax.inject:1을 추가해야 한다.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
  • Provider.get()을 통해 새로운 프로토타입 빈 생성
  • 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환
  • DL정도의 기능만 제공

특징

  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
  • 메서드가 get()하나라서 기능이 매우 단순
  • 별도 라이브러리 필요

웹 스코프

복습

  • 싱글톤은 스프링 컨테이너의 시작과 끝까지 함께
  • 프로토타입은 생성과 의존관계 주입, 초기화까지

웹 스코프 특징

  • 웹 환경에서 동작
  • 프로토타입과 다르게 스프링이 스코프 종료까지 관리함

웹 스코프 종류

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

request 스코프 예제

  • 다음과 같이 로그를 남겨서 개발
[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
- 포멧: [UUID][requestURL]{message}

MyLogger

@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 스코프 지정
    • HTTP 요청 마다 하나씩 생성되고 끝나면 소멸함
  • 이 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid 생성해 저장함.
  • 빈이 소멸되는 시점에 @PreDestroy를 사용해서 종료 메시지 남김
  • requestURL은 이 빈이 생성되는 시점에 알수 없으므로. 외부에서 setter로 입력받음

LogDemoController

@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();
        myLogger.setRequestURL(requestURL);

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

LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log("Service id = " + id);
    }
}
  • request scope를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘기면 보기 안좋다.
  • 웹과 관련된 부분은 컨트롤러까지만 사용하자!
  • 서비스 계층은 웹 기술에 종속되지 않는 것이 좋다.

결과

  • 실제 고객의 요청이 오지 않아 제대로 결과가 나오지 않는다.

해결방법

1. 스코프와 Provider

LogDemoController

@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();
        myLogger.setRequestURL(requestURL);

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

LogDemoService

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

    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("Service id = " + id);
    }
}
  • ObjectProvider 덕분에 getObject()를 호출하는 시점까지 request scope 빈의 생성 지연 가능
  • 호출시점에 HTTP 요청이 가서 정상적으로 빈이 생성됨

2. 스코프와 프록시

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

proxyMode = ScopedProxyMode.TARGET_CLASS를 추가한다
- 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS
- 적용 대상이 인터페이스면 INTERFACES 선택

  • MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP Request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입시킨다.

MyLogger 출력

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

결과

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
  • CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 개체를 만들어서 주입한다.
    • CGLIB는 스프링 컨테이너가 바이트코드를 조작하는 라이브러리다.
  • 의존관계 주입도 이 가짜 프록시 개체가 주입된다.

  • 가짜 프록시 개체가 진짜 myLogger를 찾아서 호출해준다.
  • 사용자 입장에서는 원본인지 아닌지 모르므로, 동일하게 사용할 수 있다(다형성)

결론

  • 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope 사용 가능
    • 싱글톤처럼 보이지만 다르게 동작하기 때문에 주의
  • provider, 프록시 모두 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다.

< 자료 출처: 스프링 핵심 원리 - 기본편 >

profile
💻Backend Developer

0개의 댓글