회사에서 개발하는 레거시 프로젝트의 Version Migration 작업을 준비하기 위해 Spring (boot) Version별 변경 내역을 조사하고 있는 내용입니다. 누락되거나 잘못 기술된 내용이 있을 수 있는 점 양해 부탁드립니다.

 

 

Spring Boot 2.1.x

  • Java 11 지원 (Spring Framework 5.1)
  • Spring Boot 2.1 Release Notes · spring-projects/spring-boot Wiki
  • 포함 의존성 변경, 삭제 등 (Junit 4.12 → Junit 5.2, Tomcat 8.5.39 → 9.x, Hibernate 5.2 → 5.3)
  • Spring Framework 5.1
    • Java 8 ~ 11
    • CGLIB 3.2 fork(Java 9+ API로 위임)
    • @Profile에 and, or expression 지원
    • Hibername와 read-only transaction 사용 시 Memory에 Entity Snapshot이 남지 않습니다.
    • 로거 설정 개선, 컴포넌트 스캐닝 성능 개선 용도의 컴포넌트 인덱스 기능 제공, 함수형 프로그래밍 스타일 지원, 코틀린 지원, 리액티브 프로그래밍 모델 지원, DataSize Type 등
  • 포함 프로퍼티 변경 (servlet path, HibernateProperties 별도 분리)
  • Spring JCL - JCL Interface가 잘 사용되지 않음으로써 (Runtime Logger Lookup이 복잡도 등의 문제) JCL을 사용하던 기존 코드를 쉽게 SLF4J로 전환하기 위해 추가한 JCL 용 브리지 모듈이다. (JCL-over-SLF4J 대체)
  • Auto-Configuration Exclusion @EnableAutoConfiguration, @SpringBootApplication, @ImportAutoConfiguration spring.autoconfigure.exclude에서 적용된 제외 설정이 이제 Local 환경에서만 적용되는 것이 아니라 모든 환경에서 일관되게 적용됩니다.
  • Log Group application.property, yml에서 logging.group.{group-name}={Loggers...} 동일한 Logging 구성 정보를 적용할 Logger 들의 그룹을 정의하는 속성이 지원됩니다. loging.level.{group-name}=DEBUG
    • 기본 제공 그룹으로는 org.springframework.web, org.jooq.tools.LoggerListener 등이 있습니다.
  • @EnableAsync, @EnableScheduling, spring-boot-starter-data-jdbc 등 추가 지원
  • Bean Override 비 허용 - 동일한 이름의 Bean을 중복으로 정의하고 ApplicationContext에 등록할 때 기존에 등록된 Bean을 덮어 쓸지 말지 결정하는 옵션입니다. spring.main.allow-bean-definition-overriding = false (default, BeanDefinitionOverrideException 발생)
    • Spring Boot의 Auto Configuration과 사용자의 Configuration, Component 등이 중복됨으로써 발생하는 상황으로 @ConditionalOnMissingBean와 같은 필터링 용 제약을 거는 것을 권장합니다.
  • Spring Data JPA 저장소 초기화에 대해서 Lazy mode 지원 (bootstrap mode)
  • Spring Actuator /info, /health end-point 사용자의 Spring Security 설정으로 인가 처리가 적용되지 않는 한 /info, /health end-point는 인증, 인가 없이 접근 가능한 공개 API가 됩니다.
  • @WebMvcTest and @WebFluxTest Security Configuration @WebMvcTest, @WebFluxTest Slice Test에서 사용자가 정의한 보안 설정인 WebSecurityConfigurer, ServerHttpSecurity bean을 자동으로 구성 정보에 포함합니다.
  • Context ApplicationConversionService Support ApplicationConversionService가 기본적으로 등록되며, @Value Annotation에 사용할 수 있습니다.
  • h2 Compatibility mode 사용 방법 변경 - 이전 버전까지는 spring.jpa.properties.hibernate.dialect 설정에 따라 Compatibility mode가 설정되었으나 현재는 spring.datasource.url=jdbc:h2...;MODE=MYSQL 직접 설정해주어야 mode type이 적용되도록 변경되었습니다.
  • Property Migrator 달라진 property 정보를 반영하지 않아도 정상적으로 Application을 수행할 수 있도록 도와주는 Module입니다. (Runtime에서 동작)

 

 

 

