티스토리 뷰
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 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 | 
 
                                                     
                                                     
                                                    