아키텍처

Aspectran 자동 리로딩 메커니즘 심층 분석

1. 개요

Aspectran 프레임워크의 자동 리로딩(Hot-Reloading) 기능은 애플리케이션 실행 중에 설정을 변경했을 때, JVM을 재시작하지 않고도 변경 사항을 동적으로 적용할 수 있게 해주는 강력한 메커니즘입니다. 이 기능의 주된 목적은 다음과 같습니다.

  • 개발 생산성 향상: 개발 중에 코드를 수정하거나 설정을 변경한 후, 서버를 재시작하는 시간 없이 즉시 변경 사항을 확인하여 개발 주기를 단축합니다.
  • 운영 중 동적 업데이트: 운영 중인 서버를 중단하지 않고 설정 변경(예: 기능 플래그 토글, 데이터소스 정보 변경)을 적용할 수 있습니다.

이 메커니즘의 핵심은 파일 변경을 감지하여, Aspectran의 심장부인 ActivityContext를 파괴하고 처음부터 다시 빌드하는 방식으로 동작합니다.

2. 핵심 컴포넌트

자동 리로딩 기능은 다음의 핵심 클래스들이 유기적으로 상호작용하여 구현됩니다.

  • ContextReloadingTimer: ScheduledExecutorService를 내부적으로 사용하여, 설정된 주기마다 리로딩 작업을 실행하도록 스케줄링하는 타이머 클래스입니다.
  • ContextReloadingTask: Runnable 구현체로, 실제 파일 변경을 감시하는 작업을 수행합니다. 각 설정 파일의 lastModified 타임스탬프를 기록해두고, 이전 값과 달라졌는지 주기적으로 검사합니다.
  • ActivityContextBuilder: 컨텍스트 빌드 과정의 마지막 단계에서 autoReload 설정이 활성화되어 있으면, ContextReloadingTimer를 생성하고 시작시키는 주체입니다.
  • ServiceLifeCycle: 변경이 감지되었을 때, ContextReloadingTask가 애플리케이션의 재시작을 트리거하기 위해 호출하는 인터페이스입니다.

3. 동작 흐름

자동 리로딩은 다음과 같은 명확한 단계를 거쳐 수행됩니다.

  1. 설정 및 활성화: 개발자는 aspectran-config.apon 파일의 context 섹션에 autoReload 설정을 추가하여 기능을 활성화합니다.
    context: {
        autoReload: {
            enabled: true
            scanIntervalSeconds: 5
        }
    }
    
  2. 초기화 및 감시 시작: ActivityContextBuilder가 컨텍스트를 빌드하는 마지막 단계에서, autoReload 설정이 true이면 ContextReloadingTimer를 시작합니다. 이때, 빌드 과정에서 참조된 모든 설정 파일과 <resourceLocations>에 지정된 리소스들이 감시 대상으로 등록됩니다.
  3. 변경 감시: ContextReloadingTaskscanIntervalSeconds에 설정된 주기(예: 5초)마다 등록된 파일들의 lastModified 타임스탬프를 검사합니다.
  4. 변경 감지 및 재시작 트리거: 리로딩 로직은 여러 파일이 복사되는 등 불완전한 파일 업데이트 상황에 대해 안전하도록 설계되었습니다. 재시작은 변경이 감지되지 않는 ‘고요한 기간’이 지난 후에만 트리거됩니다.
    • 파일 변경이 처음 감지되면, 태스크는 변경 사실만 기록하고 다음 스캔을 기다립니다.
    • 만약 다음 스캔에서 더 이상의 변경이 감지되지 않으면, scanIntervalSeconds 만큼의 고요한 기간이 지난 것으로 간주합니다. 그 후 태스크는 ServiceLifeCycle 인스턴스의 restart() 메서드를 호출합니다.
    • 만약 스캔할 때마다 계속해서 변경이 감지된다면, 새로운 변경이 없는 스캔이 완료될 때까지 재시작은 보류됩니다.
  5. 재시작 실행: restart() 호출을 받은 서비스는 기존 ActivityContext를 파괴하고, ActivityContextBuilder를 통해 컨텍스트 빌드 과정을 처음부터 다시 수행합니다. 이 과정에서 변경된 설정이 적용된 새로운 ActivityContext가 생성되어 애플리케이션에 반영됩니다.

4. 계층적 서비스와 리로딩

Aspectran은 여러 서비스가 부모-자식 관계를 맺는 계층 구조를 지원합니다. 자동 리로딩 메커니즘은 이러한 구조에서 안정성과 일관성을 유지하기 위해 다음과 같이 동작합니다.

  • 분산된 감시, 중앙화된 실행: 각 하위 서비스는 자신만의 autoReload 설정을 가질 수 있으며, 이에 따라 독립적인 ContextReloadingTimer가 생성되어 자신에게 속한 리소스만 감시할 수 있습니다. 하지만 어떤 하위 서비스의 타이머가 변경을 감지하더라도, 실제 restart() 명령은 항상 루트 서비스(RootService)로 전달됩니다.
  • 안전성 확보: 이 ‘중앙화된 실행’ 방식은 AbstractActivityContextBuilder 내부 로직을 통해 보장됩니다.
    // The restart command must be delivered to the root service to safely reload the entire application.
    contextReloadingTimer = new ContextReloadingTimer(masterService.getRootService().getServiceLifeCycle());
    

    모든 재시작 요청이 루트 서비스로 집중됨으로써, AbstractServiceLifeCycle에 정의된 Assert.state(isRootService(), ...) 제약 조건을 자연스럽게 만족시킵니다. 이는 부분 재시작으로 인해 발생할 수 있는 복잡한 상태 불일치 문제를 원천적으로 방지하고, 전체 애플리케이션의 일관성을 보장하는 안전한 설계입니다.

5. SiblingClassLoader와 리로딩

자동 리로딩은 Aspectran의 독자적인 SiblingClassLoader와 밀접하게 연관되어 클래스 레벨의 변경을 반영합니다.

  • hardReload 모드: autoReload 설정 시 reloadMode="hard" 옵션을 주면, 재시작 과정에서 기존 SiblingClassLoader 그룹 전체를 폐기하고 새로운 인스턴스를 생성합니다. 이를 통해 <resourceLocations>에 지정된 JAR 파일이 변경되었을 때, 새로운 버전의 클래스를 로드할 수 있습니다.
  • 근본적 한계: 이 메커니즘은 SiblingClassLoader에 의해 로드된 클래스에만 적용됩니다. 애플리케이션의 메인 클래스로더(부모 클래스로더)가 로드한 프레임워크 핵심 클래스나 애플리케이션 부트스트랩 관련 클래스들은 JVM 재시작 없이는 리로드할 수 없습니다.

6. 제약 사항 및 실용적 활용

자동 리로딩 기능은 매우 강력하지만, 다음과 같은 특성과 제약 사항을 이해하고 사용해야 합니다.

  • 메모리 상태 소실: 리로딩은 ActivityContext 전체를 파괴하고 새로 만드는 과정이므로, 싱글톤 빈의 인스턴스 변수 등 메모리에 저장된 모든 상태는 초기화됩니다.
  • 짧은 다운타임: 컨텍스트가 재시작되는 짧은 시간 동안 서비스는 요청을 처리할 수 없는 상태가 될 수 있습니다.
  • IDE 환경 제약: IDE에서 실행될 경우 클래스 로딩 충돌을 피하기 위해 <resourceLocations> 설정이 무시될 수 있습니다. 따라서 JAR 파일 변경 감지는 주로 실제 배포 환경에서 의미가 있습니다.
  • 권장 배포 전략: 루트 애플리케이션과 하위 서비스의 빌드/배포 단위를 분리하고, 변경된 하위 서비스의 JAR 파일만 <resourceLocations>로 지정된 디렉터리에 복사하여 업데이트하는 방식이 효과적입니다.

7. 고급 주제: 부분 재시작, 다른 프레임워크와의 비교

가. 중간 서비스 재시작의 복잡성

