[Spring]스프링 핵심 원리(기본편) - 9. 빈 스코프

Wooyong Jung·2023년 9월 20일
0
post-thumbnail
post-custom-banner
  • 해당 게시물은 인프런 "스프링 핵심 원리 - 기본편" 강의를 참고하여 작성한 글입니다.
  • 자세한 코드 및 내용은 강의를 참고해 주시길 바랍니다.
    강의링크 -> 스프링 핵심 원리 - 기본편 (김영한)

Section9. 빈 스코프


📄 빈 스코프란?

빈 스코프란 빈이 존재할 수 있는 범위를 뜻한다. 지금까지는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지된다고 학습했는데 이는 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스프링은 다양한 스코프를 지원한다

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

빈 스코프는 다음과 같이 지정할 수 있다.


📄 프로토타입 스코프

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

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

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

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

프로토타입 빈의 특징

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

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

public class SingletonWithPrototypeTest1 {

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = 
        	new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);	// ClientBean과 PrototypeBean은 컴포넌트 스캔으로 자동 등록

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        	...

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        	...
    }

    @Scope("singleton")
    static class ClientBean {

        private final PrototypeBean prototypeBean;
 
 		@Autowired
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        public int logic() {
        	...
        }
    }

    @Scope("prototype")
    static class PrototypeBean {
		...
    }
}

싱글톤 빈인 ClientBean은 new AnnotationConfigApplicationContext()으로 생성시점에 생성자를 통해 의존관계 주입을 받았다. 사용자는 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때마다 새로 생성해서 사용하길 원할 것이다. 그런데, 위의 경우에는 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되게 되는데 이는 Provider로 문제를 해결할 수 있다.

ObjectFactory, ObjectProvider

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
	PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
 	prototypeBean.addCount();
 	int count = prototypeBean.getCount();
 	return count;
}
  • 항상 새로운 프로토타입 빈이 생성된다.
  • ObjectProvidergetObject()를 호출하면 그 때서야 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
  • 스프링에 의존하기 때문에 자바 표준인 JSR-330 Provider를 사용해도 괜찮다.

JSR-330 Provider

@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
	PrototypeBean prototypeBean = provider.get();
 	prototypeBean.addCount();
 	int count = prototypeBean.getCount();
 	return count;
}
  • get() 메서드 하나로 기능이 매우 단순하다.
  • 별도의 라이브러리가 필요하다.
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

📄 웹 스코프

웹 스코프의 특징

  • 웹 환경에서만 동작한다
  • 프로토타입 스코프와 다르게 스프링이 해당 스코프의 종료시점까지 관리한다
  • 종료 메서드가 호출된다

웹 스코프 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

📄 request 스코프

동시에 여러 HTTP 요청이 오면 정확힌 어떤 요청이 남긴 로그인지 구분하기 어려운데 이럴 때 request 스코프를 사용하면 구분을 쉽게 할 수 있다.

@Component
@Scope(value = "request")
public class MyLogger {

    private String uuid;
    private String requestURL;

	...

    @PostConstruct
    public void init() {
    	...
    }

    @PreDestroy
    public void close() {
    	...
    }
}

로그 출력을 위한 MyLogger클래스이다. request스코프로 지정했고 해당 빈은 HTTP 요청 당 하나 씩 생성되고, 요청이 끝나는 시점에 소멸된다.

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
    	...
	}
}

로거가 잘 작동하는지 확인하기위한 테스트용 컨트롤러인데 코드를 실행하면 Scope 'request' is not active for the current thread;라고 에러가 뜬다. 이는 LogDemoController가 의존성 주입을 받을 때 스프링 컨테이너에게 MyLogger를 요청하는데 MyLogger는 아직 생성되지 않았기 때문에 발생하는 에러이다. request 스코프 빈은 HTTP 요청 하나가 들어와야 생성되기 때문이다.

이는 앞에서 배운 Provider를 통해 해결할 수 있다.



@Controller
@RequiredArgsConstructor
public class LogDemoController {

	private final LogDemoService logDemoService;
	private final ObjectProvider<MyLogger> myLoggerProvider;

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

ObjectProvidergetObject()를 호출하는 시점까지 reuest 스코프 빈의 생성을 지연할 수 있다. ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로 request 스코프 빈의 생성이 정상 처리된다.

그런데 이것보다 더 간단한 방법이 있다.

스코프와 프록시

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

프록시를 사용하면 LogDemoController의 코드를 바꾸지 않아도 된다. @ScopeproxyMode = ScopedProxyMode.TARGET_CLASS를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받는 가짜 프록시 객체를 생성한다. 가짜 프록시를 주입하고 실제 요청이 오면 내부에 들이어있는 실제 빈을 요청하는 위임 로직을 이용하여 실제 메서드를 호출한다. 중요한 것은 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 점이다.

profile
실패를 두려워하지 않는 백엔드 개발자가 되기 위해 노력하고 있습니다.
post-custom-banner

0개의 댓글