Spring BeanPostProcessor 심화

BeanPostProcessor란?

BeanPostProcessor는 Spring IoC 컨테이너가 빈을 생성한 직후, 초기화 전후에 개입할 수 있는 확장 포인트입니다. Spring의 핵심 기능인 @Autowired, @Async, @Transactional, AOP 프록시 생성이 모두 BeanPostProcessor로 구현되어 있습니다.

public interface BeanPostProcessor {
    // 빈 초기화(@PostConstruct, afterPropertiesSet) 전에 호출
    default Object postProcessBeforeInitialization(
            Object bean, String beanName) throws BeansException {
        return bean;
    }

    // 빈 초기화 후에 호출 — 프록시 생성의 주요 타이밍
    default Object postProcessAfterInitialization(
            Object bean, String beanName) throws BeansException {
        return bean;
    }
}

반환된 Object가 원본 빈을 대체합니다. 프록시 객체를 반환하면 컨테이너에 프록시가 등록됩니다. @Transactional이 프록시로 동작하는 이유가 바로 이 메커니즘입니다.

빈 생성 흐름에서의 위치

BeanPostProcessor는 빈 라이프사이클의 정확한 위치에서 동작합니다.

1. 빈 인스턴스 생성 (new)
2. 의존성 주입 (@Autowired, 생성자 주입)
3. Aware 인터페이스 콜백 (BeanNameAware, ApplicationContextAware 등)
4. ★ BeanPostProcessor.postProcessBeforeInitialization()
5. @PostConstruct 메서드 실행
6. InitializingBean.afterPropertiesSet()
7. @Bean(initMethod) 실행
8. ★ BeanPostProcessor.postProcessAfterInitialization()
9. 빈 사용 가능 상태
... (애플리케이션 실행)
10. @PreDestroy
11. DisposableBean.destroy()
12. @Bean(destroyMethod)

4단계에서 빈 상태를 검증하거나 설정을 변경하고, 8단계에서 프록시로 래핑하는 것이 일반적인 패턴입니다.

실전 1: 커스텀 어노테이션 자동 처리

@Encrypted 어노테이션이 붙은 필드를 자동으로 복호화하는 BeanPostProcessor를 만들어봅시다.

// 커스텀 어노테이션
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypted {}

// 설정 클래스에서 사용
@Component
@ConfigurationProperties(prefix = "app")
public class AppConfig {
    @Encrypted
    private String dbPassword;

    @Encrypted
    private String apiSecret;

    // getter, setter
}

// BeanPostProcessor 구현
@Component
public class EncryptedFieldProcessor implements BeanPostProcessor {

    private final EncryptionService encryptionService;

    public EncryptedFieldProcessor(EncryptionService encryptionService) {
        this.encryptionService = encryptionService;
    }

    @Override
    public Object postProcessAfterInitialization(
            Object bean, String beanName) {

        for (Field field : bean.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(Encrypted.class)) {
                field.setAccessible(true);
                try {
                    String encrypted = (String) field.get(bean);
                    if (encrypted != null) {
                        String decrypted = encryptionService.decrypt(encrypted);
                        field.set(bean, decrypted);
                    }
                } catch (IllegalAccessException e) {
                    throw new BeanCreationException(
                        beanName, "필드 복호화 실패: " + field.getName(), e);
                }
            }
        }
        return bean;
    }
}

이제 프로퍼티 파일에 암호화된 값을 넣으면, 빈 초기화 시 자동으로 복호화됩니다.

실전 2: 프록시 기반 로깅 자동 적용

특정 어노테이션이 붙은 빈의 모든 메서드에 실행 시간 로깅을 자동으로 추가합니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timed {}

@Component
public class TimedBeanPostProcessor implements BeanPostProcessor {

    private static final Logger log =
        LoggerFactory.getLogger(TimedBeanPostProcessor.class);

    @Override
    public Object postProcessAfterInitialization(
            Object bean, String beanName) {

        Class<?> targetClass = bean.getClass();

        // @Timed가 붙은 클래스만 처리
        if (!targetClass.isAnnotationPresent(Timed.class)) {
            return bean;
        }

        // JDK Dynamic Proxy (인터페이스 기반)
        if (targetClass.getInterfaces().length > 0) {
            return Proxy.newProxyInstance(
                targetClass.getClassLoader(),
                targetClass.getInterfaces(),
                (proxy, method, args) -> {
                    long start = System.nanoTime();
                    try {
                        return method.invoke(bean, args);
                    } finally {
                        long elapsed = (System.nanoTime() - start) / 1_000_000;
                        log.info("[{}] {}.{}() → {}ms",
                            beanName,
                            targetClass.getSimpleName(),
                            method.getName(),
                            elapsed);
                    }
                }
            );
        }

        return bean;
    }
}

// 사용
@Timed
@Service
public class OrderServiceImpl implements OrderService {
    public Order createOrder(OrderRequest req) {
        // 자동으로 실행 시간 로깅
        // [orderService] OrderServiceImpl.createOrder() → 45ms
    }
}

BeanFactoryPostProcessor와의 차이

이름이 비슷하지만 동작 시점과 대상이 전혀 다릅니다.

구분 BeanPostProcessor BeanFactoryPostProcessor
동작 시점 빈 인스턴스 생성 후 정의(BeanDefinition) 로드 후, 인스턴스 생성 전
대상 빈 인스턴스 (Object) 빈 메타데이터 (BeanDefinition)
용도 프록시 생성, 필드 주입, 검증 프로퍼티 치환, 빈 정의 수정
대표 구현 AutowiredAnnotationBPP, AsyncAnnotationBPP PropertySourcesPlaceholderConfigurer
// BeanFactoryPostProcessor: 빈 정의 단계에서 수정
@Component
public class CustomBeanFactoryPostProcessor
        implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(
            ConfigurableListableBeanFactory factory) {

        // 특정 빈의 스코프를 런타임에 변경
        BeanDefinition bd = factory.getBeanDefinition("myService");
        bd.setScope("prototype");

        // 프로퍼티 값 동적 변경
        bd.getPropertyValues().add("timeout", 5000);
    }
}

Spring 내부의 주요 BeanPostProcessor들

  • AutowiredAnnotationBeanPostProcessor: @Autowired, @Value 주입 처리
  • CommonAnnotationBeanPostProcessor: @PostConstruct, @PreDestroy, @Resource 처리
  • AsyncAnnotationBeanPostProcessor: @Async 메서드를 비동기 프록시로 래핑
  • AnnotationAwareAspectJAutoProxyCreator: AOP 프록시 생성 (@Transactional 포함)
  • ScheduledAnnotationBeanPostProcessor: @Scheduled 메서드를 스케줄러에 등록

@Autowired가 동작하는 이유가 “Spring이 알아서” 가 아니라, AutowiredAnnotationBeanPostProcessor가 리플렉션으로 @Autowired 어노테이션을 탐색하고 의존성을 주입하기 때문입니다.

우선순위 제어: Ordered·PriorityOrdered

여러 BeanPostProcessor가 등록되면 실행 순서가 중요합니다.

@Component
public class FirstProcessor implements BeanPostProcessor, Ordered {
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE; // 가장 먼저 실행
    }
}

@Component
public class LastProcessor implements BeanPostProcessor, Ordered {
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;  // 가장 나중에 실행
    }
}

// PriorityOrdered는 Ordered보다 항상 먼저 실행
// Spring 내부 프로세서(Autowired 등)는 PriorityOrdered 구현

실행 순서: PriorityOrdered 구현체 → Ordered 구현체 → 순서 미지정 → 각 그룹 내에서 order 값 오름차순.

관련 글

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