Aspectran Beans: The Official Guide

Aspectran Beans는 Aspectran 프레임워크의 핵심에 내장된 강력한 IoC(Inversion of Control) 컨테이너입니다. Spring Beans의 견고한 개념(IoC, DI 등)에서 영감을 받았지만, POJO 기반, 단순함, 그리고 빠른 개발 및 구동 속도라는 Aspectran의 핵심 철학에 맞춰 처음부터 다시 설계되었습니다.


1. 핵심 개념: IoC와 DI

Aspectran Beans의 핵심은 애플리케이션의 객체(“빈”이라 불림)를 관리하여 더 깨끗하고, 모듈화되고, 테스트하기 쉬운 코드를 작성하도록 돕는 것입니다.

  • IoC (Inversion of Control, 제어의 역전): 개발자가 객체의 생명주기를 직접 생성하고 관리하는 대신, Aspectran 컨테이너가 이를 대신합니다. 개발자는 객체를 정의하기만 하면, 프레임워크가 적절한 시점에 객체를 인스턴스화, 설정 및 조립합니다. 이러한 제어의 “역전”을 통해 개발자는 비즈니스 로직에만 집중할 수 있습니다.

  • DI (Dependency Injection, 의존성 주입): IoC를 구현하는 주요 메커니즘입니다. 객체가 자신의 의존성을 직접 생성하는 대신(new MyService()), 외부 소스(IoC 컨테이너)로부터 의존성을 “주입”받습니다. 이를 통해 컴포넌트 간의 결합도를 낮추어 관리, 테스트, 재사용이 더 쉬워집니다.


2. 기본: 빈(Bean) 정의와 스코프

@Component를 사용한 자동 탐지

빈을 등록하는 가장 쉬운 방법은 클래스에 @Component 어노테이션을 추가하는 것입니다. 애플리케이션 시작 시 Aspectran의 클래스패스 스캐너가 이를 자동으로 탐지하여 빈으로 등록합니다.

package com.example.myapp.service;

import com.aspectran.core.component.bean.annotation.Component;

@Component
public class MyService {
    public String getMessage() {
        return "Hello from MyService!";
    }
}

@Bean을 사용한 명시적 정의

@Bean 어노테이션은 빈을 명시적으로 선언하고 ID나 스코프 같은 세부 속성을 지정할 때 사용됩니다. 클래스나 팩토리 메소드에 적용할 수 있습니다.

  • 클래스에 사용: @Component와 함께 사용하여 빈의 ID를 지정할 수 있습니다.
    @Component
    @Bean(id = "anotherService")
    public class AnotherService { /* ... */ }
    
  • 팩토리 메소드에 사용: 복잡한 초기화 로직이나 서드파티 라이브러리 객체를 빈으로 등록할 때 유용합니다. @Component 클래스 내부에 객체를 반환하는 메소드를 만들고 @Bean을 붙입니다.
    @Component
    public class AppConfig {
        @Bean
        public SomeLibraryClient someLibraryClient() {
            return new SomeLibraryClient("api.example.com", "your-api-key");
        }
    }
    

빈 스코프(Bean Scopes) 심층 분석

빈 스코프는 빈 인스턴스의 생명주기와 가시성을 제어합니다. @Scope 어노테이션으로 설정할 수 있습니다.

스코프 (Scope)설명생명주기주요 사용 사례
singleton컨텍스트 내 단일 인스턴스애플리케이션 전체상태 없는 서비스, DAO
prototype요청 시마다 새 인스턴스GC에 의해 관리상태 있는 객체, Builder
request요청마다 새 인스턴스단일 Activity 실행요청 관련 데이터 처리
session세션마다 새 인스턴스단일 사용자 세션사용자별 데이터 관리
  • singleton (기본값): IoC 컨테이너 내에서 단 하나의 인스턴스만 생성되어 공유됩니다.
  • prototype: 빈을 주입받거나 요청할 때마다 매번 새로운 인스턴스가 생성됩니다. 컨테이너는 생성 이후 생명주기를 관리하지 않습니다.
  • request: Activity 실행(예: HTTP 요청) 범위 내에서 단일 인스턴스가 유지됩니다. 현재 ActivityRequestAdapter를 지원해야 합니다.
  • session: 사용자 세션 범위 내에서 단일 인스턴스가 유지됩니다. 현재 ActivitySessionAdapter를 지원해야 합니다.
import com.aspectran.core.component.bean.annotation.Scope;
import com.aspectran.core.context.rule.type.ScopeType;

@Component
@Bean
@Scope(ScopeType.PROTOTYPE)
public class MyPrototypeBean { /* ... */ }

3. 핵심: 의존성 주입 (Dependency Injection)

@Autowired 어노테이션을 사용하여 빈 간의 의존성을 주입합니다.

생성자 주입 (권장)

의존성을 불변(immutable)으로 만들고, 객체가 생성될 때 완전한 상태임을 보장하는 가장 좋은 방법입니다.

@Component
public class MyController {
    private final MyService myService;

    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }
}

필드 및 수정자(Setter) 주입

선택적 의존성을 주입할 때 유용하지만, 생성자 주입을 우선적으로 고려해야 합니다.

  • 수정자(Setter) 주입: public 수정자 메소드에 @Autowired를 붙입니다.
  • 필드 주입: public 필드에만 주입 가능하며, 권장되지 않습니다.

@Qualifier로 모호성 해결

동일한 타입의 빈이 여러 개 있을 때, @Qualifier("beanId")를 사용하여 주입할 특정 빈을 지정할 수 있습니다.

public interface NotificationService { /* ... */ }

@Component @Bean("email")
public class EmailNotificationService implements NotificationService { /* ... */ }

@Component @Bean("sms")
public class SmsNotificationService implements NotificationService { /* ... */ }

@Component
public class OrderService {
    private final NotificationService notificationService;

    @Autowired
    public OrderService(@Qualifier("email") NotificationService notificationService) {
        this.notificationService = notificationService;
    }
}

@Value로 설정값 주입

@Value 어노테이션을 사용하여 AsEL 표현식의 평가 결과(주로 외부 설정값)를 주입할 수 있습니다.

@Component
public class AppInfo {
    private final String appVersion;

    @Autowired
    public AppInfo(@Value("%{app^version:1.0.0}") String appVersion) {
        this.appVersion = appVersion;
    }
}

컬렉션 주입 (List<T>, Map<String, T>)

동일한 인터페이스를 구현하는 모든 빈을 ListMap으로 한 번에 주입받을 수 있습니다. 이는 전략 패턴(Strategy Pattern) 등을 구현할 때 매우 유용합니다.

// 모든 NotificationService 구현체를 주입받음
@Component
public class NotificationManager {
    private final List<NotificationService> services;
    private final Map<String, NotificationService> serviceMap;

    @Autowired
    public NotificationManager(List<NotificationService> services) {
        this.services = services; // [EmailNotificationService, SmsNotificationService]
        this.serviceMap = services.stream()
                .collect(Collectors.toMap(s -> s.getClass().getSimpleName(), s -> s));
    }

    public void sendToAll(String message) {
        for (NotificationService service : services) {
            service.send(message);
        }
    }
}

선택적 의존성 주입 (Optional<T>)

특정 프로파일에서만 활성화되는 등, 존재하지 않을 수도 있는 빈을 주입받아야 할 때 java.util.Optional<T>을 사용할 수 있습니다.

@Component
public class MainService {
    private final Optional<OptionalService> optionalService;

    @Autowired
    public MainService(Optional<OptionalService> optionalService) {
        this.optionalService = optionalService;
    }

    public void doSomething() {
        optionalService.ifPresent(service -> service.performAction());
    }
}

4. 고급 기능

프로파일(@Profile)을 이용한 환경별 설정

@Profile 어노테이션을 사용하면 특정 프로파일(예: dev, prod)이 활성화되었을 때만 빈을 등록하도록 할 수 있습니다.

// 개발 환경에서만 사용될 Mock 서비스
@Component
@Profile("dev")
public class MockNotificationService implements NotificationService { /* ... */ }

// 운영 환경에서 실제 SMS를 발송하는 서비스
@Component
@Profile("prod")
public class RealSmsNotificationService implements NotificationService { /* ... */ }

활성화할 프로파일은 Aspectran 설정에서 지정할 수 있습니다.

FactoryBean으로 복잡한 빈 생성하기

생성 로직이 매우 복잡하거나 캡슐화가 필요할 때 FactoryBean 인터페이스를 구현합니다. getObject() 메소드가 반환하는 객체가 실제 빈으로 등록됩니다.

@Component
@Bean("myProduct")
public class MyProductFactory implements FactoryBean<MyProduct> {
    @Override
    public MyProduct getObject() throws Exception {
        // 복잡한 생성 및 설정 로직
        return new MyProduct();
    }
}

Aware 인터페이스로 프레임워크에 접근하기

ActivityContextAware와 같은 Aware 인터페이스를 구현하면, 빈이 Aspectran의 내부 객체(e.g., ActivityContext)에 접근할 수 있습니다.

@Component
public class MyAwareBean implements ActivityContextAware {
    private ActivityContext context;

    @Override
    public void setActivityContext(ActivityContext context) {
        this.context = context;
    }
}

이벤트 발행 및 구독 (Event Handling)

Aspectran은 애플리케이션 내의 컴포넌트(빈) 간의 느슨한 결합(loosely coupled)을 위해 발행-구독(Publish-Subscribe) 방식의 이벤트 처리 메커니즘을 제공합니다. 이를 통해 특정 로직의 수행 결과를 다른 여러 컴포넌트에 전파해야 할 때, 직접 의존 관계를 맺지 않고 이벤트를 통해 간단하게 구현할 수 있습니다.

이벤트 리스너 만들기 (@EventListener)

이벤트를 수신하여 처리하는 리스너는 @EventListener 어노테이션을 사용하여 간단하게 만들 수 있습니다.

  • 이벤트를 처리할 메소드에 @EventListener 어노테이션을 붙입니다.
  • 해당 메소드는 반드시 하나의 파라미터를 가져야 하며, 이 파라미터의 타입이 구독할 이벤트의 타입이 됩니다.
  • 프레임워크가 시작될 때, @Component로 등록된 빈들에서 @EventListener가 붙은 메소드를 찾아 자동으로 이벤트 리스너로 등록합니다.

예시: 주문 완료 이벤트를 처리하는 리스너

// 1. 이벤트 정의 (POJO)
public class OrderCompletedEvent {
    private final String orderId;

    public OrderCompletedEvent(String orderId) {
        this.orderId = orderId;
    }

    public String getOrderId() {
        return orderId;
    }
}

// 2. 이벤트 리스너 빈 정의
@Component
public class OrderEventListener {

    @EventListener
    public void handleOrderCompleted(OrderCompletedEvent event) {
        // 주문 완료 이벤트가 발행되면 이 메소드가 호출됩니다.
        System.out.println("Order [" + event.getOrderId() + "] has been completed.");
        // ... 재고 감소, 배송 알림 등의 후속 처리 로직 ...
    }

    @EventListener
    public void handleAnyObject(Object event) {
        // 모든 타입의 이벤트를 수신하려면 Object 타입으로 선언할 수 있습니다.
    }
}

이벤트 발행하기 (EventPublisher)

이벤트 발행은 EventPublisher 인터페이스를 통해 이루어집니다. 이 타입의 빈을 주입받아 publish() 메소드를 호출하기만 하면 됩니다.

  • 이벤트 발행 빈을 정의하고, 해당 빈을 주입받습니다.
  • publish(Object event) 메소드를 호출하여 이벤트를 발행합니다.
  • EventPublisher는 발행된 이벤트 객체의 타입을 확인하고, 해당 이벤트를 구독하는 모든 @EventListener에게 이벤트를 전파합니다.

예시: 주문 서비스에서 주문 완료 이벤트 발행

// 주문 이벤트 발행 빈을 명시적으로 정의
@Component
public class OrderEventPublisher extends InstantActivitySupport {

    // 주문 완료 이벤트 발행
    public void publish(OrderCompletedEvent orderCompletedEvent) {
        getEventPublisher().publish(orderCompletedEvent);
    }

    // 주문 취소 이벤트 발행
    public void publish(OrderCanceledEvent orderCanceledEvent) {
        getEventPublisher().publish(orderCanceledEvent);
    }
}

@Component
public class OrderService {

    private final EventPublisher orderEventPublisher;

    @Autowired
    public OrderService(OrderEventPublisher orderEventPublisher) {
        this.eventPublisher = orderEventPublisher;
    }

    public void completeOrder(String orderId) {
        // ... 주문 완료 처리 로직 ...
        System.out.println("Processing completion for order [" + orderId + "]");

        // 이벤트 생성 및 발행
        OrderCompletedEvent event = new OrderCompletedEvent(orderId);
        this.orderEventPublisher.publish(event);
    }

    public void cancelOrder(String orderId) {
        // ... 주문 취소 처리 로직 ...
        System.out.println("Processing cancellation for order [" + orderId + "]");

        // 이벤트 생성 및 발행
        OrderCanceledEvent event = new OrderCanceledEvent(orderId);
        this.orderEventPublisher.publish(event);
    }
}

이처럼 이벤트 메커니즘을 활용하면, OrderService는 주문 완료 후 어떤 작업들이 수행되어야 하는지 알 필요 없이 자신의 핵심 책임에만 집중할 수 있습니다. 이벤트에 관심 있는 다른 컴포넌트들이 @EventListener를 통해 작업을 이어가므로, 시스템의 유연성과 확장성이 크게 향상됩니다.

비동기 메소드 실행 (@Async)

@Async 어노테이션을 사용하면, 시간이 오래 걸리는 작업을 별도의 스레드에서 비동기적으로 실행하여 현재 요청 처리 스레드를 차단하지 않고 즉시 반환할 수 있습니다. 이 기능은 Aspectran의 빈 프록시(Bean Proxy)를 통해 구현됩니다.

@Async 기본 사용법

Bean의 메소드에 @Async 어노테이션을 추가하면 해당 메소드는 별도의 스레드에서 비동기로 호출됩니다. 반환 타입은 void 또는 java.util.concurrent.Future의 구현체여야 합니다.

@Component
@Bean("myAsyncTaskService")
public class MyAsyncTaskService {

    @Async
    public void doSomething() {
        // 이 코드는 별도의 스레드에서 실행됩니다.
    }

    @Async
    public Future<String> doSomethingAndReturn() {
        // 작업을 실행하고 Future 객체를 통해 결과를 반환합니다.
        return new CompletableFuture<>(() -> "Hello from async task!");
    }
}

비동기 컨텍스트와 ProxyActivity

  • @Async 메소드가 호출될 때, 현재 스레드에 Activity가 없으면 어드바이스 실행을 위한 경량 컨텍스트인 ProxyActivity가 새로 생성됩니다.
  • 하나의 비동기 작업 내에서 여러 @Advisable 메소드가 연쇄적으로 호출될 경우, 최초에 생성된 ProxyActivity 인스턴스가 해당 스레드 내에서 계속 공유됩니다. 이를 통해 작업 단위 내에서 일관된 컨텍스트를 유지할 수 있습니다.
  • 만약 기존 Activity가 존재하는 스레드에서 @Async가 호출되면, ProxyActivity는 기존 Activity래핑(wrapping)하여 생성됩니다. 이 경우, 원본 Activity의 데이터(ActivityData)를 공유하게 되어 비동기 작업과 호출자 간의 데이터 교환이 가능해집니다.

CompletableFuture 사용 시 주의사항

@Async 메소드 내에서 CompletableFuture.supplyAsync()thenApplyAsync()와 같이 새로운 스레드 풀에서 코드를 실행하는 CompletableFuture의 조합을 사용할 경우, Aspectran의 Activity 컨텍스트가 해당 스레드로 전파되지 않습니다. 즉, CompletableFuture가 만드는 새로운 스레드에서는 getCurrentActivity()를 호출하면 NoActivityStateException이 발생합니다.

@Async에 의해 생성된 스레드 내에서 모든 작업을 동기적으로 처리하고 최종 결과만 CompletableFuture.completedFuture()로 감싸서 반환하는 것이 안전합니다.

@Async
public Future<String> correctUsage() {
    // 이 블록은 @Async에 의해 관리되는 스레드에서 실행되므로 Activity 컨텍스트에 접근 가능
    getCurrentActivity().getActivityData().put("key", "value");

    // CompletableFuture를 단순히 결과 전달용으로만 사용
    return CompletableFuture.completedFuture("some-result");
}

@Async
public Future<String> wrongUsage() {
    // 잘못된 사용 예: supplyAsync 내부에서는 Activity 컨텍스트에 접근할 수 없음
    return CompletableFuture.supplyAsync(() -> {
        // 이 블록은 별도의 스레드에서 실행되므로,
        // getCurrentActivity()를 호출하면 NoActivityStateException이 발생합니다.
        getCurrentActivity().getActivityData().put("key", "value"); // 예외 발생!
        return "some-result";
    });
}

사용자 정의 Executor 사용

기본 Executor 대신 별도의 스레드 풀 정책을 적용하고 싶다면, AsyncTaskExecutor 타입의 Bean을 직접 정의하고 @Async 어노테이션에 해당 Bean의 ID나 클래스를 지정할 수 있습니다.

// "myCustomExecutor"라는 ID로 등록된 Executor 사용
@Async("myCustomExecutor")
public void doSomethingWithCustomExecutor() {
    // ...
}

5. 빈 생명주기(Lifecycle) 관리

전체 생명주기 순서

싱글톤 빈은 다음과 같은 순서로 생성되고 소멸됩니다.

  1. 인스턴스화: 생성자 호출
  2. 의존성 주입: @Autowired가 붙은 필드 및 수정자(setter)에 의존성 주입
  3. Aware 인터페이스 처리: Aware 인터페이스의 set*() 메소드 호출
  4. 초기화 콜백 (Post-Initialization):
    • @Initialize 어노테이션이 붙은 메소드 호출
    • InitializableBean 인터페이스의 initialize() 메소드 호출
  5. (빈 사용 가능 상태)
  6. 소멸 전 콜백 (Pre-Destruction):
    • @Destroy 어노테이션이 붙은 메소드 호출
    • DisposableBean 인터페이스의 destroy() 메소드 호출

어노테이션 기반 콜백: @Initialize & @Destroy

  • @Initialize: 모든 의존성이 주입된 후 초기화 로직을 실행합니다.
  • @Destroy: 빈이 소멸되기 직전 정리 로직을 실행합니다.
@Component
public class LifecycleBean {
    @Initialize
    public void setup() { /* ... */ }

    @Destroy
    public void cleanup() { /* ... */ }
}

인터페이스 기반 콜백: InitializableBean & DisposableBean

프레임워크 인터페이스를 직접 구현하여 동일한 목적을 달성할 수도 있습니다.

@Component
public class LifecycleBean implements InitializableBean, DisposableBean {
    @Override
    public void initialize() throws Exception { /* ... */ }

    @Override
    public void destroy() throws Exception { /* ... */ }
}

6. 구성 설정 (Configuration)

어노테이션 기반 설정 활성화

어노테이션을 사용한 빈을 활성화하려면, Aspectran의 메인 설정 파일(APON 형식)에서 context.scan 파라미터에 스캔할 기본 패키지를 지정해야 합니다.

context: {
    scan: [
        com.example.myapp
    ]
}

XML 기반 설정

XML을 사용하면 소스 코드 변경 없이 빈의 구성과 관계를 정의할 수 있어 유연성이 높습니다.

기본 정의 및 의존성 주입

<bean> 요소로 빈을 정의하고, <argument>(생성자 주입)와 <property>(수정자 주입) 자식 요소를 사용하여 의존성을 설정합니다.

<bean id="myService" class="com.example.myapp.service.MyService"/>

<bean id="myController" class="com.example.myapp.controller.MyController">
    <!-- 생성자 인자 주입 -->
    <argument>#{myService}</argument>
    <!-- 수정자(Setter) 속성 주입 -->
    <property name="timeout" value="5000"/>
</bean>

프로파일을 이용한 조건부 아이템 그룹화

여러 개의 <argument> 또는 <property> 요소들을 특정 프로파일에서만 함께 활성화하거나 비활성화해야 할 경우, <arguments> 또는 <properties> 래퍼(wrapper) 요소를 사용할 수 있습니다. 이 래퍼 요소에 profile 속성을 지정하면, 내부에 포함된 모든 <item> 요소들이 해당 프로파일에 종속됩니다.

