스프링의 아키텍처 철학과 확장 메커니즘

스프링의 핵심 설계 원칙

스프링 프레임워크는 현대 소프트웨어 개발에서 요구되는 모듈성, 유연성, 유지보수성을 기반으로 설계되었다. 이는 단순한 라이브러리가 아니라, 전체 애플리케이션의 제어 흐름을 관리하는 컨테이너 기반 플랫폼이다. 핵심은 의존성 역전(IoC)과 관점 지향 프로그래밍(AOP)을 통해 비즈니스 로직과 공통 관심사(예: 트랜잭션, 로깅)를 명확히 분리한다는 점이다.

1. 의존성 역전 및 주입 (DI)

스프링의 중심 개념인 의존성 역전은 객체의 생성과 의존성 관리를 애플리케이션 코드에서 분리하여, 스프링 컨테이너가 책임지도록 한다. 이를 통해 클래스 간 결합도를 낮추고, 테스트 용이성을 높이며, 구성의 유연성을 제공한다.

  • BeanFactory: 기본적인 빈 관리 인터페이스로, getBean()과 같은 기능을 제공한다.
  • ApplicationContext: BeanFactory를 확장하며, 국제화, 이벤트 발행, 리소스 로딩 등의 기능을 포함한다.

컨테이너 초기화 과정은 AbstractApplicationContext.refresh() 메서드에서 정의되며, 다음과 같은 순서로 동작한다:

  1. 환경 준비 (prepareRefresh())
  2. 빈 정의 로드 및 등록 (obtainFreshBeanFactory())
  3. 빈 팩토리 설정 (prepareBeanFactory())
  4. 후처리기 실행 (postProcessBeanFactory(), invokeBeanFactoryPostProcessors())
  5. 이벤트 다중 방송자 및 메시지 소스 초기화
  6. 특수 빈 생성 (onRefresh())
  7. 이벤트 리스너 등록 (registerListeners())
  8. 비지연 싱글톤 빈 생성 (finishBeanFactoryInitialization())
  9. 최종 준비 완료 (finishRefresh())

이 과정은 템플릿 메서드 패턴을 활용해 일관된 구조를 제공하면서도, 다양한 확장 포인트를 통해 사용자 정의 로직을 삽입할 수 있도록 설계되었다.

2. 빈 사이클릭 의존 해결: 3단계 캐시 전략

스프링은 싱글톤 빈 간의 순환 참조를 효과적으로 처리하기 위해 3단계 캐시를 사용한다:

  • 1차 캐시: 완전히 초기화된 빈 저장 (singletonObjects)
  • 2차 캐시: 초기화 중인 빈의 프록시 참조 저장 (earlySingletonObjects)
  • 3차 캐시: 빈 생성기 저장 (singletonFactories)

핵심 메서드인 DefaultSingletonBeanRegistry.getSingleton()는 각 캐시에서 빈을 탐색하고, 필요 시 프록시를 생성하여 순환 의존 문제를 해결한다.

3. 관점 지향 프로그래밍 (AOP)

AOP는 로깅, 보안, 트랜잭션과 같은 횡단 관심사를 비즈니스 로직에서 분리하여 재사용 가능하게 만든다. 이는 프록시 패턴 기반으로 동작한다.

  • JDK 동적 프록시: 인터페이스 기반. JdkDynamicAopProxyInvocationHandler를 구현하여 메서드 호출을 가로챈다.
  • CGLIB 프록시: 클래스 기반. ObjenesisCglibAopProxy는 목표 클래스의 서브클래스를 생성하여 프록시를 만든다.

프록시 호출 시 ReflectiveMethodInvocation.proceed()책임 연쇄 패턴을 적용해 각 MethodInterceptor를 순차적으로 실행하고, 최종적으로 대상 메서드를 호출한다.

@Transactional 어노테이션은 AOP의 대표적 사례로, TransactionInterceptor가 트랜잭션 시작/커밋/롤백을 자동으로 처리함으로써 개발자가 직접 트랜잭션 코드를 작성하지 않아도 된다.

4. 모듈화와 확장성

스프링은 spring-core, spring-beans, spring-context, spring-aop, spring-tx, spring-web 등으로 분리된 모듈로 구성되어 있으며, 각 모듈은 특정 목적에 집중한다. 이는 단일 책임 원칙을 준수하고, 필요에 따라 선택적으로 사용할 수 있게 한다.

5. 핵심 디자인 패턴 활용