Spring Boot 2.2.x

  • Java 13 지원 (Spring Framework 5.2)
  • Spring Boot 2.2 Release Notes · spring-projects/spring-boot Wiki
  • 포함 의존성 변경, 삭제 등
    • Jakarta EE dependencies 추가 및 기존 JavaEE dependencies 이전
    • ReactiveWebServerApplicationContext 사용 중단
  • Spring Framework 5.2
    • Spring WebFlux Support for Kotlin Coroutines - Spring WebFlux에서 이제 Kotlin의 Coroutines을 지원합니다.
    • RSocket, R2DBC 지원
    • Spring MVC Router Support for Kotlin DSL Web Flux에서의 DSL을 통한 Router 등록 방식과 유사한 모델을 MVC에서도 지원합니다.
    • @Configuration에 대한 Bean Lite Mode 지원 (proxyBeanMethods = false) - Configuration 생성 시 Proxy를 통해 Configuration Instance를 Wrapping 하는 작업을 수행하지 않도록 설정하는 속성으로 기본 제공되는 AutoConfiguration에 적용하기 위해 추가되었습니다.
      • 성능 향상 (구동 시간 감소)가 있으나, Configuration Method를 여러 번 호출할 경우 매번 Instance를 생성해 반환하기 때문에 조심하여야 합니다.
  • Spring Security 5.2
  • Spring Data Moore
    • 선언적 리액티브 트랜잭션 지원
      • PlatformTransactionManager에서 ReactiveTransactionManager로 전환되었습니다.
      • TransactionalOperator@Transactional 구성이 통합되었습니다.
    • 리액티브 QueryDSL 지원
    • 기타 성능 향상
    • What's new in Spring Data Moore?
  • 프로퍼티 변경
  • Spring Actuator HTTP Trace disable by default - 많은 리소스를 소모하고, in-memory에 저장하는(기본 구현) 방식이 Clustering 구조에 맞지 않아 기본적으로 비활성화되었습니다.
  • Prometheus Push Gateway를 위한 https end-point 추가
  • Lazy initialization by Global spring.main.lazy-initialization property를 통해 Application 전역에 Lazy initialization를 지원할 수 있으며, @Lazy(false)LazyInitializationExcludeFilter 를 통해 지연 초기화 대상에서 제외할 수 있습니다.
  • JMX disabled by default spring.jmx.enabled=false
  • spring-boot-starter-test default by JUnit 5 기본 사용하는 Junit Version이 4에서 5로 변경되었습니다.
  • Hibernate Dialect - Hibernate가 감지한 Database dialect를 사용하던 설정에서 사용자 정의를 통해 선택 가능하도록 변경되었습니다.
  • jOOQ RecordListenerProvider 다수의 RecordListenerProvider Bean을 등록하던 구조에서 RecordListenerProvider[] 방식으로 반환하는 Bean을 정의하는 방식으로 변경되었습니다.
  • Shutdown configuration of task execution and scheduling 자동 구성된 TaskExecutor, TaskScheduler의 종료 시 필요한 설정을 적용할 수 있도록 spring.task.execution.shutdown and spring.task.scheduling.shutdown이 추가되었습니다. (gracefully-shotdown)
  • Kubernetes detection @ConditionalOnCloudPlatform을 통해 현재 Application이 Kubernetes 위에서 동작하고 있는 상태인지 감지할 수 있습니다.
  • Test Application Arguments in integration tests 통합 테스트에서 사용할 Application Argument를 정의할 수 있는 ApplicationArguments Bean이 추가되었습니다.
  • @ConfigurationProperties scanning @ConfigurationProperties이 적용된 Class를 Scan 하여 Bean으로 등록할 수 있는 @ConfigurationPropertiesScan, @EnableConfigurationProperties이 추가되었습니다.
  • Immutable @ConfigurationProperties binding @ConfigurationProperties이 붙은 Class를 불변 객체로 선언할 수 있는 @ConstructorBinding을 지원합니다. (이는 Records에도 적용됩니다.)
    • 생성자를 통한 객체 생성을 지원하며 여러 생성자가 존재할 경우 특정 생성자 위에 정의하면 됩니다.
  • Callback for Redis cache configuration ****RedisCacheManager의 불변 설정을 위한 RedisCacheManagerBuilderCustomizer가 추가되었습니다.
  • Qualifier for Spring Batch datasource Spring Batch에서 사용될 DataSource Bean을 설정하기 위한 @BatchDataSource 가 추가되었습니다. ****
  • Idle JDBC connections metrics 현재 대기 상태에 있는 DataSource Connection Pool의 크기를 확인할 수 있는 Metric 정보가 추가되었습니다.

 

 

 

