Spring

Spring Basic- Bean Scope(프로토타입 스코프)

녁이 2023. 6. 9. 18:56
728x90
반응형

빈 스코프가 무엇일까?

우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되며, 종료와 동시에 같이 사라지는 것을 알고 있다.

이는 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는 말 그대로 존재할 수 있는 범위 이다.


스프링이 제공하는 스코프에는 대표적으로 뭐가 있을까?

  • 싱글톤
  • 프로토타입
  • 웹 관련 스코프
    • request
    • session
    • application

자세한 내용은 위의 순서대로 설명하겠다.


스코프를  설정(지정)하는 방식은 예상하는 바와 같이 컴포넌트 스캔 자동 등록으로 가능하다.

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

또한, 수동 등록도 가능하다.

@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
	return new HelloBean();
}

싱글톤 스코프는 기본 스코프로서, 스프링 컨테이너의 시작과 끝을 함께하는 넓은 범위의 스코프다.

이는 계속 사용했으니 생략하겠다.


프로토타입 스코프

 

-스프링 컨테이너는 프로토타입 빈의 생성과 DI 주입까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프이다.

싱글톤 스코프는 빈을 조회하면 항상 같은 Instance의 스프링 빈을 반환

But, 프로토타입 스코프는 조회 시마다 새로운 인스턴스를 생성해서 반환

 

예를 들어, 여러 클라이언트들이 싱글톤 스코프인 memberService를 조회한다면, 동일한 인스턴스의 빈을 반환받게 됨.

그러나, 프로토타입 스코프라면 각각의 클라이언트들이 다른 인스턴스 빈을 받게 된다.

더불어서, 스프링 컨테이너는 생성과 DI 주입과 동시에 이를 관리를 하지 않는다,

[스프링 컨테이너가 DI 주입 이후로 관리하지 않기때문에 이를 조회한 클라이언트가 관리를 해야함.]

그렇기 때문에 프로토타입 스코프는 종료 메서드(@PreDestroy)가 호출되지 않는다!

 

코드로 간단한 예시를 들어 알아보자.

public class PrototypeTest {
	@Test
    public 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);
        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 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext - 
Closing

프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성되고, 초기화도 2번 실행된 것을 알 수 있다.

앞서 말했듯이 스프링 컨테이너는 생성과 의존관계 주입까지만 관여하기 때문에 종료 메서드가 따로 실행되지 않았다.

종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.

 


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

우리는 프로토타입의 특성인 각각의 클라이언트에게 새로운 인스턴스를 생성해서 반환하는 특성을 활용하고 싶을 것이다.

그런데, 이 특성이 싱글톤 빈과 함께 사용하면서 의도한 대로 잘 작동하지 않는 문제가 있다. 

이에 대해 알아보자.

[ clientBean은 의존관계 자동 주입을 사용, 주입 시에 스프링 컨테이너에 프로토타입 빈을 요청 ]

프로토타입 스코프인 prototypeBean을 통해서 인스턴스를 받았다. 또한, prototypeBean 내부의 로직인 addCount()를 통해서 호출할 때마다 count++가 되도록 했다.

여기까지 설정한 우리는 각각의 클라이언트가 이를 호출할 때마다 count가 각각 1씩으로 바뀌는걸 원할 것이다.

하지만, 우리의 예상대로 흘러가지 않고 카운트는 1이 아닌 2로 바뀌게 된다. 결국 싱글톤처럼 사용된다.

어떤게 문제일까?

clientBean 내부에 있는 프로토타입 빈은 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 새로 생성된 것이지, 사용할 때마다 새로 생성되지 않는다는 점을 알아야 한다!

코드도 함께 첨부하겠다.

public class SingletonWithPrototypeTest1 {
	@Test
    void singletonClientUsePrototype() {
    	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);
        
        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
        }

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

@Scope("prototype")
static class PrototypeBean {
	private int count = 0;
    
    public void addCount() {
    	count++;
    }
    public int getCount() {
    	return count;
    }
    @PostConstruct
    public void init() {
    	System.out.println("PrototypeBean.init " + this);
        }
    @PreDestroy
    public void destroy() {
    	System.out.println("PrototypeBean.destroy");
        }
    }
}

이 문제를 해결하기 위해선 어떻게 해야할까?

-> 가장 간단한 방법은 사용할 때마다 항상 새로운 프로토타입 빈을 요청하는 것이다. 하지만 이 방법은 효율적이지 않기 때문에 추천하지 않는다.

우리는 우리가 지정한 프로토타입 빈을 스프링 컨테이너에서 대신 찾아주는 DL(Dependency Lookup)의 기능만 제공하는 무언가만 있으면 이를 해결할 수 있게 된다.

이를 제공해주는 기능이 스프링에는 존재한다.


ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 기능을 제공하는 것이 ObjectProvider이다.

이를 적용해서 어떻게 해결하는지 코드로 알아보자.

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider; //스프링이 알아서 빈으로 등록

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

clientBean 클래스의 내부 로직만 이와 같이 변경해주면 된다.

getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환(DL 기능)한다.

ObjectProvider를 통해 손쉽게 문제를 해결했지만, 이는 스프링에 의존적이다라는 단점은 알고 사용해야 한다.

 

요즘은 왠만하면 다 스프링 컨테이너를 사용하고 있기 때문에 걱정하지말고 Provider를 적극적으로 활용하자.

그런데, 다른 컨테이너를 사용해야 하는 경우가 있다면 자바 표준인 JSR-330 Provider를 사용하면 된다.

ObjectProvider나 JSR-330 Provider는 프로토타입 뿐만 아니라 DL이 필요한 경우에는 언제든지 사용할 수 있다.


다음 게시글

웹 스코프 + 총 정리

728x90
반응형