당니의 개발자 스토리

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

스프링/스프링 핵심 원리 - 기본편

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

clainy 2024. 1. 27. 23:26

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

이번 시간에는 프로토타입 스코프싱글톤 빈과 함께 사용할 때 어떤 문제가 생기는지 알아보겠습니다.

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면, 항상 새로운 객체 인스턴스를 생성해서 반환해준다고 했잖아요. 그런데 그러지 않을 때가 있습니다.

언제냐면 프로토타입 빈을 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않아요.

이거를 코드와 그림으로 굉장히 자세히 설명을 드릴게요.


자 먼저 그냥 스프링 컨테이너에 프로토타입 빈을 직접 요청하는 예제를 한번 보겠습니다.

클라이언트 A가 PrototypeBean라는 이름의 빈을 요청해요. 그 요청을 받았어요. 그럼 스프링 컨테이너에서 새로 만들어서 반환하잖아요? 반환한 인스턴스의 참조값을 PrototypeBean@x01이라고 할게요. 주소값이죠.

만약에 PrototypeBean 안에는 count 라는 필드가 있는데, 거기에서 클라이언트 A가 addCount() 라는 걸 호출했어요. 호출하면 count가 하나 올라간다고 합시다. 그러면 PrototypeBean 내부에 있는 count가 0에서 1로 올라가겠죠. 그래서 결과적으로 count가 0에서 1이 되는 거예요.

그 다음에 다른 클라이언트가 다시 나타나가지고 또 스프링 DI 컨테이너한테 요청을 해요.

클라이언트 B가 "PrototypeBean 주세요." 해서 참조값이 x02인 PrototypeBean을 받았어요. 그 다음에 클라이언트 B가 addCount()를 호출을 해요. 그럼 PrototypeBean 내부의 count 값은 얼마가 되겠어요? 초기값이 0이라 치고, 또 0에서 1이 되는 거에요.

그러면 현재 서로 다른 2개의 프로토타입 빈이 있고, 지금 둘 다 count 값이 1인 거예요.


다음 예제를 위해서 코드로 작성해서 보겠습니다.

test 폴더의 scope에다가 SingletonWithPrototypeTest1 이라고 하고요.

그 다음에 프로토타입 빈을 만들어야죠.

이렇게 PrototypeBean 안에 필드와 메서드를 만들고요. 프로토타입 빈이니까 @PostConstruct, @PreDestroy를 만들겠습니다.

this 하면, 현재 자기 자신을 찍어 주는거죠. 그럼 나의 참조값을 볼 수 있을 거예요.

그리고 @PreDestroy는 호출 안되겠죠?

호출이 안되지만 확인하기 위해서 만들어줬습니다.

이제 스프링 컨테이너를 띄워서 동작하도록 만들어봐야죠. 그런 다음에 아까의 시나리오대로 빈을 찾아야 됩니다. 찾은 다음에는 addCount()를 호출해야죠.

그리고 이제 검증을 해볼겁니다.

static import를 미리 해주고,

prototypeBean1의 count 값이 1인지 확인합니다.

그리고 또 다른 클라이언트가 프로토타입을 다시 호출하는 거예요.

그럼 prototypeBean2도 역시 count 값이 1이 되어야 합니다.

돌려보면,

테스트가 성공했습니다. init으로 호출되면서 찍히는 참조값도 서로 다른 걸 볼 수 있죠.


그러면 이번에는 싱글톤 빈에서 프로토타입 빈을 사용하면 어떻게 되는지 보여드릴게요.

이번에는 clientBean이라는 싱글톤 빈이 있습니다.

이 싱글톤 빈의 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 케이스를 볼게요.

그러니까 코드를 바로 보면,

ClientBean 이라는 게 있고 내부에 PrototypeBean을 의존관계 주입으로 스프링 컨테이너한테 가져와서 사용하는 경우에 어떻게 되는지 한번 보겠습니다.

그림으로 설명드릴게요. 먼저 clientBean은 PrototypeBean이 필요하다고 했잖아요. 그러면 클라이언트 빈을 생성할 때 어떻게 합니까?

의존관계 주입으로 필요한 걸 받겠죠. 그래서 자동 의존관계 주입으로 스프링 DI 컨테이너한테 "나 PrototypeBean이 필요해!" 라고 하면, 스프링 DI 컨테이너가 프로토타입 빈을 요청하는 시점에 생성해서,

clientBean에다가 주입을 해주겠죠. 그래서 클라이언트 빈은 의존관계 주입 시점에 이 프로토타입 빈을 가지고 있는 거예요. 자신의 프로토타입 빈 필드에다가 값을 주입받아서, 정확히는 참조값을 주입받아서 보관하는 거죠.

그러면 이 클라이언트 빈이 싱글톤이기 때문에, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생하죠.

1번, clientBean 은 의존관계 자동 주입을 사용할 거예요. 그러면 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청을 하게 됩니다.

2번, 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 한테 반환해줘요. 그러고 나서, 더이상 스프링 컨테이너는 프로토타입 빈을 관리하지 않죠. 클라이언트한테 반환해주고 나면 이젠 관리를 안해요.