Spring Boot 2.3.x

  • 자바 14 지원
  • Gradle 5.6.x (Legacy), 6.3+
  • Spring Boot 2.3 Release Notes · spring-projects/spring-boot Wiki
  • 포함 의존성 변경, 삭제 등
    • Spring Cloud Connectors starter has been removed
    • Junit Jupiter 5.6
    • Mockito 3.3
    • Jackson 2.11
      • Date, Calendar의 Timezone 표현 방식이 표준에 맞게 변경되었습니다. +00:00
      • 필드 명 없이 JSON Array를 만들 수 없도록 변경되었습니다.
      • JSON 생성 시 Serialization 순서를 결정할 수 있습니다. @JsonProperty(index = )
      • 표준 통화에 대한 객체를 제공하는 Joda Money를 지원하기 위해 새로운 모듈이 추가되었습니다. jackson-datatype-joda-money
  • Spring Integration 5.3
    • RSocket support for Spring Integration spring-integration-rsocket 의존성 추가 및 해당 의존성 사용 시 IntegrationRSocketEndpoint, RSocketOutboundGateway을 통한 end-point 설정이 가능합니다.
  • Spring Security 5.3
    • Oauth 2.0 Client, Resource Server 개선
  • Spring Kafka 2.5, Spring HATEOAS 1.1
  • Spring Data Neumann
    • Cassandra v4 이전 버전과 호환되지 않는 요소들이 다수 추가되었습니다.
    • Couchbase SDK v3 이전 버전과 호환되지 않는 요소들이 다수 추가되었습니다.
    • Elasticsearch 7.5
    • MongoDB 4 - MongoClientSettingsBuilderCustomizer bean이 필수 드라이버(Mongo Operations) 설정에 적용됩니다. spring-boot-starter-data-mongodb-reactive 의존성의 경우 Mongo Operations이 구성되지 않기 때문에 spring-boot-starter-data-mongodb 의존성을 추가하는 것을 권장합니다.
    • Neo4j Neo4j의 Open Session이 기본적으로 Default 됩니다. spring.data.neo4j.open-in-view property를 통해 설정할 수 있습니다.
    • Data JDBC 2.0
      • 낙관적 락 지원, PagingAndSortingRepository 지원, H2 지원 (모든 기능), 식별자 따옴표, Query Derivation 지원 (query method)
    • R2DBC support
      • auto-configuration, health indicator for Connection Factory, @DataR2dbcTest
  • Validation Starter no longer included in web starters - validation 관련 의존성(javax.validation.*)은 spring-boot-strater-web에서 spring-boot-starter-validation라는 별도의 의존성으로 분리되었습니다.
  • Build OCI images with Cloud Native Buildpacks Cloud Native Buildpack을 사용한 Docker Image 구축 방식 지원 및 spring-boot:build-image, bootBuildImage 가 추가되었습니다. (Maven, Gradle)
  • Build layered jars for inclusion in a Docker image Jar에서 자주 변경되는 부분을 식별하여 Layer를 분할해 재 사용할 수 있도록 개선되었습니다. (효율적인 Docker Image build를 위함)
    • JDK → Library → Application Code 순으로 Layer를 생성합니다.
  • Support of wildcard locations for configuration files Configuration File load시 *를 지원합니다.
  • Graceful shutdown embedded Jetty, Reactor Netty, Tomcat, Undertow 및 Servlet, Reactive 기반 Application에서 Gracefully shutdown이 지원됩니다. server.shutdown=graceful, spring.lifecycle.timeout-per-shutdown-phase=10s
  • WebServerInitializedEvent and ContextRefreshedEvent - Graceful shutdown 기능 지원의 일환으로 Web Server의 초기화 시점이 변경되었습니다. (새로 고침 이후 → Application Context 새로 고침 이후)
  • Spring Actuator Liveness and Readiness probes Application의 활성, 처리 가능 상태를 추적할 수 있는 end-point가 추가되었습니다. (kubernetes 위에서 실행될 경우 자동 설정) /actuator/health/readiness, management.health.probes.enabled=true
  • Date-Time conversion in web applications Application.properties, yml을 통해 시간 및 날짜 변환 시 형식을 지정할 수 있습니다. ex) spring.mvc.format.date=iso , spring.mvc.format.date-time, spring.mvc.format.time, spring.webflux.format.date, spring.webflux.format.date-time, spring.webflux.format.time
  • Unique DataSource Name By Default 자동 구성된 DataSource가 고유한 이름으로 생성되며, H2 DB 사용 시 Database URL이 testdb를 참조하지 않아 기존(H2를 자동 구성에 의존하여 사용 중이던) Project에서 해당 Version으로 Migration 수행 시 영향을 받습니다.
  • Embedded Servlet web server threading configuration Embedded Web Server의 Thread를 구성하는 속성이 server.{tomcat, jetty...}.threads 로 이동되었습니다.
  • Changes to the Default Error Page’s Content Error Message 및 Binding Error는 기본 Error Page에 포함되지 않습니다. server.error.include-message, server.error.include-binding-errors 설정을 통해 포함하거나 상황에 따라 제어할 수 있습니다. (always, on-param, never)
  • ApplicationContextRunner disables bean overriding by default SpringApplication을 통한 실행 방식과 일관성을 제공하기 위해 ApplicationContextRunner에서도 Bean Override를 비 허용합니다. withAllowBeanDefinitionOverriding을 통해 허용할 수 있습니다.
  • Activating multiple profiles with @ActiveProfiles @ActiveProfiles에서 쉼표를 통한 다중 Profile을 지원합니다. @ActiveProfiles({"p1","p2"})

 

 

 

