CDI를 사용한 Quarkus의 종속성 주입: ArC, 범위, 한정자 및 빌드 시간 주입
DI(종속성 주입)는 현대 엔터프라이즈 Java 애플리케이션 개발의 기반입니다. 수년 동안 Java 개발자는 Spring IoC(Inversion of Control) 또는 Hibernate Weld 또는 OpenWebBeans와 같은 표준 Jakarta EE CDI(컨텍스트 및 종속성 주입) 컨테이너를 사용하여 구성 요소 수명 주기를 분리하고, 서비스 종속성을 관리하고, 구성 속성을 주입했습니다.
그러나 엔터프라이즈 애플리케이션이 클라우드 네이티브 마이크로서비스, Docker 및 Kubernetes를 사용한 컨테이너화, 서버리스 실행 모델(AWS Lambda, Knative)로 전환함에 따라 기존 종속성 주입 컨테이너는 과중한 런타임 반영 및 높은 메모리 오버헤드라는 치명적인 결함을 드러냈습니다.
Quarkus와 경량 CDI 엔진인 ArC를 만나보세요. Kubernetes 및 GraalVM 기본 컴파일을 위해 처음부터 구축된 Quarkus는 리플렉션, 클래스 경로 검색 및 Bean 그래프 연결을 애플리케이션 런타임에서 빌드 시간으로 전환하여 종속성 주입을 재정의합니다.
이 심층 가이드에서는 ArC 컨테이너, CDI 범위, 사용자 정의 한정자, 생산자, 인터셉터, 이벤트 기반 관찰자, 프로그래밍 방식 조회 및 고성능 클라우드 네이티브 Java 애플리케이션 작성을 위한 모범 사례의 내부 메커니즘을 다루는 CDI를 사용하여 Quarkus의 종속성 주입을 살펴보겠습니다.
1. ArC 이해하기: Quarkus 빌드타임 CDI 엔진
표준 자카르타 EE CDI 컨테이너는 애플리케이션 시작 시 완전히 작동합니다. 레거시 애플리케이션이 부팅되면 CDI 컨테이너는 클래스 경로의 모든 JAR을 스캔하고, 메타데이터를 구문 분석하고, Java Reflection(java.lang.reflect)을 사용하여 주석을 검사하고, 메모리에 동적 프록시를 생성하고, 종속성 그래프를 작성합니다. 이러한 동적 반영은 상당한 RAM(메타공간)을 소비하고 콜드 스타트 부팅 시간을 늘립니다.
Quarkus는 ArC(Jakarta CDI 4.0 사양을 기반으로 한 빌드 타임 CDI 구현)를 통해 이 병목 현상을 해결합니다.
ArC가 표준 CDI와 다른 점:
- 빌드 시간 확대:
mvn package또는./gradlew build중에 ArC는 모든 애플리케이션 Bean을 스캔하고 주입 지점의 유효성을 검사하며 종속성 그래프를 사전 구축합니다. - AOT(Ahead-Of-Time) 바이트코드 생성: ArC는 인스턴스화, Bean 프록시 및 인터셉터를 위한 직접 Java 바이트코드를 생성하여 런타임 시 반사를 제거합니다.
- 사용되지 않은 Bean 정리(데드 코드 제거): 동적으로 삽입되거나 참조되지 않은 모든 CDI Bean은 컴파일 중에 식별되고 최종 바이너리에서 자동으로 제거되어 메모리 공간이 줄어듭니다.
- GraalVM 네이티브 이미지 최적화: Bean 배선은 빌드 시 해결되므로 Quarkus 애플리케이션은 리플렉션 구성 힌트가 전혀 없는 네이티브 실행 파일로 원활하게 컴파일됩니다.
2. CDI 빈 범위 및 라이프사이클 관리
CDI에서 범위는 Bean 인스턴스의 수명 주기와 가시성을 나타냅니다. Quarkus ArC는 빌드 시간 최적화를 통해 표준 Jakarta CDI 범위를 지원합니다.
Quarkus의 기본 CDI 범위:
| 범위 주석 | 수명주기 및 동작 | 프록시 생성 | 최고의 사용 사례 |
|---|---|---|---|
@ApplicationScoped |
애플리케이션당 단일 인스턴스입니다. 첫 번째 호출 시 초기화가 지연됩니다. 모든 스레드에서 공유되는 상태입니다. | 클라이언트 프록시가 생성되었습니다. | 상태 비저장 서비스, 저장소, 구성. |
@RequestScoped |
단일 HTTP 요청 또는 이벤트 호출에 바인딩된 인스턴스입니다. 요청이 완료되면 폐기됩니다. | 클라이언트 프록시가 생성되었습니다. | 사용자 세션, 요청 컨텍스트 데이터, 보안 토큰. |
@종속 |
지정되지 않은 경우 기본 범위입니다. 인스턴스는 주입되는 Bean의 수명과 직접적으로 연결됩니다. | 프록시 없음(직접 인스턴스) | 도우미 유틸리티 클래스, 임시 상태 보유자입니다. |
@싱글턴 |
애플리케이션당 단일 인스턴스입니다. 열심히 또는 느리게 인스턴스화됩니다. 클라이언트 프록시 래퍼가 없습니다. | 프록시 없음(직접 인스턴스) | 클라이언트 프록시 오버헤드를 피해야 하는 고성능 싱글톤. |
@제거할 수 없음 |
빌드 시간 최적화 중에 빈을 제거하지 않도록 ArC에 지시하는 마커 주석입니다. | 범위에 따라 다름 | 동적 조회 대상 또는 반사 참조 Bean. |
코드 예: 범위가 지정된 Bean 선언
package com.ghaznix.services;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import java.util.UUID;
@ApplicationScoped
public class OrderProcessingService {
public String processOrder(String orderId) {
return "Order " + orderId + " processed successfully.";
}
}
@RequestScoped
public class RequestContextHolder {
private final String requestId = UUID.randomUUID().toString();
public String getRequestId() {
return requestId;
}
}
3. Quarkus의 종속성 주입 패턴
Quarkus는 표준 jakarta.inject.Inject 주석을 사용하여 다양한 주입 스타일을 지원합니다.
A. 생성자 주입(권장 모범 사례)
생성자 주입은 불변성(final 필드)을 보장하고 CDI 컨테이너 없이 단위 테스트를 단순화하며 종속성을 명시적으로 만듭니다.
package com.ghaznix.resources;
import com.ghaznix.services.OrderProcessingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/orders")
public class OrderResource {
private final OrderProcessingService orderService;
// In Quarkus, if a class has a single constructor, @Inject is OPTIONAL!
@Inject
public OrderResource(OrderProcessingService orderService) {
this.orderService = orderService;
}
@POST
@Path("/{id}")
public String checkout(@PathParam("id") String id) {
return orderService.processOrder(id);
}
}
프로 팁: Quarkus ArC에서 빈에 하나의 생성자만 있는 경우
@Inject주석을 완전히 생략할 수 있습니다. ArC는 이를 자동으로 주입 생성자로 인식합니다.
B. 필드 주입
필드 주입은 간결하지만 필드를 변경 가능하게 만들고 독립형 단위 테스트를 복잡하게 만듭니다.
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. 한정자 및 다중 구현 명확화
여러 Bean이 동일한 인터페이스를 구현하는 경우 CDI는 빌드 시 모호한 종속성 예외를 발생시킵니다. Quarkus는 Qualifiers를 사용하여 대상 Bean 구현을 안전하게 해결합니다.
맞춤 한정자 정의
package com.ghaznix.payment;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface PaymentType {
Mode value();
enum Mode {
CREDIT_CARD,
PAYPAL,
CRYPTO
}
}
적격 빈 구현하기
public interface PaymentProcessor {
void process(double amount);
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.CREDIT_CARD)
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing Credit Card payment: $" + amount);
}
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.PAYPAL)
public class PayPalProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
적격 빈 주입
@ApplicationScoped
public class CheckoutService {
private final PaymentProcessor creditCardProcessor;
private final PaymentProcessor payPalProcessor;
@Inject
public CheckoutService(
@PaymentType(PaymentType.Mode.CREDIT_CARD) PaymentProcessor creditCardProcessor,
@PaymentType(PaymentType.Mode.PAYPAL) PaymentProcessor payPalProcessor) {
this.creditCardProcessor = creditCardProcessor;
this.payPalProcessor = payPalProcessor;
}
}
5. 생산자 및 처리자: 타사 개체 주입
때로는 @ApplicationScoped 주석이 추가되지 않은 타사 라이브러리(예: AWS SDK 클라이언트, Redis 연결, 사용자 지정 구성 Bean)의 클래스를 삽입해야 합니다. Quarkus는 생산자 메서드 및 디스포저를 사용하여 이를 처리합니다.
package com.ghaznix.config;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@ApplicationScoped
public class DatabaseConnectionProducer {
@Produces
@ApplicationScoped
public Connection createDatabaseConnection() throws SQLException {
return DriverManager.getConnection("jdbc:postgresql://localhost:5432/mydb", "user", "pass");
}
// Disposer method called automatically when bean is destroyed
public void closeConnection(@Disposes Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
}
6. CDI 인터셉터: 관점 지향 프로그래밍(AOP)
인터셉터를 사용하면 비즈니스 로직을 오염시키지 않고 교차 관심사(로깅, 감사 추적, 메트릭, 속도 제한)를 Bean 메서드에 연결할 수 있습니다.
1단계: 인터셉터 바인딩 정의
package com.ghaznix.interceptors;
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface LogExecutionTime {
}
2단계: 인터셉터 로직 구현
package com.ghaznix.interceptors;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@LogExecutionTime
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LogExecutionTimeInterceptor {
@AroundInvoke
public Object logInvocation(InvocationContext context) throws Exception {
long startTime = System.currentTimeMillis();
try {
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
System.out.printf("[AUDIT] Method %s executed in %d ms%n",
context.getMethod().getName(), duration);
}
}
}
3단계: 대상 Bean에 주석 달기
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
Quarkus는 빌드 시 정적 인터셉터 프록시를 생성하므로 메서드 호출 오버헤드는 Spring Boot의 동적 CGLIB 런타임 프록시에 비해 사실상 0입니다.
7. CDI 이벤트를 사용한 이벤트 중심 아키텍처
CDI는 분리된 프로세스 내 통신을 위해 내장된 동기 및 비동기 이벤트 게시를 제공합니다.
게시 및 참관 이벤트
// 1. Define Event Payload
public record OrderCreatedEvent(String orderId, double totalAmount) {}
// 2. Publish Event
@ApplicationScoped
public class OrderService {
@Inject
Event<OrderCreatedEvent> orderEventPublisher;
public void createOrder(String orderId, double amount) {
// Create order logic...
orderEventPublisher.fire(new OrderCreatedEvent(orderId, amount));
}
}
// 3. Observe Event Synchronously or Asynchronously
@ApplicationScoped
public class InventoryObserver {
public void onOrderCreated(@Observes OrderCreatedEvent event) {
System.out.println("Updating inventory for order: " + event.orderId());
}
public void onOrderCreatedAsync(@ObservesAsync OrderCreatedEvent event) {
System.out.println("Async audit log for order: " + event.orderId());
}
}
애플리케이션 수명주기 이벤트
Quarkus는 애플리케이션이 시작되거나 종료될 때 초기화 코드를 실행하기 위해 시작 및 종료 이벤트를 노출합니다.
@ApplicationScoped
public class ApplicationLifecycleBean {
void onStart(@Observes StartupEvent ev) {
System.out.println("Quarkus Application starting up...");
}
void onStop(@Observes ShutdownEvent ev) {
System.out.println("Quarkus Application shutting down...");
}
}
8. Instance<T> 및 Arc.container()을 사용한 프로그래밍 방식의 Bean 조회
런타임 시(또는 레거시 비 CDI 인스턴스 내에서) 대상 구현을 동적으로 선택해야 하는 경우 Instance<T> 또는 Quarkus Arc.container() 정적 조회를 사용하세요.
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. 아키텍처 비교: Quarkus ArC, Spring IoC, Weld
| 특징 | 쿼커스 ArC | 스프링 IoC 컨테이너 | 표준 자카르타 용접 |
|---|---|---|---|
| 종속성 해결 | 빌드 시간(AOT) | 런타임 반영 | 런타임 반영 |
| 메타공간 RAM 오버헤드 | 매우 낮음(메타데이터의 경우 ~0MB) | 높음(동적 프록시 클래스 캐시) | 보통에서 높음 |
| 시작 시간 | 밀리초(< 0.05초) | 초(2.5초 - 8초) | 초(1.5초 - 4초) |
| GraalVM 기본 지원 | 즉시 사용 가능한 네이티브 | Spring 네이티브 힌트가 필요합니다. | 복잡한 구성 |
| 사용하지 않은 콩 가지치기 | 자동 빌드 시간 가지치기 | 클래스 경로에 보관됨 | 클래스 경로에 보관됨 |
| 단일 생성자 주입 | 암시적(주석이 필요하지 않음) | 지원됨 | @Inject가 필요합니다. |
10. @QuarkusTest 및 @InjectMock을 사용한 CDI 빈 단위 테스트
Quarkus는 JUnit 5 및 RestAssured를 사용하여 CDI 빈 테스트를 원활하게 만듭니다. 테스트 중에 종속성 빈을 Mockito 모의 객체로 자동 교체하려면 @InjectMock을 사용하세요.
package com.ghaznix;
import com.ghaznix.services.OrderProcessingService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class OrderServiceTest {
@Inject
OrderService orderService;
@InjectMock
OrderProcessingService orderProcessingService;
@Test
public void testOrderCheckout() {
Mockito.when(orderProcessingService.processOrder("ORD-101"))
.thenReturn("Mocked Success");
String result = orderService.checkout("ORD-101");
Assertions.assertEquals("Mocked Success", result);
}
}
11. 결론
CDI(ArC)를 사용하는 Quarkus의 종속성 주입은 엔터프라이즈 Java 설계에 획기적인 변화를 가져옵니다. Quarkus는 빈 메타데이터 스캐닝, 프록시 생성, 주입 그래프 해상도를 컴파일 시간으로 전환함으로써 초음속 시작 속도, 아원자 메모리 공간 및 원활한 GraalVM 기본 바이너리 지원을 제공합니다.
Spring Boot에서 마이그레이션하든 Kubernetes에서 그린필드 클라우드 네이티브 마이크로서비스를 구축하든 Quarkus CDI를 마스터하면 최신 소프트웨어 아키텍처에 필요한 성능과 개발자 생산성을 얻을 수 있습니다.
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.