코드 그라데이션
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결 본문
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결
싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때 마다 항상 새로운 프로토타입 빈을 생성할 수 있을까?
스프링 컨테이너에 요청
가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다. 새로 받으면 됨.
public class PrototypeProviderTest {
@Test
void providerTest() {
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(1);
}
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
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");
}
}
}
핵심 코드
@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 가 만들어졌다.
package inflearn.spring_core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider; // 주입 바꿈
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); // 꺼내옴
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
// "prototype"으로 사용자 정의 범위인 PrototypeBean을 정의합니다.
@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");
}
}
}
핵심 코드
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
실행 결과
- 프로토타입 빈이 2개 생성됨을 확인할 수 있음.
대신 찾아주는 '대리자' 정도로 생각하라.
특징
- ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
- ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
스프링에 의존하지 않는 방법이 나온다.
JSR-330 Provider
- 마지막 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.
- 이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.16'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'inflearn'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '11'
}
//lombok 설정 추가 시작
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.junit.jupiter:junit-jupiter:5.8.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
implementation 'javax.inject:javax.inject:1'
}
tasks.named('test') {
useJUnitPlatform()
}
의존성 주입해주고 나면
/*
* Copyright (C) 2009 The JSR-330 Expert Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.inject;
/**
* Provides instances of {@code T}. Typically implemented by an injector. For
* any type {@code T} that can be injected, you can also inject
* {@code Provider<T>}. Compared to injecting {@code T} directly, injecting
* {@code Provider<T>} enables:
*
* <ul>
* <li>retrieving multiple instances.</li>
* <li>lazy or optional retrieval of an instance.</li>
* <li>breaking circular dependencies.</li>
* <li>abstracting scope so you can look up an instance in a smaller scope
* from an instance in a containing scope.</li>
* </ul>
*
* <p>For example:
*
* <pre>
* class Car {
* @Inject Car(Provider<Seat> seatProvider) {
* Seat driver = seatProvider.get();
* Seat passenger = seatProvider.get();
* ...
* }
* }</pre>
*/
public interface Provider<T> {
/**
* Provides a fully-constructed and injected instance of {@code T}.
*
* @throws RuntimeException if the injector encounters an error while
* providing an instance. For example, if an injectable member on
* {@code T} throws an exception, the injector may wrap the exception
* and throw it to the caller of {@code get()}. Callers should not try
* to handle such exceptions as the behavior may vary across injector
* implementations and even different configurations of the same injector.
*/
T get();
}
이게 들어온다.
전체 코드
package inflearn.spring_core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Provider;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider; // 주입 또 바꿈
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get(); // 여기도 바꿈
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
// "prototype"으로 사용자 정의 범위인 PrototypeBean을 정의합니다.
@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");
}
}
}
핵심 로직
돌려보면 두 개 다 들어온다.
특징
- get() 메서드 하나로 기능이 매우 단순하다.
- 별도의 라이브러리가 필요하다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
> 참고: 스프링이 제공하는 메서드에 @Lookup 애노테이션을 사용하는 방법도 있지만, 이전 방법들로 충분하고,
고려해야할 내용도 많아서 생략.
> 참고: 실무에서 자바 표준인 JSR-330 Provider를 사용할 것인지,
아니면 스프링이 제공하는ObjectProvider를 사용할 것인지 고민이 될 것이다.
ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고
스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 편리하다.
만약(정말 그럴일은 거의 없겠지만) 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면
JSR-330 Provider를 사용해야 한다.> 스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도
자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다.
대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면,
스프링이 제공하는 기능을 사용하면 된다.
'Spring > 핵심 원리 구현' 카테고리의 다른 글
request 스코프 예제 만들기 (1) | 2024.02.17 |
---|---|
웹 스코프 (0) | 2024.02.16 |
프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 문제점 (0) | 2024.02.15 |
프로토타입 스코프 (0) | 2024.02.14 |
빈 스코프란? (0) | 2024.02.13 |