Spring Boot 2.4.x

  • 자바 15 지원
  • Spring Boot 2.4 Release Notes · spring-projects/spring-boot Wiki
  • 포함 의존성 변경, 삭제 등
    • Flyway 7 Overall ordering of Java and SQL Callbacks · Issue #2785 · flyway/flyway
    • Logback Configuration Properties Logback properties들이 logging.pattern.rolling-file-namelogging.logback.rollingpolicy.file-name-pattern과 같은 형대로 변경되었습니다.
      • logging.logback.{기존 속성 path}
    • Hazelcast 4 내부 의존성이 Hazelcast 3.2.x → 4.x로 변경되었습니다. 하위 버전이 필요한 경우 hazelcast.version property를 통해 사용할 버전을 설정할 수 있습니다.
    • Neo4 j Spring Data Neo4j
  • 버전 관리 체계 변경 Updates to Spring Versions
  • Spring Framework 5.3
    • Spring Framework 5.3.x가 LTS로 계속 유지되며, 5.4.x를 만들지 않습니다.
    • GraalVM 관련 개선
    • Spring-R2DBC Module 제공
  • Spring Data 2020.0 버전 명명을 Calander 기반으로 변경하였으며, 코드명은 수학자의 이름을 사용하는 것을 유지합니다.
    • Neo4j
    • R2DBC
    • Supported RxJava 3
  • Spring Batch 4.3
  • Spring Security 5.4
  • Spring Integration 5.4
  • Spring Kafka 2.6, Spring HATEOAS 1.2, Spring Session 2020.0, Spring Security 5.4…
  • Config File Processing (application properties and YAML files) application.properties 및 application.yml 파일의 처리 방법이 변경되었습니다. (properties 파일 안에서 여러 profile 정의 가능) 또한 spring.profiles 대신 spring.config.activate.on-profile을 사용하도록 변경되었습니다.
    • 옛날 파일 처리 방식을 사용하려면 spring.config.use-legacy-processing=true를 사용합니다.
  • Supported Configuration Tree spring.config.location, spring.config.import 속성 설정 시 configtree, optional 등의 prefix를 지원합니다.
  • Custom property name support configtree, optional 이외에도 Custem prefix를 정의하여 기능을 확장할 수 있는 방법을 제공합니다. ConfigDataLoader, ConfigDataLocationResolver
  • Layered jar enabled by default Spring Boot 2.3.x 에서 추가되었던 Build layered jars 기능이 기본적으로 활성화되었습니다.
  • Importing Additional Application Config application.properties 또는. yml 파일에서 spring.config.import속성을 통해 Spring 환경을 구성하는 상황에서 같이 참조할 하나 이상의 구성 파일을 지정할 수 있습니다. (spring.config.use-legacy-processing=true를 사용하지 않는 경우)
  • Volume Mounted Config Directory Trees spring.config.import속성을 통해 ConfigMaps와 같은 Key:Value 형식의 Directory Tree를 가져올 수 있습니다.
  • Importing Config Files That Have no File Extension 파일 확장자를 작성할 수 없는 Cloud Platform에서 [.extension] 형식의 Hint를 지정하여 파일의 확장자를 특정할 수 있습니다.
  • Origin Chains
  • Startup Endpoint Actuator에서 Application의 시작 정보를 제공하는 startup end-point를 지원합니다. 제공되는 timeline object의 events array를 통해 생성되는 Bean의 이름이나 시작 및 초기화 지연 시간 등을 식별할 수 있습니다.
  • Docker/Buildpack Support Maven의 spring-boot:build-image, gradle의 bootBuildImage task를 통해 생성된 Docker Image를 Docker Registry에 등록하는 기능이 추가되었습니다.
  • Maven Buildpack Support Maven에서 spring-boot:build-image을 통해 Docker Image를 생성할 때 모든 Project Module Dependency를 Application Layer에 포함합니다. (여러 Project Module이 존재하는 경우 하나의 Layer에 집약됩니다.)
  • Gradle Buildpack Support Gradle에서 bootBuildImage task를 통해 Docker Image를 생성할 때 모든 Project Module Dependency를 Application Layer에 포함합니다. (여러 Project Module이 존재하는 경우 하나의 Layer에 집약됩니다.)
  • Redis Cache Metric Redis Cache를 사용하는 경우 Micrometer가 응답하는 Metric 정보에서 Cache hit, miss, Cache put, Cache evict 등의 모든 통계 정보를 확인할 수 있습니다. spring.cache.redis.enable-statistics=true
  • Web Configuration Properties Spring MVC, Spring WebFlux에서 지정하는 locale 및 Resource path 등의 Properties 속성들이 해당 Module 여부와 상관없이 일관적으로 구성될 수 있도록 변경되었습니다. spring.mvc.locale, spring.mvc.locale-resolver, spring.resources.*spring.web.locale, spring.web.locale-resolver, spring.web.resources.*
  • Register @WebListeners in a way that allows them to register servlets and filters Servlet Container가 자체적으로 @WebListener 이 적용된 클래스를 인스턴스화 합니다. 이를 통해 @WebListener가 붙은 객체에게 제공하던 Spring Container를 통한 의존성 주입이 지원되지 않습니다.
  • Slice Test for Cassandra @DataCassandraTest를 통한 Cassandra Slice Test를 사용할 수 있습니다.
  • Configuration property for H2 Console’s web admin password H2 Console Admin의 Password를 지정 가능한 spring.h2.console.settings.web-admin-password 속성이 추가되었습니다.
  • CqlSession-Based Health Indicators for Apache Cassandra Spring Data Cassandra에서 제공하던 Health Indicator가 지원되지 않으며, CqlSession 기반의 CassandraDriverHealthIndicator, CassandraDriverReactiveHealthIndicator가 추가되었습니다.
  • Filtered Scraping with Prometheus /actuator/prometheus end-point에서 응답하는 Sample을 Filter 하는 용도로 새로운 query parameter가 지원됩니다.? includedNames=jvm_memory_used_bytes
  • Spring Security SAML Configuration Properties SAML2 관련 acs와 decryption 속성이 추가되었습니다.
  • Failure Analyzers 이제 FailureAnalizers가 ApplicationContext 등이 구성되지 않는 경우도 포함합니다. BeanFactoryAware 또는 EnvironmentAware를 구현하여 추가하는 분석기는 ApplicationContext가 만들어지지 않는 경우 사용되지 않습니다.
  • Jar Optimizations Spring Boot jar를 생성할 때 모든 비어있는 (마킹 용도의) starter dependencies들이 제거됩니다. spring-boot-autoconfigure-processor 또한 Build 시에만 유효하며 이후 제거됩니다.
  • Spring Boot Gradle Plugin Gradle bootJar task의 DSL이 설정 속성이 변경되었습니다. mainClassName → mainClass
  • JUnit 5’s Vintage Engine Removed from spring-boot-starter-test Junit4 vintage engine이 해당 의존성에서 제거되었습니다. (org.junit.Test 등 사용 시 Compile Error 발생) 해당 버전에서 Junit4를 사용하려면 별도의 vintage 의존성을 추가하여야 합니다.
  • Support K8s ConfigMap @ConfigurationProperties나 environment 객체를 통해 K8s ConfigMap에 저장된 설정에 접근할 수 있습니다.
  • Embedded database detection in-memory로 띄워진 Database가 아닌 경우 Database 초기화 및 사용자 이름 설정 (default = sa)가 사용되지 않습니다. (Server, Hybrid H2, File based Derby, HSQL 등)
    • spring.datasource.initialization-mode를 통해 데이터베이스 초기화를 하도록 구성할 수 있습니다.
  • User-defined MongoClientSettings no longer customized MongoClientSettings를 Bean으로 정의하여 사용하는 경우 Auto-configuration을 통한 properties가 적용되지 않습니다. 그렇기에 MongoPropertiesClientSettingsBuilderCustomizer를 활용해 properties, environment를 인자로 넘겨 설정한 다음 반환하여야 합니다.
  • Metrics export in integration tests
  • Elasticsearch RestClient - RestClient bean이 더 이상 Auto-configuration 되지 않습니다. RestHighLevelClient bean은 계속 지원됩니다.
  • Default Servlet Registration 해당 버전부터 Servlet Container에서 제공하는 DefaultServlet을 등록하지 않습니다.
    • 사용을 위해서는 server.servlet.register-default-servlet=true property를 정의하여야 합니다.
  • HTTP traces no longer include cookie headers by default
  • Supported Profile Group 특정 Profile에 여러 Profile들을 묶어 Grouping 하여 같이 사용할 수 있습니다. spring.profiles.group.{profile}={sub profiles...}

 

 

 

