[스프링 핵심원리 기본편] 빈 스코프

흑수·2022년 2월 8일
0

김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.

빈 스코프란?

앞의 작성한 글에서는 스프링 빈이 컨테이너 생성을 시작으로 생성되어서 컨테이너가 종료될 때, 소멸된다고 이야기 했습니다. 이것은 스프링 빈이 싱글톤 스코프로 생성되기 때문입니다.

스코프란 말 그대로 범위. 즉, 빈이 존재할 수 있는 범위입니다.

스프링은 다양한 빈 스코프를 지원합니다.

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

다음과 같이 등록할 수 있습니다.

@Scope("prototype")
@Component
public class HelloBean {}

프로토타입 스코프

싱글톤 스코프의 경우 늘 같은 인스턴스를 반환하지만, 프로토타입 스코프는 매번 새로운 인스턴스를 생성해서 다른 인스턴스를 반환합니다.

싱글톤 빈의 경우
1. 클라이언트가 요청(MemberService)을 날리면 스프링 컨테이너는 해당하는 스프링 빈을 반환합니다.
2. 또 다른 클라이언트가 요청(MemberService)을 날리면 1번의 빈과 동일한 스프링 인스턴스를 반환해줍니다.

프로토타입 빈의 경우
1. 클라이언트가 요청(prototype Bean)을 날리면 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입하고 반환해줍니다.
2. 또 다른 클라이언트가 요청을 날리면 새로운 프로토타입 빈을 생성, 의존관계 주입하고 새로운 인스턴스를 반환해줍니다.

핵심은 스프링 컨테이너가 프로토타입 빈을 생성하고 의존관계를 주입, 초기화까지만 관여를 한다는것. 그 이후에는 스프링 컨테이너가 관리를 하지 않습니다.

이 말은 즉, @PreDestroy를 수행하지 않는다는 것입니다.


테스트

  • 싱글톤 스코프
@Test
public 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);
}

@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
  • 프로토타입 스코프
@Test
public void prototypeBeanFind() {
	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
	PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); 
	PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); 
    System.out.println("prototypeBean1 = " + prototypeBean1); 
    System.out.println("prototypeBean2 = " + prototypeBean2); 
}
@Scope("prototype")
static class PrototypeBean {
	@PostConstruct
    public void init() {
    	System.out.println("PrototypeBean.init");
   	}
    @PreDestroy
    public void destroy() {
    	System.out.println("PrototypeBean.destroy");
    }
}

이를 실행해 보면 두개의 인스턴스가 다르다는 것과 destroy()함수 호출이 안되는 것을 알 수 있습니다.

PrototypeBean.init
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971 
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing

✓ 프로토타입 빈의 특징 정리

  • 스프링 컨테이너에 요청할 때마다 생성
  • 스프링 컨테이너는 생성, 의존관계 주입, 초기화까지만 관여 (종료 메서드 호출 x)
  • 그렇기 때문에 클라이언트에서 직접 관리

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

그렇다면 이런 상황일 때는 어떻게 해야 할까요? 🤔

첫번째 상황

A라는 클라이언트가 프로토 타입 빈을 요청하면 스프링 컨테이너에서 새로 생성해서 count필드가 0인 객체를 반환(x01)합니다. 그 이후에, addCount()를 통해 count 값을 1증가 시킵니다. 결국, 빈(x01)의 count필드 값은 1이 됩니다.

B라는 새로운 클라이언트가 A와 동일한 행동을 하게 된다면, count필드가 1인 객체(x02)를 반환받게 됩니다.


두번째 상황

이번에는 싱글톤 빈 안에서 의존관계 주입을 통해 프로토타입 빈을 주입받는 상황입니다.

싱글톤 빈은 스프링 컨테이너 생성 시점에 생성되어 의존관계 주입을 받게 되므로, 주입 시점에 프로토 타입을 주입 받게 됩니다.

이 때, 프로토 타입 빈의 필드 값인 count값은 0이고 A 클라이언트가 logic() 메서드(프로토 타입 빈의 count 값 1증가)를 호출하면 count값이 1이 됩니다.

또 다른, B 클라이언트가 동일한 메서드 logic()을 호출하게 된다면 싱글톤이기에 같은 객체를 받게 되고 그 안에 미리 주입 받은 프로토 타입 빈(이전에 증가해서 count값이 1) 내의 count 값은 2가 됩니다.

싱글톤 내부에 있는 프로토 타입 빈은 이전에 의존관계 주입이 끝났기 때문에 새롭게 생성하지 않습니다!!

static class ClientBean {
	private final PrototypeBean prototypeBean;
    
    @Autowired
    public ClientBean(PrototypeBean prototypeBean) {
    	this.prototypeBean = prototypeBean;
    }
    