그럼 이 프로토타입 빈은 클라이언트 빈이 관리를 하게 됩니다. 이제 clientBean 은 프로토타입 빈을 내부 필드에 보관합니다. 정확히는 참조값을 보관하겠죠.

일단 처음 프로토타입 빈의 count 필드 값은 0이겠죠.

이런 상황인데,

여기서 클라이언트 A가 clientBean의 어떤 로직을 호출하는 거예요. clientBean의 logic이 프로토타입 빈의 addCount()한번 호출해주는 거예요.

일단, 클라이언트 A가 clientBean 을 스프링 컨테이너에 요청해서 받고, clientBean은 싱글톤이므로 항상 같은 clientBean 이 반환되겠죠.

3번, 클라이언트 A가 clientBean.logic() 메서드를 호출해요.
4번, 그러면 clientBean은 이 logic 안에서 무슨 일을 하냐면, prototypeBean의 addCount() 를 호출해줍니다. 그래서 프로토타입 빈의 count를 증가시켜요. 그러면 count가 0에서 1로 늘어나겠죠.

로직을 잠깐 보면, logic()을 호출하면 prototypeBean 안의 addCount()를 호출하는 거예요. 그 다음에 prototypeBean에서 getCount()로 count를 반환해주는 겁니다.

이렇게 하면 어쨌든 count가 0에서 1이 되고, 클라이언트 A는 1을 반환받게 됩니다.

그 다음에

클라이언트 B가 clientBean을 받아서 logic()을 호출해요. 그러면 addCount()를 호출하겠죠. 그럼 count 값은 어떻게 될까요?

기존에 있던 프로토타입을 그대로 사용합니다. 그래서 count가 1에서 2가 되고, 결과적으로 클라이언트는 2를 받아요.

여기서 중요한 점이 있는데, clientBean은 싱글톤 빈이니까, 클라이언트 A와 클라이언트 B가 같은 clientBean을 써요.

그런데 clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이에요.

주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 거기 때문에 logic() 안에서 프로토타입 빈을 사용할 때마다 새로 생성되는 게 아니예요. 지금 클라이언트 A, B가 같은 싱글톤 빈을 공유하고 있잖아요. 싱글톤 빈은 스프링 컨테이너가 올라올 때 생성돼서, 싱글톤 빈을 요청할 때마다 같은 애를 반환해줘요. 그럼 싱글톤 빈은 처음 생성될 때만 의존관계 주입으로 prototypeBean을 스프링 컨테이너가 생성해서 넣어주는 거예요. 그리고 더이상 스프링 컨테이너가 관리하지 않고, clientBean이 관리하겠죠.

그럼 그 뒤로 같은 clientBean이 사용되면서, 얘의 필드에 있는 prototypeBean도 계속 같은 애가 사용되는 거예요. final만 봐도 알 수 있죠. 생성자 주입이니까 불변인 거예요.

5번, 클라이언트 B는 clientBean.logic() 을 호출하면,
6번, clientBean 이 prototypeBean의 addCount() 를 호출하면서, 프로토타입 빈의 count를 증가시킵니다. 원래 count 값이 1이었으므로 2가 됩니다.


이제 테스트 코드를 한번 짜볼게요.

SingletonWithPrototypeTest1에서 추가로 singletonClientUsePrototype() 라는 테스트를 하나 더 만들겠습니다.

이제 스프링 컨테이너에 등록할 싱글톤 클래스를 만들어야 합니다.

그 다음에 프로토타입 빈을 주입받기 위해,

final로 하고 생성자로 의존관계 주입을 받습니다. @Autowired생성자가 하나기 때문에 생략해도 됩니다.

그게 아니면, @RequiredArgsConstructor 써서 해도 됩니다.

일단 이렇게 작성하면 됩니다. cmd + option + N 하면,

합칠 수도 있습니다.

그냥 다시 원상복구 해서, singletonClientUsePrototype()을 완성해보겠습니다.

스프링 빈으로 등록할 건 2개입니다. ClientBean.class, PrototypeBean.class 두 개 다 컴포넌트 스캔해서 자동으로 빈 등록을 해줘야 됩니다.

그리고 나서, clientBean1이 logic()을 호출하는 거예요.

그러면 count1의 값은 1이 되어야죠. 그리고 clientBean2가 똑같이 logic()을 호출하면,

count2의 값은 2가 됩니다. 돌려보면,

테스트가 성공했죠. '어? 분명히 프로토타입 빈은 새로 만들어지는거 아닌가요?' 라고 하실 수 있는데,

먼저, ClientBean싱글톤입니다.

일단 스프링이 ClientBean컴포넌트 스캔으로 자동으로 등록합니다. 그런데 빈으로 등록하면서,

생성자@Autowired가 있죠. 그러면, 이때 '프로토타입 빈을 내놔!' 라고 스프링 컨테이너에 내부적으로 요청을 하는 거예요. 그러면 스프링 컨테이너가 프로토타입 빈을 만들어서 그때 던져줘요.