Aspectran은 계층적 서비스 구조를 지원하지만, 자동 리로딩은 루트 서비스의 전체 재시작으로 중앙화되어 있습니다. 이는 의도된 설계로, 중간 하위 서비스만 독립적으로 재시작하는 것은 다음과 같은 잠재적 위험을 내포하기 때문입니다.

  • 상태 불일치: 재시작되는 하위 서비스와 계속 실행 중인 상위/형제 서비스 간에 공유되는 리소스나 상태의 일관성이 깨질 수 있습니다.
  • 의존성 파괴: 상위 컨텍스트의 다른 컴포넌트가 재시작 중인 하위 서비스의 빈을 참조하고 있었다면, 일시적으로 의존성을 해결할 수 없는 상태에 빠질 수 있습니다.
  • 진행 중인 작업 처리의 어려움: 재시작 시점에 해당 서비스가 처리하던 트랜잭션이나 비동기 작업을 안전하게 완료하거나 롤백하는 것은 매우 복잡한 문제입니다.

따라서 Aspectran은 부분 재시작의 복잡성 대신, 전체 컨텍스트를 재구성하여 예측 가능하고 안정적인 상태를 보장하는 방식을 채택했습니다.

나. 다른 프레임워크와의 비교

동적 리로딩은 모든 프레임워크의 숙제이며, 다양한 방식으로 접근합니다.

  • OSGi: 모듈(번들)의 독립적인 생명주기를 관리하는 기술 명세로, 진정한 의미의 ‘부분 재시작’이 가능하지만, 매우 복잡하여 대중적으로 사용되지는 않습니다.
  • Spring Boot DevTools: Aspectran과 가장 유사한 방식으로, 변경 감지 시 애플리케이션 컨텍스트 전체를 매우 빠르게 재시작합니다. 이는 부분 재시작이 아닌 ‘빠른 전체 재시작’으로, 대부분의 최신 프레임워크가 채택한 실용적인 방식입니다.
  • 마이크로서비스 아키텍처 (MSA): 문제를 프레임워크가 아닌 아키텍처 레벨에서 해결합니다. 각 서비스를 독립된 프로세스로 분리하여, 다른 서비스에 영향 없이 개별적으로 재시작할 수 있습니다.
  • JRebel: 단순한 클래스 핫스왑을 넘어, 프레임워크를 인지하는 플러그인을 통해 변경된 클래스와 연관된 빈만 컨테이너(예: Spring ApplicationContext) 내에서 다시 생성하고 의존성을 재주입합니다. 이는 매우 강력하지만, 프레임워크와의 깊은 통합이 필요한 고기능성 상용 도구입니다.

다. 성능과 유연성의 트레이드오프

만약 프레임워크를 처음부터 JRebel처럼 완벽한 동적 교체가 가능하도록 설계했다면, 아마도 런타임 성능에서 손해를 보았을 것입니다. 동적 교체를 위해서는 모든 컴포넌트 호출에 프록시와 같은 간접 계층이 필요하고, JIT 컴파일러의 최적화를 방해하며, 리플렉션 사용이 증가하기 때문입니다.

Aspectran을 포함한 대부분의 주류 프레임워크는 ‘한 번 빌드되면 거의 바뀌지 않는다’는 전제 하에, 구동 시점에 무거운 최적화 작업을 모두 끝내고 런타임에는 가장 빠른 경로로 동작하도록 설계되어 있습니다. 이는 ‘동적인 유연성’보다는 ‘안정적인 런타임 성능’에 우선순위를 둔 합리적인 설계적 선택입니다.

8. 결론

Aspectran의 자동 리로딩 메커니즘은 개발 생산성을 높이고 동적 업데이트를 가능하게 하는 강력한 기능입니다. 특히, 부분 재시작의 복잡성과 위험성을 피하고 ‘루트 서비스 전체 재시작’이라는 중앙화된 실행 모델을 채택하여 안정성과 예측 가능성을 확보했습니다. 개발자는 이 메커니즘의 장점과 함께 상태 소실, 짧은 다운타임과 같은 특성을 이해하고 자신의 시나리오에 맞게 활용해야 합니다.