Spring Boot 2.5.x (WIP)

 

 

 

 

 

참고 자료


이 글은 Java 환경에서 Dynamic Proxy 기반의 기능들을 사용할 때 주로 발생하는 문제인 Self-Invocation을 Lambda를 통해 어떻게 회피할 수 있는지에 대한 내용을 담고 있는 글입니다.

Dynamic Proxy, AOP에 대한 이론적인 부분(발생 원인, 호출 흐름 등)들은 다루지 않을 것이기 때문에 다른 글들을 참고하시길 바랍니다.


Lambda와 관련해서는 학습을 진행하며 작성한 글이기 때문에 잘못된 내용이 있을 수도 있습니다. 틀린 내용이 있다면 채널톡 혹은 댓글을 통해 피드백해주시길 바랍니다. ㅎㅎ


Self Invocation

Self Invocation은 Dynamic Proxy 기반의 기능들을 사용할 때 사소한 실수로 인하여 자주 발생하는 문제입니다. 쉽게 설명하자면, 객체 외부에서 보내는 메시지(요청)에 대해서만 반응하도록 설계되어 있기에 내부의 요청에 대해서는 반응하지 못하기 때문입니다.

JVM 생태계에서 많은 사랑을 받는 Spring Framework는 다양한 기능들을 Dynamic Proxy 방식으로 제공하고 있습니다.

  • @Transcational, @Async, @Cacheable, AOP(Before, Around) 등의 Aspect 기능들이 속합니다.


