ObjectProvider란?
Spring에서 의존성 주입(DI)은 보통 @Autowired나 생성자 주입으로 처리합니다. 하지만 빈이 존재하지 않을 수도 있거나, 여러 후보 중 조건부로 선택해야 하거나, 지연 초기화가 필요한 경우에는 ObjectProvider<T>가 훨씬 유연합니다. Spring 4.3에서 도입된 이 인터페이스는 타입 안전한 지연·선택적 의존성 주입을 제공합니다.
기본 사용법: 선택적 빈 주입
@Autowired(required = false) 대신 ObjectProvider를 사용하면 null 체크 없이 안전하게 처리할 수 있습니다:
@Service
public class NotificationService {
private final ObjectProvider<EmailSender> emailSenderProvider;
private final ObjectProvider<SmsSender> smsSenderProvider;
public NotificationService(
ObjectProvider<EmailSender> emailSenderProvider,
ObjectProvider<SmsSender> smsSenderProvider) {
this.emailSenderProvider = emailSenderProvider;
this.smsSenderProvider = smsSenderProvider;
}
public void notify(String message) {
// 빈이 있으면 실행, 없으면 무시
emailSenderProvider.ifAvailable(
sender -> sender.send(message));
// 빈이 없으면 기본값 사용
SmsSender sms = smsSenderProvider
.getIfAvailable(NoopSmsSender::new);
sms.send(message);
}
}
주요 메서드 비교:
| 메서드 | 빈 없음 | 빈 1개 | 빈 여러 개 |
|---|---|---|---|
getObject() |
NoSuchBeanException | 반환 | NoUniqueBeanException |
getIfAvailable() |
null | 반환 | NoUniqueBeanException |
getIfAvailable(Supplier) |
기본값 | 반환 | NoUniqueBeanException |
getIfUnique() |
null | 반환 | null |
ifAvailable(Consumer) |
무시 | 실행 | NoUniqueBeanException |
stream() |
빈 Stream | Stream(1개) | Stream(전부) |
orderedStream() |
빈 Stream | Stream(1개) | @Order 정렬된 Stream |
다중 빈 처리: 전략 패턴 구현
같은 인터페이스를 구현한 여러 빈을 우선순위에 따라 순회하거나 조건부로 선택하는 패턴입니다:
public interface PaymentProcessor {
boolean supports(PaymentMethod method);
PaymentResult process(PaymentRequest request);
int priority(); // 낮을수록 우선
}
@Component @Order(1)
public class CardPaymentProcessor implements PaymentProcessor {
public boolean supports(PaymentMethod m) {
return m == PaymentMethod.CARD;
}
// ...
}
@Component @Order(2)
public class BankTransferProcessor implements PaymentProcessor {
public boolean supports(PaymentMethod m) {
return m == PaymentMethod.BANK_TRANSFER;
}
// ...
}
@Service
public class PaymentService {
private final ObjectProvider<PaymentProcessor> processors;
public PaymentService(
ObjectProvider<PaymentProcessor> processors) {
this.processors = processors;
}
public PaymentResult pay(PaymentRequest request) {
// @Order 순서대로 정렬된 스트림에서 조건 매칭
return processors.orderedStream()
.filter(p -> p.supports(request.getMethod()))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentException(
"No processor for: " + request.getMethod()))
.process(request);
}
// 모든 프로세서 상태 조회
public List<String> getAvailableProcessors() {
return processors.orderedStream()
.map(p -> p.getClass().getSimpleName())
.toList();
}
}
지연 초기화: 순환 참조 해결
두 빈이 서로를 참조하는 순환 의존성(circular dependency) 문제를 ObjectProvider로 깔끔하게 해결할 수 있습니다:
// ❌ 순환 참조 발생
@Service
public class OrderService {
private final InventoryService inventoryService; // → 순환!
public OrderService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
}
@Service
public class InventoryService {
private final OrderService orderService; // → 순환!
public InventoryService(OrderService orderService) {
this.orderService = orderService;
}
}
// ✅ ObjectProvider로 지연 해결
@Service
public class OrderService {
private final ObjectProvider<InventoryService> inventoryProvider;
public OrderService(
ObjectProvider<InventoryService> inventoryProvider) {
this.inventoryProvider = inventoryProvider;
}
public void processOrder(Order order) {
// 실제 사용 시점에 빈을 가져옴 (지연 조회)
InventoryService inventory = inventoryProvider.getObject();
inventory.reserve(order.getItems());
}
}
주의: 순환 참조 자체가 설계 문제일 수 있습니다. ObjectProvider는 임시 해결책이며, 가능하면 이벤트 기반 설계로 의존성을 끊는 것이 좋습니다.
Prototype 스코프 빈과 ObjectProvider
Singleton 빈에서 Prototype 스코프 빈을 주입받으면, 한 번만 생성되어 Prototype의 의미가 사라집니다. ObjectProvider를 사용하면 매번 새 인스턴스를 얻을 수 있습니다:
@Component
@Scope("prototype")
public class RequestContext {
private final String requestId = UUID.randomUUID().toString();
private final Map<String, Object> attributes = new HashMap<>();
public String getRequestId() { return requestId; }
public void setAttribute(String key, Object value) {
attributes.put(key, value);
}
}
@Service // Singleton
public class RequestHandler {
private final ObjectProvider<RequestContext> contextProvider;
public RequestHandler(
ObjectProvider<RequestContext> contextProvider) {
this.contextProvider = contextProvider;
}
public void handle(HttpServletRequest request) {
// 매 호출마다 새로운 RequestContext 인스턴스
RequestContext ctx = contextProvider.getObject();
ctx.setAttribute("path", request.getRequestURI());
process(ctx);
}
}
@Conditional과 조합: 플러그인 아키텍처
특정 조건에서만 등록되는 빈을 ObjectProvider로 안전하게 사용하는 패턴입니다:
// Redis가 설정된 경우에만 등록
@Component
@ConditionalOnProperty(name = "cache.type", havingValue = "redis")
public class RedisCacheStore implements CacheStore {
// Redis 캐시 구현
}
// 항상 등록되는 기본 캐시
@Component
public class InMemoryCacheStore implements CacheStore {
// 인메모리 캐시 구현
}
@Service
public class CacheService {
private final CacheStore cacheStore;
public CacheService(ObjectProvider<CacheStore> providers) {
// Redis가 있으면 Redis, 없으면 InMemory
this.cacheStore = providers.orderedStream()
.findFirst()
.orElseGet(InMemoryCacheStore::new);
}
}
이 패턴은 Spring 조건부 빈 등록과 함께 사용하면 매우 강력합니다.
ObjectProvider vs @Lazy vs Optional
비슷해 보이는 세 가지 접근법의 차이를 명확히 이해해야 합니다:
// 1. @Lazy — 프록시를 통한 지연 초기화
@Service
public class ServiceA {
private final @Lazy ServiceB serviceB;
// 첫 호출 시 실제 초기화. 빈이 없으면 예외.
}
// 2. Optional — 선택적 주입 (존재 여부만)
@Service
public class ServiceA {
private final Optional<ServiceB> serviceB;
// 빈 있으면 Optional.of(), 없으면 Optional.empty()
// 주입 시점에 즉시 조회. 다중 빈 불가.
}
// 3. ObjectProvider — 가장 유연
@Service
public class ServiceA {
private final ObjectProvider<ServiceB> serviceB;
// 지연 조회 + 선택적 + 다중 빈 + 기본값 + 스트리밍
// 사용 시점마다 조회 가능 (Prototype에 적합)
}
| 기능 | @Lazy | Optional | ObjectProvider |
|---|---|---|---|
| 지연 초기화 | ✅ | ❌ | ✅ |
| 빈 없어도 안전 | ❌ | ✅ | ✅ |
| 다중 빈 처리 | ❌ | ❌ | ✅ |
| 기본값 지정 | ❌ | ✅ | ✅ |
| Prototype 스코프 | ❌ | ❌ | ✅ |
| 정렬된 스트리밍 | ❌ | ❌ | ✅ |
테스트에서의 활용
테스트 시 ObjectProvider를 모킹하거나, 테스트 전용 빈을 조건부로 주입하는 패턴:
@SpringBootTest
class PaymentServiceTest {
@MockBean
private CardPaymentProcessor cardProcessor;
@Autowired
private PaymentService paymentService;
@Test
void shouldUseCardProcessor() {
when(cardProcessor.supports(PaymentMethod.CARD))
.thenReturn(true);
when(cardProcessor.process(any()))
.thenReturn(PaymentResult.success());
PaymentResult result = paymentService.pay(
new PaymentRequest(PaymentMethod.CARD, 10000));
assertThat(result.isSuccess()).isTrue();
}
}
// 직접 ObjectProvider를 모킹하는 단위 테스트
@Test
void shouldFallbackWhenNoProcessor() {
@SuppressWarnings("unchecked")
ObjectProvider<PaymentProcessor> provider =
mock(ObjectProvider.class);
when(provider.orderedStream()).thenReturn(Stream.empty());
PaymentService service = new PaymentService(provider);
assertThatThrownBy(() ->
service.pay(new PaymentRequest(PaymentMethod.CARD, 10000)))
.isInstanceOf(UnsupportedPaymentException.class);
}
마치며
ObjectProvider는 Spring DI의 “스위스 아미 나이프”입니다. 선택적 주입, 지연 조회, 다중 빈 처리, Prototype 스코프 관리를 하나의 인터페이스로 해결합니다. @Autowired(required=false)나 Optional이 부족할 때, 전략 패턴이나 플러그인 아키텍처를 구현할 때, 순환 참조를 임시로 해결할 때 ObjectProvider를 활용하세요. 특히 orderedStream()은 확장 가능한 아키텍처의 핵심 도구입니다.