Spring ObjectProvider 지연 주입

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()은 확장 가능한 아키텍처의 핵심 도구입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux