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의 클래스패스 스캐너가 이를 자동으로 탐지하여 빈으로 등록합니다.
@Component 어노테이션은 모든 어노테이션 기반 설정의 기본 진입점 역할을 합니다. @Bean, @Aspect, @Schedule, @Profile과 같은 다른 설정 어노테이션들은 @Component가 함께 표시된 클래스에 있을 때만 처리됩니다. 만약 @Component 없이 이런 어노테이션들을 사용하면, 해당 설정은 무시되고 시작 시 경고 로그가 출력됩니다. 따라서 어노테이션으로 설정하려는 모든 클래스에는 항상 @Component를 먼저 추가하는 것부터 시작해야 합니다.
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 요청) 범위 내에서 단일 인스턴스가 유지됩니다. 현재Activity가RequestAdapter를 지원해야 합니다.session: 사용자 세션 범위 내에서 단일 인스턴스가 유지됩니다. 현재Activity가SessionAdapter를 지원해야 합니다.
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>)
동일한 인터페이스를 구현하는 모든 빈을 List나 Map으로 한 번에 주입받을 수 있습니다. 이는 전략 패턴(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)이 활성화되었을 때만 빈을 등록하도록 할 수 있습니다. 이 어노테이션은 @Component 어노테이션이 함께 붙은 클래스에 위치해야만 효력을 발휘합니다.
// 개발 환경에서만 사용될 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) 관리
전체 생명주기 순서
싱글톤 빈은 다음과 같은 순서로 생성되고 소멸됩니다.
- 인스턴스화: 생성자 호출
- 의존성 주입:
@Autowired가 붙은 필드 및 수정자(setter)에 의존성 주입 - Aware 인터페이스 처리:
Aware인터페이스의set*()메소드 호출 - 초기화 콜백 (Post-Initialization):
@Initialize어노테이션이 붙은 메소드 호출InitializableBean인터페이스의initialize()메소드 호출
- (빈 사용 가능 상태)
- 소멸 전 콜백 (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>를 사용하는 것이 권장되는 스타일입니다.
<append>를 이용한 프로파일별 설정 파일 분리
속성을 그룹화하는 것도 유용하지만, XML에서 환경별 빈을 관리하는 더 강력한 방법은 빈들을 아예 다른 파일로 분리하고 <append> 요소를 사용하여 조건부로 포함하는 것입니다. <append> 요소에 profile 속성을 추가하면, 해당 프로파일이 활성화될 때만 특정 설정 파일을 로드하도록 Aspectran에 지시할 수 있습니다.
이것이 XML에서 환경에 따라 어떤 빈을 등록할지 제어하는 가장 관용적인 방법입니다.
예시:
개발 환경과 운영 환경을 위한 빈 정의가 별도로 있다고 가정해 보겠습니다.
- 프로파일별 XML 파일 생성:
conf/db/dev-beans.xml: 개발 환경을 위한 빈들을 포함합니다.conf/db/prod-beans.xml: 운영 환경을 위한 빈들을 포함합니다.
- 메인 설정 파일에서 조건부로 포함:
<!-- 메인 설정 --> <aspectran> ... <append resource="conf/common-beans.xml"/> <append resource="conf/db/dev-beans.xml" profile="dev"/> <append resource="conf/db/prod-beans.xml" profile="prod"/> ... </aspectran>이 설정에서
dev프로파일이 활성화되면dev-beans.xml이 로드되고,prod프로파일이 활성화되면prod-beans.xml이 대신 로드됩니다. 이를 통해 환경별 빈 정의를 깔끔하고 완벽하게 분리할 수 있습니다.
컴포넌트 스캔 (<bean scan="...">)
XML에서도 <bean scan="...">을 사용하여 컴포넌트 스캔을 활성화할 수 있습니다.
<!-- 'com.example.myapp' 패키지와 그 하위 패키지를 모두 스캔 -->
<bean scan="com.example.myapp.**"/>
내부 빈과 중첩 제한
다른 빈의 속성으로만 사용될 익명의 내부 빈을 정의할 수 있습니다. 유연한 파싱 아키텍처 덕분에 내부 빈의 중첩 깊이에 대한 임의의 제한은 없지만, 가독성을 위해 구조를 단순하게 유지하는 것이 좋습니다.
<bean id="outerBean" class="com.example.OuterBean">
<property name="inner">
<!-- ID가 없는 내부 빈 (1단계) -->
<bean class="com.example.InnerBean">
<!-- ... -->
</bean>
</property>
</bean>
어노테이션과 XML 설정의 조합
어노테이션 기반의 컴포넌트 스캔과 XML 기반의 명시적 빈 정의를 함께 사용할 수 있습니다. 일반적으로 컴포넌트 스캔을 기본으로 사용하고, 특정 빈을 재정의하거나 외부 라이브러리를 등록할 때 XML을 사용합니다. 동일한 ID의 빈이 둘 다에 정의된 경우, 나중에 로드되는 설정이 우선권을 가질 수 있으며, <bean important="true"> 속성으로 덮어쓰기를 강제할 수 있습니다.
7. Best Practices 및 흔한 실수 (Pitfalls)
생성자 주입을 선호하세요
- 불변성(Immutability):
final필드를 사용할 수 있어 빈의 상태가 변경되지 않음을 보장합니다. - 의존성 명시: 객체가 기능하는 데 필요한 모든 의존성이 생성자에 명확하게 드러납니다.
- 순환 참조 방지: 생성자 주입을 사용할 경우, 빈 A와 B가 서로를 필요로 하는 순환 참조가 발생하면 애플리케이션 시작 시점에 오류가 발생하여 문제를 즉시 발견할 수 있습니다.
순환 의존성을 피하세요
순환 의존성은 설계상의 문제를 나타내는 신호일 수 있습니다. 두 클래스가 서로 너무 많은 책임을 지고 있다는 의미일 수 있으므로, 책임을 분리하여 제3의 클래스로 옮기는 리팩토링을 고려하세요. 불가피한 경우, 수정자(setter) 주입을 사용하면 순환 참조 문제를 해결할 수 있습니다.
prototype 빈의 생명주기를 이해하세요
prototype 스코프의 빈은 컨테이너가 생성하고 의존성을 주입한 후에는 더 이상 관리하지 않습니다. 따라서 @Destroy나 DisposableBean과 같은 소멸 관련 콜백이 호출되지 않습니다. prototype 빈이 데이터베이스 커넥션과 같은 중요한 리소스를 점유하고 있다면, 해당 리소스를 해제하는 로직을 직접 호출해야 합니다.
싱글톤 빈과 상태(State)
싱글톤 빈은 애플리케이션 전체에서 단 하나의 인스턴스만 존재하므로, 여러 스레드에서 동시에 접근할 수 있습니다. 만약 싱글톤 빈이 변경 가능한 상태(e.g., 멤버 변수)를 가지고 있다면, 동시성(concurrency) 문제가 발생할 수 있습니다. 싱글톤 빈은 가급적 상태를 가지지 않도록(stateless) 설계하는 것이 가장 좋습니다. 상태가 꼭 필요하다면 ThreadLocal을 사용하거나, 동기화(synchronization) 처리를 신중하게 구현해야 합니다.
항상 @Component를 진입점으로 사용하세요
@Aspect, @Bean, @Schedule, @Profile과 같은 어노테이션들은 @Component가 함께 표시된 클래스에서만 활성화된다는 점을 기억하세요. 프레임워크의 컴포넌트 스캐너는 @Component를 먼저 찾고, 그 클래스들에 있는 다른 어노테이션들을 처리합니다. @Component 없이 다른 설정 어노테이션을 사용하면 해당 설정이 무시되어 구성 오류의 흔한 원인이 될 수 있습니다. 프레임워크는 이런 경우를 감지하면 경고 로그를 출력합니다.