이러한 기능들에 대한 보편적인 해결 방법으로는

  • AspectJ으로 전환 혹은 부분 적용 (Weaving 방식 전환)
    • @EnableTransactionManagement(proxyTargetClass = true, mode = AdviceMode.ASPECTJ)
  • AopContext의 currentProxy() 메서드를 통해 해당 객체를 감싸고 있는 Proxy 객체를 반환
    • ((Type) AopContext.currentProxy()). someMethod();
  • 상태 변수를 통한 자기 참조 (@Autowired나 ApplicationContext.getBean() 활용)
  • 객체 외부에서 호출하는 메서드에 Dynamic Proxy가 반응하도록 설정하기

등이 있지만, 이것들을 적용하기에는 과한 상황이거나 Spring Container에 종속되는 좋지 않은 코드를 작성하게 될 수 있습니다.


Lambda를 이용하여 Self-Invocation 회피

Lambda로 어떻게 Self-Invocation을 회피할 수 있을까요?

결론만 이야기하자면 Lambda를 통해 실행되는 메서드를 접근하기 위해서 현재 호출 객체 외부로 메시지가 나가고, 최종적으로 호출해야 되는 메서드를 찾아 요청하였을 때, 외부에서 전달되기 때문에 감싸고 있는 Proxy가 해당 요청을 인지할 수 있기 때문입니다.