<bean id="dbConnector" class="com.example.DbConnector">
    <properties profile="dev">
        <item name="url" value="jdbc:h2:mem:devdb"/>
        <item name="username" value="sa"/>
    </properties>
    <properties profile="prod">
        <item name="url" value="jdbc:mysql://prod.db.server/main"/>
        <item name="username" value="prod_user"/>
    </properties>
</bean>

개별 아이템을 정의할 때는 <argument>/<property>를 사용하고, 여러 아이템을 프로파일에 따라 그룹화할 때만 <arguments>/<properties>를 사용하는 것이 권장되는 스타일입니다.

컴포넌트 스캔 (<bean scan="...">)

XML에서도 <bean scan="...">을 사용하여 컴포넌트 스캔을 활성화할 수 있습니다.

<!-- 'com.example.myapp' 패키지와 그 하위 패키지를 모두 스캔 -->
<bean scan="com.example.myapp.**"/>

내부 빈과 중첩 제한

다른 빈의 속성으로만 사용될 익명의 내부 빈을 정의할 수 있습니다. Aspectran은 설정의 과도한 복잡성을 방지하기 위해 내부 빈의 최대 중첩을 3단계(depth)로 제한합니다.

<bean id="outerBean" class="com.example.OuterBean">
    <properties>
        <item name="inner">
            <!-- ID가 없는 내부 빈 (1단계) -->
            <bean class="com.example.InnerBean">
                <!-- ... -->
            </bean>
        </item>
    </properties>
</bean>

어노테이션과 XML 설정의 조합

어노테이션 기반의 컴포넌트 스캔과 XML 기반의 명시적 빈 정의를 함께 사용할 수 있습니다. 일반적으로 컴포넌트 스캔을 기본으로 사용하고, 특정 빈을 재정의하거나 외부 라이브러리를 등록할 때 XML을 사용합니다. 동일한 ID의 빈이 둘 다에 정의된 경우, 나중에 로드되는 설정이 우선권을 가질 수 있으며, <bean important="true"> 속성으로 덮어쓰기를 강제할 수 있습니다.


7. Best Practices 및 흔한 실수 (Pitfalls)

생성자 주입을 선호하세요

  • 불변성(Immutability): final 필드를 사용할 수 있어 빈의 상태가 변경되지 않음을 보장합니다.
  • 의존성 명시: 객체가 기능하는 데 필요한 모든 의존성이 생성자에 명확하게 드러납니다.
  • 순환 참조 방지: 생성자 주입을 사용할 경우, 빈 A와 B가 서로를 필요로 하는 순환 참조가 발생하면 애플리케이션 시작 시점에 오류가 발생하여 문제를 즉시 발견할 수 있습니다.

순환 의존성을 피하세요

순환 의존성은 설계상의 문제를 나타내는 신호일 수 있습니다. 두 클래스가 서로 너무 많은 책임을 지고 있다는 의미일 수 있으므로, 책임을 분리하여 제3의 클래스로 옮기는 리팩토링을 고려하세요. 불가피한 경우, 수정자(setter) 주입을 사용하면 순환 참조 문제를 해결할 수 있습니다.

prototype 빈의 생명주기를 이해하세요

prototype 스코프의 빈은 컨테이너가 생성하고 의존성을 주입한 후에는 더 이상 관리하지 않습니다. 따라서 @DestroyDisposableBean과 같은 소멸 관련 콜백이 호출되지 않습니다. prototype 빈이 데이터베이스 커넥션과 같은 중요한 리소스를 점유하고 있다면, 해당 리소스를 해제하는 로직을 직접 호출해야 합니다.

싱글톤 빈과 상태(State)

싱글톤 빈은 애플리케이션 전체에서 단 하나의 인스턴스만 존재하므로, 여러 스레드에서 동시에 접근할 수 있습니다. 만약 싱글톤 빈이 변경 가능한 상태(e.g., 멤버 변수)를 가지고 있다면, 동시성(concurrency) 문제가 발생할 수 있습니다. 싱글톤 빈은 가급적 상태를 가지지 않도록(stateless) 설계하는 것이 가장 좋습니다. 상태가 꼭 필요하다면 ThreadLocal을 사용하거나, 동기화(synchronization) 처리를 신중하게 구현해야 합니다.