Loading...
Spring Framework Reference Documentation 7.0.2의 Ahead of Time Optimizations의 한국어 번역본입니다.
아래의 경우에 피드백에서 신고해주신다면 반영하겠습니다.
감사합니다 :)
이 장에서는 Spring의 Ahead of Time(AOT) 최적화에 대해 다룹니다.
통합 테스트에 특화된 AOT 지원은 Ahead of Time Support for Tests를 참고하십시오.
Spring의 AOT 최적화 지원은 빌드 시점에 ApplicationContext를 검사하고, 일반적으로 런타임에 발생하는 결정 및 discovery 로직을 적용하기 위한 것입니다.
이렇게 하면 주로 classpath와 Environment에 기반한 고정된 기능 집합에 초점을 맞춘 보다 단순한 application startup 구성을 빌드할 수 있습니다.
이러한 최적화를 일찍 적용하는 것은 다음과 같은 제약을 의미합니다:
@Profile은 빌드 시점에 선택되어야 하며, AOT가 활성화되면 런타임에 자동으로 활성화됩니다.@Conditional)의 존재에 영향을 주는 Environment property는 빌드 시점에만 고려됩니다.ConfigurableListableBeanFactory로부터 일반적으로 registerSingleton 사용)은 역시 ahead of time으로 변환될 수 없습니다.Best Practices 섹션도 참고하십시오.
이러한 제약이 적용되면, 빌드 시점에 ahead-of-time processing을 수행하고 추가 asset을 생성하는 것이 가능해집니다. Spring AOT로 처리된 application은 일반적으로 다음을 생성합니다:
RuntimeHints현재 AOT는 Spring application을 GraalVM을 사용한 native image로 배포할 수 있도록 하는 데 중점을 두고 있습니다.<br>향후에는 더 많은 JVM 기반 use case를 지원할 예정입니다.
ApplicationContext를 처리하기 위한 AOT engine의 entry point는 ApplicationContextAotGenerator입니다.
이는 application을 최적화하기 위해 GenericApplicationContext와 GenerationContext를 기반으로 다음 단계를 처리합니다:
ApplicationContext를 refresh합니다. 기존의 전통적인 refresh와 달리, 이 버전은 bean instance가 아니라 bean definition만 생성합니다.BeanFactoryInitializationAotProcessor 구현을 호출하고, 그들의 contribution을 GenerationContext에 적용합니다. 예를 들어, core 구현은 모든 candidate bean definition을 순회하며 BeanFactory의 상태를 복원하는 데 필요한 코드를 생성합니다.이 프로세스가 완료되면, GenerationContext는 application 실행에 필요한 generated code, resource, class로 업데이트됩니다.
RuntimeHints instance는 관련 GraalVM native image configuration file을 생성하는 데에도 사용할 수 있습니다.
ApplicationContextAotGenerator#processAheadOfTime는 context를 AOT 최적화와 함께 시작할 수 있도록 하는 ApplicationContextInitializer entry point의 class name을 반환합니다.
이러한 단계는 아래 섹션에서 더 자세히 다룹니다.
AOT processing을 위한 refresh는 모든 GenericApplicationContext 구현에서 지원됩니다.
application context는 보통 @Configuration이 붙은 class 형태의 여러 entry point와 함께 생성됩니다.
간단한 예제를 살펴보겠습니다:
1@Configuration(proxyBeanMethods=false) 2@ComponentScan 3@Import({DataSourceConfiguration.class, ContainerConfiguration.class}) 4public class MyApplication { 5}
이 application을 일반적인 runtime으로 시작하면 classpath scanning, configuration class parsing, bean instantiation, lifecycle callback handling 등의 여러 단계를 포함합니다.
AOT processing을 위한 refresh는 기본 refresh에서 발생하는 작업의 일부만 적용합니다.
AOT processing은 다음과 같이 트리거할 수 있습니다:
1RuntimeHints hints = new RuntimeHints(); 2AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); 3context.register(MyApplication.class); 4context.refreshForAotProcessing(hints); 5// ... 6context.close();
이 모드에서 BeanFactoryPostProcessor 구현은 평소와 같이 호출됩니다.
여기에는 configuration class parsing, import selector, classpath scanning 등이 포함됩니다.
이러한 단계는 BeanRegistry가 application에 대한 관련 bean definition을 포함하도록 보장합니다.
bean definition이 condition(@Profile 등)에 의해 보호되는 경우, 이들은 평가되며,
condition과 일치하지 않는 bean definition은 이 단계에서 폐기됩니다.
custom code가 programmatically하게 추가 bean을 등록해야 하는 경우, custom registration code가 bean이 아니라 bean definition만 고려되므로 BeanFactory 대신 BeanDefinitionRegistry를 사용하도록 하십시오.
좋은 패턴은 ImportBeanDefinitionRegistrar를 구현하고, 이를 configuration class 중 하나에 @Import를 통해 등록하는 것입니다.
이 모드는 실제로 bean instance를 생성하지 않기 때문에, BeanPostProcessor 구현은 AOT processing과 관련된 특정 variant를 제외하고는 호출되지 않습니다.
이는 다음과 같습니다:
MergedBeanDefinitionPostProcessor 구현은 bean definition을 post-process하여 init 및 destroy method와 같은 추가 설정을 추출합니다.SmartInstantiationAwareBeanPostProcessor 구현은 필요하다면 더 정확한 bean type을 결정합니다. 이는 runtime에 필요한 proxy를 생성하도록 보장합니다.이 부분이 완료되면, BeanFactory는 application 실행에 필요한 bean definition을 포함하게 됩니다.
이는 bean instantiation을 트리거하지 않지만, AOT engine이 runtime에 생성될 bean을 검사할 수 있도록 합니다.
이 단계에 참여하려는 component는 BeanFactoryInitializationAotProcessor interface를 구현할 수 있습니다.
각 구현은 bean factory의 상태에 기반하여 AOT contribution을 반환할 수 있습니다.
AOT contribution은 특정 동작을 재현하는 generated code를 제공하는 component입니다.
또한 reflection, resource loading, serialization, JDK proxy의 필요성을 나타내기 위해 RuntimeHints를 제공할 수도 있습니다.
BeanFactoryInitializationAotProcessor 구현은 interface의 fully-qualified name을 key로 하여 META-INF/spring/aot.factories에 등록할 수 있습니다.
BeanFactoryInitializationAotProcessor interface는 bean이 직접 구현할 수도 있습니다.
이 모드에서 bean은 regular runtime에서 제공하는 기능과 동등한 AOT contribution을 제공합니다.
결과적으로, 이러한 bean은 AOT-optimized context에서 자동으로 제외됩니다.
bean이
BeanFactoryInitializationAotProcessorinterface를 구현하는 경우, 해당 bean과 그 모든 dependency는 AOT processing 동안 초기화됩니다.<br>일반적으로 이 interface는 이미 bean factory lifecycle 초기에 초기화되고 dependency가 제한적인BeanFactoryPostProcessor와 같은 infrastructure bean에서만 구현할 것을 권장합니다.<br>이러한 bean이@Beanfactory method를 사용해 등록되는 경우, enclosing@Configurationclass를 초기화할 필요가 없도록 method를static으로 지정해야 합니다.
core BeanFactoryInitializationAotProcessor 구현은 각 candidate BeanDefinition에 필요한 contribution을 수집하는 역할을 담당합니다.
이는 전용 BeanRegistrationAotProcessor를 사용하여 수행됩니다.
이 interface는 다음과 같이 사용됩니다:
BeanPostProcessor bean에 의해 구현됩니다. 예를 들어 AutowiredAnnotationBeanPostProcessor는 @Autowired가 붙은 member를 주입하는 코드를 생성하기 위해 이 interface를 구현합니다.META-INF/spring/aot.factories에 등록된 type에 의해 구현됩니다. 보통 bean definition을 core framework의 특정 기능에 맞게 조정해야 할 때 사용됩니다.bean이
BeanRegistrationAotProcessorinterface를 구현하는 경우, 해당 bean과 그 모든 dependency는 AOT processing 동안 초기화됩니다.<br>일반적으로 이 interface는 이미 bean factory lifecycle 초기에 초기화되고 dependency가 제한적인BeanFactoryPostProcessor와 같은 infrastructure bean에서만 구현할 것을 권장합니다.<br>이러한 bean이@Beanfactory method를 사용해 등록되는 경우, enclosing@Configurationclass를 초기화할 필요가 없도록 method를static으로 지정해야 합니다.
특정 등록된 bean을 처리하는 BeanRegistrationAotProcessor가 없는 경우, 기본 구현이 이를 처리합니다.
이는 bean definition에 대한 generated code 조정이 corner case로 제한되어야 하기 때문에 기본 동작입니다.
앞선 예제에서, DataSourceConfiguration이 다음과 같다고 가정해 봅시다:
1@Configuration(proxyBeanMethods = false) 2public class DataSourceConfiguration { 3 4 @Bean 5 public SimpleDataSource dataSource() { 6 return new SimpleDataSource(); 7 } 8 9}
1@Configuration(proxyBeanMethods = false) 2class DataSourceConfiguration { 3 4 @Bean 5 fun dataSource() = SimpleDataSource() 6 7}
잘못된 Java identifier(문자로 시작하지 않거나, 공백을 포함하는 등)를 사용하는 backtick이 있는 Kotlin class name은 지원되지 않습니다.
이 class에 특별한 condition이 없으므로, dataSourceConfiguration과 dataSource는 candidate로 식별됩니다.
AOT engine은 위의 configuration class를 다음과 유사한 code로 변환합니다:
1/** 2 * Bean definitions for {@link DataSourceConfiguration} 3 */ 4@Generated 5public class DataSourceConfiguration__BeanDefinitions { 6 /** 7 * Get the bean definition for 'dataSourceConfiguration' 8 */ 9 public static BeanDefinition getDataSourceConfigurationBeanDefinition() { 10 Class<?> beanType = DataSourceConfiguration.class; 11 RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType); 12 beanDefinition.setInstanceSupplier(DataSourceConfiguration::new); 13 return beanDefinition; 14 } 15 16 /** 17 * Get the bean instance supplier for 'dataSource'. 18 */ 19 private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() { 20 return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource") 21 .withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource()); 22 } 23 24 /** 25 * Get the bean definition for 'dataSource' 26 */ 27 public static BeanDefinition getDataSourceBeanDefinition() { 28 Class<?> beanType = SimpleDataSource.class; 29 RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType); 30 beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier()); 31 return beanDefinition; 32 } 33}
정확히 생성되는 code는 bean definition의 정확한 특성에 따라 달라질 수 있습니다.
각 generated class는<br>예를 들어 static analysis tool에서 제외해야 하는 경우 이를 식별할 수 있도록
org.springframework.aot.generate.Generated로 annotation됩니다.
위에서 생성된 code는 가능하다면 reflection을 전혀 사용하지 않고, @Configuration class와 동등한 bean definition을 보다 직접적인 방식으로 생성합니다.
dataSourceConfiguration에 대한 bean definition과 dataSourceBean에 대한 bean definition이 존재합니다.
datasource instance가 필요할 때, BeanInstanceSupplier가 호출됩니다.
이 supplier는 dataSourceConfiguration bean의 dataSource() method를 호출합니다.
AOT는 Spring application을 native executable로 변환하기 위한 필수 단계이므로,
native image 내에서 실행될 때 자동으로 활성화됩니다.
그러나 spring.aot.enabled System property를 true로 설정하여 JVM에서도 AOT 최적화를 사용할 수 있습니다.
AOT 최적화가 포함되면, 빌드 시점에 이루어진 일부 결정이<br>application setup에 하드코딩됩니다. 예를 들어, 빌드 시점에 활성화된 profile은<br>런타임에서도 자동으로 활성화됩니다.
AOT engine은 application code를 변경하지 않고 가능한 많은 use case를 처리하도록 설계되었습니다. 그러나 일부 최적화는 bean의 static definition에 기반하여 빌드 시점에 수행된다는 점을 명심해야 합니다.
이 섹션에서는 application이 AOT에 대비되었는지 확인하는 best practice를 나열합니다.
AOT engine은 @Configuration model과 configuration 처리의 일부로 호출될 수 있는 모든 callback을 처리합니다.
추가 bean을 programmatically하게 등록해야 하는 경우, BeanDefinitionRegistry를 사용하여 bean definition을 등록하도록 하십시오.
이는 일반적으로 BeanDefinitionRegistryPostProcessor를 통해 수행할 수 있습니다.
다만, 이 자체가 bean으로 등록된 경우, BeanFactoryInitializationAotProcessor도 구현하지 않는 한 런타임에 다시 호출됩니다.
보다 idiomatic한 방법은 ImportBeanDefinitionRegistrar를 구현하고 configuration class 중 하나에 @Import를 사용해 등록하는 것입니다.
이는 configuration class parsing의 일부로 custom code를 호출합니다.
다른 callback을 사용해 programmatically하게 추가 bean을 선언하는 경우, AOT engine에서 처리되지 않을 가능성이 높으며, 그에 따라 해당 bean에 대한 hint가 생성되지 않을 것입니다. 환경에 따라 이러한 bean은 전혀 등록되지 않을 수도 있습니다. 예를 들어, classpath 개념이 없기 때문에 native image에서는 classpath scanning이 동작하지 않습니다. 이와 같은 경우에는 scanning이 빌드 시점에 발생하는 것이 매우 중요합니다.
application이 bean이 구현하는 interface와 상호작용할 수 있더라도, 가장 정확한 type을 선언하는 것이 매우 중요합니다.
AOT engine은 @Autowired member 또는 lifecycle callback method의 존재를 감지하는 등 bean type에 대한 추가 검사를 수행합니다.
@Configuration class의 경우, @Bean factory method의 return type이 가능한 한 정확한지 확인하십시오.
다음 예제를 고려해 보십시오:
1@Configuration(proxyBeanMethods = false) 2public class UserConfiguration { 3 4 @Bean 5 public MyInterface myInterface() { 6 return new MyImplementation(); 7 } 8 9}
1@Configuration(proxyBeanMethods = false) 2class UserConfiguration { 3 4 @Bean 5 fun myInterface(): MyInterface = MyImplementation() 6 7}
위 예제에서 myInterface bean에 대해 선언된 type은 MyInterface입니다.
AOT processing 동안, 일반적인 post-processing 중 어느 것도 MyImplementation을 고려하지 않습니다.
예를 들어, context가 등록해야 할 MyImplementation의 annotated handler method가 있는 경우, 이는 AOT processing 동안 감지되지 않습니다.
따라서 위 예제는 다음과 같이 다시 작성해야 합니다:
1@Configuration(proxyBeanMethods = false) 2public class UserConfiguration { 3 4 @Bean 5 public MyImplementation myInterface() { 6 return new MyImplementation(); 7 } 8 9}
1@Configuration(proxyBeanMethods = false) 2class UserConfiguration { 3 4 @Bean 5 fun myInterface() = MyImplementation() 6 7}
bean definition을 programmatically하게 등록하는 경우, generics를 처리하는 ResolvableType을 지정할 수 있으므로 RootBeanBefinition 사용을 고려하십시오.
container는 여러 후보 constructor를 기반으로 사용할 가장 적절한 constructor를 선택할 수 있습니다.
그러나 이에 의존하는 것은 best practice가 아니며, 필요한 경우 @Autowired로 선호 constructor를 표시하는 것이 좋습니다.
수정할 수 없는 code base에서 작업하는 경우, 관련 bean definition에 preferredConstructors attribute를 설정하여 어떤 constructor를 사용할지 지정할 수 있습니다.
RootBeanDefinition을 programmatically하게 작성할 때, 사용할 수 있는 type에 대한 제약은 없습니다.
예를 들어, bean이 constructor argument로 사용하는 여러 property를 가진 custom record가 있을 수 있습니다.
이는 regular runtime에서는 잘 동작하지만, AOT는 custom data structure code를 생성하는 방법을 알지 못합니다. 좋은 thumb rule은 bean definition이 여러 model 위에 있는 abstraction이라는 점을 염두에 두는 것입니다. 이러한 구조를 사용하는 대신, 단순한 type으로 분해하거나 그러한 방식으로 구축된 bean을 참조하는 것이 좋습니다.
마지막 수단으로, 자체 org.springframework.aot.generate.ValueCodeGenerator$Delegate를 구현할 수 있습니다.
이를 사용하려면, org.springframework.aot.generate.ValueCodeGenerator$Delegate를 key로 사용하여 그 fully-qualified name을 META-INF/spring/aot.factories에 등록하십시오.
Spring AOT는 bean을 생성하는 데 필요한 작업을 감지하고, instance supplier를 사용하는 generated code로 변환합니다. container는 또한 custom argument로 bean을 생성하는 것도 지원하며, 이는 AOT에서 여러 문제를 야기할 수 있습니다:
custom argument로 생성되는 prototype-scoped bean을 사용하는 대신, instance 생성에 책임을 지는 bean이 있는 manual factory pattern을 권장합니다.
특정 use case는 하나 이상의 bean 사이에 circular dependency를 초래할 수 있습니다.
regular runtime에서는 setter method나 field에 대한 @Autowired를 통해 이러한 circular dependency를 wiring할 수 있을 수 있습니다.
그러나 AOT-optimized context는 명시적인 circular dependency가 있는 경우 시작에 실패합니다.
AOT-optimized application에서는 circular dependency를 피하도록 노력해야 합니다.
그럴 수 없는 경우, @Lazy injection point 또는 ObjectProvider를 사용하여 필요한 협력 bean을 lazily하게 access 또는 retrieve할 수 있습니다.
자세한 내용은 이 팁을 참고하십시오.
FactoryBean은 bean type resolution 측면에서 개념적으로 필요하지 않을 수 있는 중간 layer를 도입하므로 주의해서 사용해야 합니다.
thumb rule로, FactoryBean instance가 장기 state를 보유하지 않고 런타임의 이후 시점에서 필요하지 않은 경우, 가능하면 regular @Bean factory method로 대체하고(선언적 configuration 목적을 위한 FactoryBean adapter layer를 위에 둘 수 있음) 사용해야 합니다.
FactoryBean 구현이 object type(즉, T)을 resolve하지 않는 경우, 추가적인 주의가 필요합니다.
다음 예제를 고려해 보십시오:
1public class ClientFactoryBean<T extends AbstractClient> implements FactoryBean<T> { 2 // ... 3}
1class ClientFactoryBean<T : AbstractClient> : FactoryBean<T> { 2 // ... 3}
concrete client 선언은 다음 예제와 같이 client에 대한 resolved generic을 제공해야 합니다:
1@Configuration(proxyBeanMethods = false) 2public class UserConfiguration { 3 4 @Bean 5 public ClientFactoryBean<MyClient> myClient() { 6 return new ClientFactoryBean<>(...); 7 } 8 9}
1@Configuration(proxyBeanMethods = false) 2class UserConfiguration { 3 4 @Bean 5 fun myClient() = ClientFactoryBean<MyClient>(...) 6 7}
FactoryBean bean definition이 programmatically하게 등록되는 경우, 다음 단계를 따르십시오:
RootBeanDefinition을 사용합니다.beanClass를 FactoryBean class로 설정하여 AOT가 이것이 중간 layer임을 알 수 있도록 합니다.ResolvableType을 resolved generic으로 설정하여 가장 정확한 type이 노출되도록 합니다.다음 예제는 기본 definition을 보여줍니다:
1RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class); 2beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class)); 3// ... 4registry.registerBeanDefinition("myClient", beanDefinition);
1val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java) 2beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java)) 3// ... 4registry.registerBeanDefinition("myClient", beanDefinition)
특정 최적화를 적용하려면 JPA persistence unit을 미리 알고 있어야 합니다. 다음 기본 예제를 고려해 보십시오:
1@Bean 2LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) { 3 LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); 4 factoryBean.setDataSource(dataSource); 5 factoryBean.setPackagesToScan("com.example.app"); 6 return factoryBean; 7}
1@Bean 2fun customDBEntityManagerFactory(dataSource: DataSource): LocalContainerEntityManagerFactoryBean { 3 val factoryBean = LocalContainerEntityManagerFactoryBean() 4 factoryBean.dataSource = dataSource 5 factoryBean.setPackagesToScan("com.example.app") 6 return factoryBean 7}
entity scanning이 ahead of time으로 수행되도록 하려면, PersistenceManagedTypes bean을 선언하고 다음 예제와 같이 factory bean definition에서 이를 사용해야 합니다:
1@Bean 2PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) { 3 return new PersistenceManagedTypesScanner(resourceLoader) 4 .scan("com.example.app"); 5} 6 7@Bean 8LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource, PersistenceManagedTypes managedTypes) { 9 LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); 10 factoryBean.setDataSource(dataSource); 11 factoryBean.setManagedTypes(managedTypes); 12 return factoryBean; 13}
1@Bean 2fun persistenceManagedTypes(resourceLoader: ResourceLoader): PersistenceManagedTypes { 3 return PersistenceManagedTypesScanner(resourceLoader) 4 .scan("com.example.app") 5} 6 7@Bean 8fun customDBEntityManagerFactory(dataSource: DataSource, managedTypes: PersistenceManagedTypes): LocalContainerEntityManagerFactoryBean { 9 val factoryBean = LocalContainerEntityManagerFactoryBean() 10 factoryBean.dataSource = dataSource 11 factoryBean.setManagedTypes(managedTypes) 12 return factoryBean 13}
application을 native image로 실행하려면 regular JVM runtime과 비교하여 추가 정보가 필요합니다. 예를 들어, GraalVM은 component가 reflection을 사용하는지 미리 알아야 합니다. 마찬가지로, classpath resource는 명시적으로 지정되지 않는 한 native image에 포함되지 않습니다. 따라서 application이 resource를 로드해야 하는 경우, 해당 GraalVM native image configuration file에서 참조되어야 합니다.
RuntimeHints API는 runtime에서 reflection, resource loading, serialization, JDK proxy의 필요성을 수집합니다.
다음 예제는 native image 내에서 runtime에 classpath에서 config/app.properties를 로드할 수 있도록 합니다:
1runtimeHints.resources().registerPattern("config/app.properties");
1runtimeHints.resources().registerPattern("config/app.properties")
여러 contract는 AOT processing 동안 자동으로 처리됩니다.
예를 들어, @Controller method의 return type을 검사하고, Spring이 해당 type이(serialization 대상, 보통 JSON으로) serialize되어야 한다고 감지하면 관련 reflection hint를 추가합니다.
core container가 추론할 수 없는 경우, 이러한 hint를 programmatically하게 등록할 수 있습니다. 또한 일반적인 use case를 위한 여러 편리한 annotation도 제공됩니다.
@ImportRuntimeHintsRuntimeHintsRegistrar
구현은 AOT engine이 관리하는 RuntimeHints instance에 대한 callback을 받을 수 있게 해줍니다.
이 interface의 구현은 Spring bean 또는 @Bean factory method에 @ImportRuntimeHints를 사용하여 등록할 수 있습니다.
RuntimeHintsRegistrar 구현은 빌드 시점에 감지 및 호출됩니다.
1import java.util.Locale; 2 3import org.springframework.aot.hint.RuntimeHints; 4import org.springframework.aot.hint.RuntimeHintsRegistrar; 5import org.springframework.context.annotation.ImportRuntimeHints; 6import org.springframework.core.io.ClassPathResource; 7import org.springframework.stereotype.Component; 8 9@Component 10@ImportRuntimeHints(SpellCheckService.SpellCheckServiceRuntimeHints.class) 11public class SpellCheckService { 12 13 public void loadDictionary(Locale locale) { 14 ClassPathResource resource = new ClassPathResource("dicts/" + locale.getLanguage() + ".txt"); 15 //... 16 } 17 18 static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar { 19 20 @Override 21 public void registerHints(RuntimeHints hints, ClassLoader classLoader) { 22 hints.resources().registerPattern("dicts/*"); 23 } 24 } 25 26}
가능하다면 @ImportRuntimeHints는 hint가 필요한 component에 최대한 가깝게 사용해야 합니다.
이렇게 하면, component가 BeanFactory에 contribute되지 않는 경우 hint도 contribute되지 않습니다.
interface의 fully-qualified name을 key로 하여 META-INF/spring/aot.factories에 entry를 추가함으로써 구현을 정적으로 등록하는 것도 가능합니다.
@Reflective@Reflective는 annotation된 element에 대한 reflection 필요성을 표시하는 idiomatic한 방법을 제공합니다.
예를 들어, @EventListener는 meta-annotation으로 @Reflective가 붙어 있으며, underlying 구현은 reflection을 사용해 annotation된 method를 호출합니다.
기본적으로 Spring bean만 고려되지만, @ReflectiveScan을 사용해 scanning을 opt-in할 수 있습니다.
아래 예제에서 com.example.app package 및 그 subpackage의 모든 type이 대상으로 고려됩니다:
1import org.springframework.context.annotation.Configuration; 2import org.springframework.context.annotation.ReflectiveScan; 3 4@Configuration 5@ReflectiveScan("com.example.app") 6public class MyConfiguration { 7}
scanning은 AOT processing 동안 발생하며, target package의 type은 고려되기 위해 class-level annotation을 가질 필요가 없습니다.
이는 _deep scan_을 수행하며, type, field, constructor, method, enclosed element에서 @Reflective의 존재(직접 또는 meta-annotation으로)를 확인합니다.
기본적으로 @Reflective는 annotation된 element에 대한 invocation hint를 등록합니다.
이는 @Reflective annotation을 통해 custom ReflectiveProcessor 구현을 지정하여 조정할 수 있습니다.
library author는 자체 목적을 위해 이 annotation을 재사용할 수 있습니다. 이러한 customization의 예는 다음 섹션에서 다룹니다.
@RegisterReflection@RegisterReflection는 임의의 type에 대해 reflection을 declarative하게 등록할 수 있는 @Reflective의 특수화입니다.
@Reflective의 특수화로서,@RegisterReflection은@ReflectiveScan을 사용하는 경우에도 감지됩니다.
다음 예제에서, AccountService에 대해 public constructor와 public method는 reflection을 통해 호출될 수 있습니다:
1@Configuration 2@RegisterReflection(classes = AccountService.class, memberCategories = 3 { MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS }) 4class MyConfiguration { 5}
@RegisterReflection은 class level에서 임의의 target type에 적용할 수 있지만, hint가 실제로 필요한 위치를 더 잘 나타내기 위해 method에 직접 적용할 수도 있습니다.
@RegisterReflection은 보다 구체적인 요구사항을 지원하기 위해 meta-annotation으로 사용할 수 있습니다.
@RegisterReflectionForBinding는 @RegisterReflection으로 meta-annotation된 composed annotation으로, 임의 type에 대한 serialization 필요성을 등록합니다.
일반적인 use case는 container가 추론할 수 없는 DTO 사용으로, 예를 들어 method body 내에서 web client를 사용하는 경우입니다.
다음 예제는 serialization을 위해 Order를 등록합니다.
1@Component 2class OrderService { 3 4 @RegisterReflectionForBinding(Order.class) 5 public void process(Order order) { 6 // ... 7 } 8 9}
이는 Order의 constructor, field, property, record component에 대한 hint를 등록합니다.
property 및 record component에서 transitively하게 사용되는 type에 대해서도 hint가 등록됩니다.
즉, Order가 다른 type을 노출하는 경우, 해당 type에 대해서도 hint가 등록됩니다.
core container는 많은 일반적인 type의 자동 변환에 대한 built-in support를 제공하지만(Spring Type Conversion 참조), 일부 conversion은 reflection에 의존하는 convention-based algorithm을 통해 지원됩니다.
구체적으로, 특정 source → target type pair에 대해 ConversionService에 명시적인 Converter가 등록되어 있지 않은 경우, 내부 ObjectToObjectConverter는 source object를 target type으로 변환하기 위해 source object의 method 또는 target type의 static factory method 또는 constructor에 위임하는 convention을 사용하려고 시도합니다.
이 convention-based algorithm은 runtime에 임의 type에 적용될 수 있으므로, core container는 이러한 reflection을 지원하는 데 필요한 runtime hint를 추론할 수 없습니다.
native image 내에서 runtime hint 부족으로 인해 convention-based conversion 문제가 발생하는 경우, 필요한 hint를 programmatically하게 등록할 수 있습니다.
예를 들어, application이 java.time.Instant에서 java.sql.Timestamp로의 conversion이 필요하고, ObjectToObjectConverter가 reflection을 사용해 java.sql.Timestamp.from(Instant)를 호출하는 것에 의존하는 경우, 다음 예제에서와 같이 이 use case를 native image 내에서 지원하기 위해 custom RuntimeHintsRegitrar를 구현할 수 있습니다.
1public class TimestampConversionRuntimeHints implements RuntimeHintsRegistrar { 2 3 public void registerHints(RuntimeHints hints, ClassLoader classLoader) { 4 ReflectionHints reflectionHints = hints.reflection(); 5 6 reflectionHints.registerTypeIfPresent(classLoader, "java.sql.Timestamp", hint -> hint 7 .withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE) 8 .onReachableType(TypeReference.of("java.sql.Timestamp"))); 9 } 10}
그런 다음 TimestampConversionRuntimeHints는 @ImportRuntimeHints를 통해 선언적으로, 또는 META-INF/spring/aot.factories configuration file을 통해 정적으로 등록할 수 있습니다.
위의
TimestampConversionRuntimeHintsclass는 framework에 포함되어 기본적으로 등록된<br>ObjectToObjectConverterRuntimeHintsclass의 단순화된 버전입니다.<br>따라서 이 특정Instant-to-Timestampuse case는 이미 framework에서 처리되고 있습니다.
Spring Core는 또한 기존 hint가 특정 use case와 일치하는지 확인하기 위한 utility인 RuntimeHintsPredicates를 제공합니다.
이는 자체 테스트에서 RuntimeHintsRegistrar가 예상 결과를 생성하는지 검증하는 데 사용할 수 있습니다.
SpellCheckService에 대한 테스트를 작성하고 runtime에 dictionary를 로드할 수 있는지 확인할 수 있습니다:
1@Test 2void shouldRegisterResourceHints() { 3 RuntimeHints hints = new RuntimeHints(); 4 new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader()); 5 assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt")) 6 .accepts(hints); 7}
RuntimeHintsPredicates를 사용하면 reflection, resource, serialization, proxy generation hint를 확인할 수 있습니다.
이 접근 방식은 unit test에 적합하지만, component의 runtime 동작이 잘 알려져 있다는 것을 전제로 합니다.
application의 global runtime 동작에 대해 더 많이 알아보려면, test suite(또는 app 자체)를 GraalVM tracing agent와 함께 실행하면 됩니다. 이 agent는 runtime에 GraalVM hint가 필요한 모든 관련 호출을 기록하고, 이를 JSON configuration file로 기록합니다.
보다 대상이 명확한 discovery 및 testing을 위해, Spring Framework는 core AOT testing utility가 포함된 전용 module "org.springframework:spring-core-test"를 제공합니다.
이 module에는 RuntimeHints Agent가 포함되어 있으며, runtime hint와 관련된 모든 method 호출을 기록하고, 주어진 RuntimeHints instance가 기록된 모든 호출을 포괄하는지 assert하는 데 도움을 줍니다.
AOT processing 단계 동안 contribute하는 hint를 테스트하려는 infrastructure code를 고려해 봅시다.
1import java.lang.reflect.Method; 2 3import org.apache.commons.logging.Log; 4import org.apache.commons.logging.LogFactory; 5 6import org.springframework.util.ClassUtils; 7 8public class SampleReflection { 9 10 private final Log logger = LogFactory.getLog(SampleReflection.class); 11 12 public void performReflection() { 13 try { 14 Class<?> springVersion = ClassUtils.forName("org.springframework.core.SpringVersion", null); 15 Method getVersion = ClassUtils.getMethod(springVersion, "getVersion"); 16 String version = (String) getVersion.invoke(null); 17 logger.info("Spring version: " + version); 18 } 19 catch (Exception exc) { 20 logger.error("reflection failed", exc); 21 } 22 } 23 24}
그런 다음 우리가 contribute한 hint를 확인하는(unit test, native compilation 불필요) unit test를 작성할 수 있습니다:
1import java.util.List; 2 3import org.junit.jupiter.api.Test; 4 5import org.springframework.aot.hint.ExecutableMode; 6import org.springframework.aot.hint.RuntimeHints; 7import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent; 8import org.springframework.aot.test.agent.RuntimeHintsInvocations; 9import org.springframework.core.SpringVersion; 10 11import static org.assertj.core.api.Assertions.assertThat; 12 13// @EnabledIfRuntimeHintsAgent는 annotation된 test class 또는 test 14// method가 현재 JVM에 RuntimeHintsAgent가 로드된 경우에만 활성화된다는 것을 나타냅니다. 15// 또한 테스트를 "RuntimeHints" JUnit tag로 태그합니다. 16@EnabledIfRuntimeHintsAgent 17class SampleReflectionRuntimeHintsTests { 18 19 @Test 20 void shouldRegisterReflectionHints() { 21 RuntimeHints runtimeHints = new RuntimeHints(); 22 // 다음과 같은 hint를 contribute하는 RuntimeHintsRegistrar를 호출합니다: 23 runtimeHints.reflection().registerType(SpringVersion.class, typeHint -> 24 typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE)); 25 26 // recording lambda 내에서 테스트하려는 관련 code를 호출합니다 27 RuntimeHintsInvocations invocations = org.springframework.aot.test.agent.RuntimeHintsRecorder.record(() -> { 28 SampleReflection sample = new SampleReflection(); 29 sample.performReflection(); 30 }); 31 // 기록된 호출이 contribute된 hint에 의해 포괄되는지 assert합니다 32 assertThat(invocations).match(runtimeHints); 33 } 34 35}
hint contribute를 잊은 경우, 테스트는 실패하며 호출에 대한 세부 정보를 제공합니다:
1org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection 2INFO: Spring version: 6.2.0 3 4Missing <"ReflectionHints"> for invocation <java.lang.Class#forName> 5with arguments ["org.springframework.core.SpringVersion", 6 false, 7 jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7]. 8Stacktrace: 9<"org.springframework.util.ClassUtils#forName, Line 284 10io.spring.runtimehintstesting.SampleReflection#performReflection, Line 19 11io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldRegisterReflectionHints$0, Line 25
build에서 이 Java agent를 구성하는 방법은 다양하므로, build tool 및 test execution plugin의 문서를 참고하십시오.
agent 자체는 특정 package를 instrument하도록 구성할 수 있습니다(기본적으로 org.springframework만 instrument됩니다).
자세한 내용은 Spring Framework buildSrc README 파일에서 확인할 수 있습니다.
Data Buffers and Codecs
Appendix