패턴 사용 예시 코드 예시
팩토리 패턴 빈 생성 및 관리 BeanFactory, ApplicationContext
싱글턴 패턴 기본 빈 스코프 모든 빈은 기본적으로 싱글턴으로 관리됨
프록시 패턴 AOP 구현 JdkDynamicAopProxy, CglibAopProxy
템플릿 메서드 컨테이너 초기화 흐름 AbstractApplicationContext.refresh()
옵저버 패턴 이벤트 처리 ApplicationEventPublisher, ApplicationListener
어댑터 패턴 Advice → MethodInterceptor 변환 AdvisorAdapter

6. 실제 개발에의 통찰

  • 경량성과 비침습성: POJO 기반 개발을 지원하며, 프레임워크 특화 클래스나 인터페이스를 상속하지 않아도 된다.
  • 통합 능력: JDBC, JPA, Hibernate, Quartz 등 다양한 기술을 통일된 모델로 통합한다. 예: PlatformTransactionManager
  • 디자인 적용: 스프링의 해법(의존성 분리, 확장 포인트, 인터페이스 중심)을 자신의 프로젝트 설계에 반영하면, 코드 품질이 크게 향상된다.

7. 스프링 확장 포인트 실습

스프링은 프레임워크 내부를 수정하지 않고도 강력한 커스터마이징이 가능하도록 여러 확장 포인트를 제공한다.

1. BeanFactoryPostProcessor

빈 정의가 로드되었지만 인스턴스화되기 전에 호출되며, 빈 정의 정보를 변경할 수 있다.

@Component
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String[] names = beanFactory.getBeanDefinitionNames();
        Arrays.stream(names).forEach(name -> {
            if ("targetBean".equals(name)) {
                BeanDefinition bd = beanFactory.getBeanDefinition(name);
                MutablePropertyValues pvs = bd.getPropertyValues();
                pvs.addPropertyValue("value", "modified");
            }
        });
    }
}

2. BeanDefinitionRegistryPostProcessor

보다 조기의 확장 포인트로, 빈 정의를 동적으로 등록하거나 삭제할 수 있다.

@Component
public class DynamicBeanRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        RootBeanDefinition definition = new RootBeanDefinition(DynamicBean.class);
        registry.registerBeanDefinition("dynamicBean", definition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
}

3. BeanPostProcessor

빈 인스턴스화 후 초기화 전/후에 처리를 삽입할 수 있다. 일반적으로 AOP에서 사용된다.

@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Initializing: " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Initialized: " + beanName);
        return bean;
    }
}

4. 이벤트 시스템 (ApplicationListener)

기본 이벤트 모델은 발행-구독 기반으로 동작한다.

// 이벤트 정의
public class CustomEvent extends ApplicationEvent {
    public CustomEvent(Object source) { super(source); }
}

// 리스너
@Component
public class EventConsumer implements ApplicationListener<CustomEvent> {
    @Override
    public void onApplicationEvent(CustomEvent event) {
        System.out.println("Received: " + event.getSource());
    }
}

// 이벤트 발행
@Component
public class EventPublisher implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void trigger() {
        publisher.publishEvent(new CustomEvent(this));
    }
}

5. FactoryBean

복잡한 객체 생성 로직을 추상화할 때 사용된다.

@Component
public class ComplexObjectFactory implements FactoryBean<ComplexObject> {
    @Override
    public ComplexObject getObject() throws Exception {
        return new ComplexObject(); // 복잡한 초기화 로직
    }

    @Override
    public Class<?> getObjectType() {
        return ComplexObject.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

6. 자동 구성: spring.factories

Spring Boot는 META-INF/spring.factories 파일을 읽어 자동으로 설정 클래스를 등록한다.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration

7. 기타 확장 포인트

  • ImportSelector, ImportBeanDefinitionRegistrar: @Import와 함께 사용하여 설정 클래스 또는 빈 정의를 동적으로 등록.
  • Aware 인터페이스 (예: ApplicationContextAware): 컨테이너나 리소스에 대한 참조를 얻을 수 있음.

스프링의 확장 메커니즘은 개방-폐쇄 원칙의 전형적인 적용 사례다. 프레임워크는 변경에 닫혀 있지만, 확장에는 열려 있어 사용자가 깊이 맞춤화할 수 있다. 이러한 메커니즘을 이해하면, 프레임워크의 작동 방식을 파악하고, 복잡한 문제를 효율적으로 해결할 수 있다.

태그: 스프링 의존성 역전 관점 지향 프로그래밍 빈 생명주기 확장 포인트

6월 22일 21:15에 게시됨