그래서 생성시점에 주입이 되어있어요. 그래서 계속 같은 걸 쓰는 거예요. 당연하죠. 이 뒤로 같은 싱글톤 빈이 쓰이니까 또 주입받을 일이 없어요.

그럼 clientBean1이 logic()을 호출해요.

그러면 ClientBean의 logic()을 호출해도,

이 prototypeBean은 이미 생성시점에 주입된 그 prototypeBean을 쓰는 거예요.

그리고 clientBean2가 또 logic()을 호출해요.

그러면, 얘도 아까 생성시점에 주입된 걸 똑같이 쓰겠죠.

그래서 count가 2가 반환됩니다.


프로토타입 빈은 원래 스프링 컨테이너에서 새로 요청을 받을 때마다 생성을 해주는 건데, 싱글톤 빈에서 프로토타입 빈을 주입 받을때는 한번 생성돼서 들어온 게 싱글톤이라서 똑같은 게 계속 쓰이는 거예요.

그런데 만약에 이 로직을 호출할 때마다 프로토타입을 항상 새로 만들어서 쓰고 싶다는 의도가 있어요.

그럼 어떻게 해야될까요? 그건 다음 시간에 설명을 드릴거구요.

그런데 우리가 프로토타입 빈을 쓸 때는 원하는 의도 자체가 이런게 아니에요. 프로토타입 빈을 사용할 때는 계속 새로 만들어서 쓰고 싶어서 프로토타입 빈을 사용하는 거지, 이럴 거면 싱글톤 쓰지 왜 프로토타입 빈을 썼겠어요. 그러니까 이런 건 의도한 바가 아니예요.

그래서 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때 마다 새로 생성해서 사용하는 것을 원할 거예요.

그러면 이렇게 하면 되죠.

이렇게 하면 스프링 컨테이너를 주입받아서, logic()을 호출할 때마다 스프링 컨테이너가 새로운 프로토타입 빈을 생성하고, 그 프로토타입의 count를 증가시키는 거죠.

그러면 이렇게 수정해서 돌리면 테스트가 성공할 거예요.

그런데 이렇게 하게 되면, clientBean에서 ApplicationContext를 가지고 와야 되는데 너무 지저분하죠. 다시 원상복구 할게요.

그럼 어떻게 해결할까요? 굉장히 좋은 해결 방법이 있습니다.

암튼 방금 코드 에서 의도한 것은 프로토타입 빈을 사용할 때마다 항상 새로 생성되기를 원하는 거예요. 그런데 싱글톤은 주입할 때 처음 들어와버리니까 이게 안되죠.

그래서 해결 하려면, 어쨌든 스프링 컨테이너에다가 프로토타입 빈을 사용할 때마다 getBean() 해서 다시 요청을 해야 되는데, 그렇다고 방금처럼 스프링 ApplicationContext를 주입 받아서 사용하는 것은 되게 별로예요. 이런 건 너무 스프링에 의존적입니다.

그래서 해결방법은 다음 시간에 알아보고요.

스프링은 일반적으로 싱글톤 빈을 사용하기 때문에 결과적으로 싱글톤 빈이 프로토타입 빈을 사용하게 돼요.

그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되는 건 맞는데, 처음 싱글톤 빈이 의존관계 주입을 받을 때 생성된 프로토타입 빈이 계속 유지되는 것이 문제예요.

참고로 프로토타입 빈은 주입받을 때마다 새로 생성된다고 했죠. 여러 빈에서 같은 프로토타입 빈(PrototypeBean.class)을 의존관계 주입 받는다고 해봅시다

예를 들어서, Client A라는 빈이 있고, Client B라는 빈이 있어요. 그러면 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성됩니다. 스프링 컨테이너한테 요청할 때 프로토타입 빈이 생성되니깐요.

그래서 A와 B가 의존관계 주입을 받을 때, 각각 다른 인스턴스의 프로토타입 빈을 주입받겠죠.

이걸 코드로 보면,

코드로 보면, ClientBean2를 만든 거예요. 그리고 나서 생성자에서 prototypeBean을 주입받아요. 그러면 프로토타입 빈이 각각 x01, x02서로 다른 게 생긴다는 거예요.

왜냐면 스프링 컨테이너에 요청을 하니까, 요청 시점에 새로 만들어서 주입해주기 때문이죠.

그래서 싱글톤이랑 같이 사용될 때, 주입 받을 때마다는 새로 생성된다고 보면 됩니다.

하지만 이렇게 한다고 해서 우리가 원하는 건 아니죠. 우리가 바라는 대로, 사용할 때마다 계속 생성되는 건 아니예요.

'의존관계 주입을 받을 때마다 다른 프로토타입 빈을 새로 받아서 사용하고 싶어' 라는 경우는 사실 잘 없어요. 결국 프로토타입 빈을 사용하는 의도사용할 때마다 새로 생성해서 쓰고 이럴 때 필요한 겁니다.

그래서 다음 시간에는 해결 방법을 알아보겠습니다.