Java Lambda는 Reflection API, MethodHandle, LambdaMetaFactory 인터페이스를 이용하여 기능을 제공합니다.


Lambda Method를 호출하는 흐름

  1. Reflection API를 통해 실행 대상이 되는 메서드 정보를 가져옵니다.
  2. MethodHandle Lookup API에 정의된 Factory 메서드를 통해 Lookup 객체를 가져옵니다.
  3. 1번에서 가져온 정보를 Lookup.unreflect() 메서드에 전달함으로써 해당 메서드의 구현, 수행 정보를 알고 있는 MethodHandle 객체를 가져옵니다. (실제 메서드를 바라보고 있는 일종의 포인터)
  4. LambdaMetafactory.metafactory() 메서드에 필요한 인자를 넘겨 CallSite 객체를 반환받습니다. 해당 객체는 Functional Interface를 객체로 다룰 수 있으며, 매개 변수를 설정하고 응답을 반환합니다. 인자 목록은 밑에 나열하였습니다.
    1. 접근 권한을 가지고 있는 Lookup 객체
    2. 구현할 메서드 이름(Supplier Interface를 사용했을 경우 get이라는 문자열을 넘긴다.)
    3. 메서드의 매개 변수와 응답 값의 Class 정보. methodType(Supplier.class, {Type}. class)
    4. 함수 객체(Lambda)에 의해 반환될 응답 값의 유형. methodType(Object.class)
    5. 메서드의 구현 및 수행 방식을 알고 있는 MethodHandle 객체
    6. 호출 시 동적으로 적용되어야 할 응답 값의 세부 정보. methodType({Type}. class)
  5. callSite.getTarget()을 통해 호출할 메서드 정보를 가져오고 bindTo({Type}. class)를 통해 메서드에 넘길 인자 값을 지정한 뒤 Invoke를 통해 메서드를 호출합니다.

(사실상 그 호출하는 형태는 Dynamic Proxy와 유사한 것 같습니다)

꽤 복잡하지만, 결론적으로는 Lambda를 통해 호출되는 인터페이스를 인스턴스 화 하고, 메서드를 호출하기 때문에 객체 외부 요청으로 다시 돌아오는 것입니다.

  • 이 흐름은 Bytecode의 invokedynamic이 호출된 경우에 수행 흐름을 나타냅니다. Lambda에서 Function Interface를 사용하지 않고 단순한 로직을 사용하는 경우 static 메서드를 생성하여 활용하기도 합니다.
    • invokedynamic은 Bootstrap 메서드라는 특정한 메서드를 호출하고, 이를 통해 위의 호출 프로세스를 초기화 및 실행하여 CallSite 객체를 반환받습니다. (InnerClassLambdaMetafactory를 활용하여 내부 클래스 생성 후 반환) 
  • 한번 Bootstrap 메서드가 실행된다면 이후에는 기존의 CallSite와 MethodHandle를 재사용하여 요청을 처리합니다.

 

 

 

구현 예시

Self-Invocation을 회피하기 위해 구현한 TransactionalHandler입니다.

@Service
public class TransactionHandler {

    @Transactional(propagation = Propagation.REQUIRED)
    public <T> T runInTransaction(Supplier<T> supplier) {
        return supplier.get();
    }
}


Sample Service.

@Service
@RequiredArgsConstructor
public class SampleService {

    private final SampleRepository someRepository;
    private final TransactionHandler transactionHandler;

    // 특정 객체에서 호출하는 Method
    public void addNewSamples(List<Sample> samples) {
        return samples.forEach(sample ->
            transactionHandler.runInTransaction(() -> addSample(sample.toEntity()))
        )
    }

    // 외부에서 호출되는 Method
    @Transcational
    public SomeEntity addSample(SampleEntity newSample) {
        return someRepository.insertSample(newSample);
    }
}


단순한 예시여서 실제 효용성과 조금 동떨어진 감은 있지만, 실제 업무 중 활용할 수 있을만한 부분을 특정하실 수 있을 것이라고 생각합니다. ㅎㅎ 이 글은 여기까지입니다. 감사합니다.


참고 자료

+ Recent posts