    public int logic() {
    	prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

@Scope("prototype")
static class PrototypeBean {
	private int count = 0;
    public void addCount () {
    	count++;
    }
    public int getCount() {
    	return count;
    }
}

하지만 느낌상 원하는 것은 프로토 타입 빈이 싱글톤 내부에 있지만 부를 때마다 새롭게 생성되는 것입니다.

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

다음과 같은 방법으로 해결할 수 있답니다.

스프링 컨테이너에 요청

가장 간단한 방법은 프로토타입 빈을 사용할 때마다, 스프링 컨테이너에 요청하는 것.

static class ClientBean {
	@Autowired
	private ApplicationContext ac;
    
    public int logic() {
		PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
        prototypeBean.addCount();	
		int count = prototypeBean.getCount();
        return count;
	} 
} 
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);

이 부분에서 매번 새로운 프로토 타입 빈이 생성됩니다. 이것은 의존관계 주입(DI)이 아닌 의존관계 조회, 탐색(DL, Dependency Lookup)입니다.

하지만 ApplicationContext을 지금 주입받게 되므로, 스프링 컨테이너에 종속적입니다.

또다른 방법이 없을까요? 🤔

ObjectFactory, ObjectProvider

지정한 빈 (< PrototypeBean >)을 컨테이너 대신 찾아주는 것이 ObjectProvider 입니다.
(과거에는 ObjectFactory)

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

ObjectProvider.getObject()를 통해 매번 새로운 빈이 생성됩니다.

JSR-330 Provider

이것은 자바 표준입니다.
라이브러리에 필수적으로 추가해주어야합니다. (gradle)

implementation '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;
}

웹 스코프

웹 스코프는 웹 환경에서 작동하고, 프로토 타입 스코프와 달리 종료 시점까지 스프링이 관리합니다. (-> 이 말은 곧 종료 메서드도 호출 된다는 것)

종류에는 requset, session, application, websocket이 있습니다.

예제 만들기

웹 환경을 만들기 위해 build.gradle에 추가해줍니다.

implementation 'org.springframework.boot:spring-boot-starter-web'

이는 내장 톰캣 서버를 활용해서 웹서버와 스프링을 함께 실행시키는 것을 가능하게 해줍니다.

동시에 여러 HTTP 요청이 오면 구분하기 위해 로그를 남기도록 예제를 구현하겠습니다.

[a332fav] requset scope bean created 
[a332fav] [localhost:8080] ~~~ 
[a332fav] requset scope bean close

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

Controller

@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

@Service
@RequiredArgsConstructor
public class LogDemoService {
	private final MyLogger myLogger;
	public void logic(String id) {
		myLogger.log("service id = " + id);
	} 
}

https://localhost:8080/log-demo 를 웹사이트 주소창에 입력하게 되면 위의 코드가 작동하게 됩니다.

하지만 실제로 입력을 해보면 오류가 나타나게 됩니다.

request빈은 요청이 들어와야 생성할 수 있습니다!
즉, 요청이 들어오지 않은채로 의존관계 주입을 하려 했기 때문에 생긴 문제.

스코프와 Provider

앞서 배운 Provider를 이용하면 해결 가능합니다.

private final ObjectProvider<MyLogger> myLoggerProvider;

public String logDemo(HttpServletRequest request) {
	...
	MyLogger myLogger = myLoggerProvider.getObject();
    ...
}

getObject()를 호출하는 시점은 이미 주소창에 url을 입력했을 시기이기 때문에 HTTP 요청이 진행중입니다.

스코프와 프록시

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

이 때, 클래스면 TARGET_CLASS. 인터페이스라면 TARGET_INTERFACE.

HTTP 요청에 상관 없이 CGLIB 라이브러리를 이용해 가짜 프록시 클래스를 만들고, 빈에 미리 주입해 놓을 수 있습니다.

이전에는 requset 스코프 였기 때문에 요청이 들어오기 전에 주입하는 것이 문제가 됐으므로 이 문제가 해결 된다는 것.

CGLIB는 바이트코드를 조작해 가짜의 무언가를 만듭니다. (여기서는 가짜 프록시 클래스, 즉 가짜 MyLogger 클래스)

진짜 요청이 들어오게 되면 가짜 프록시 클래스 -> 진짜 클래스를 찾게 됩니다.

myLogger.logic()은 가짜 프록시 클래스의 메서드를 호출한 것이고 이 클래스는 진짜 클래스의 logic을 호출합니다.


이상으로 마무리하겠습니다. 이번 편은 길었네요..
저도 틈틈히 보면서 열심히 공부해야 겠어요.

다들 화이팅~!~!

profile
기록용

0개의 댓글