[시즌2] 개인서버 개발/시즌2 설계(완)

Spring Properties from Lib Resources (PropertySourcesPlaceholderConfigurer, EnvironmentPostProcessor)

북극곰은콜라 2024. 1. 2. 17:07
반응형


개요

Spring 프로젝트의 Properties를 구성하는 방법은 여러 가지가 있다.
그중 프로젝트 외부(external jar)의 resources에서 Load 하여 추가하는 방법에 대해 정리하고자 한다.

 


문제 상황

1. Github Project를 public으로 설정하면 일부 정보들은 노출되지 않도록 관리해야 한다.
ex) db password.. etc
2. Multi Project 구조 같이 공통적으로 적용되어야 하는 Properties를 따로 관리해야 할 때
ex) Actuator 설정, 접속정보 등
Properties file을 Runtime에 외부에서 Load 하게 되면 위 문제상황을 해결할 수 있다.
일반적으로는 Server에 관리되는 Properties 파일을 두고 runtime 시점에 로드하거나, System Properties로 설정하는 방법을 사용한다.
이번에 소개하는 방식은, lib(jar) 안에 들어있는 Properties 파일을 runtime 시점에 load 하는 방법이다.

 


PropertySourcesPlaceholderConfigurer

PropertySourcesPlaceholderConfigurer는 Context에 적용된 Properties에 접근하여 작업을 할 수 있는 config bean이다.
org.springframework.core.env의 PropertySources를 통해 get/set 할 수 있다.

PropertySourcesPlaceholderConfigurer 분석

Member Variables Desc
MutablePropertySources propertySources 적용할 PropertySource 들
PropertySources appliedPropertySources 기존에 적용된 PropertySource들
Environment environment Spring Context
Method Desc
void setPropertySources(PropertySources propertySources) PropertySources를 추가할 수 있는 method이다.
configurer당 1개의 PropertySources(Iterable)을 가지며 최종 적용 시 기존 Sources에 추가된다.
PropertySources getAppliedPropertySources() Runtime 상 설정된 Properties를 가져오는 method
PropertySources를 add할 수 있다.
대부분 자동설정되는 property는 immutable 하다.
configurer에서 주로 사용되는 property getter, setter들이다.
그 외 상위 클래스의 method를 활용해서 세팅하는 것도 가능하다.

Secret Application Yaml 적용

@Bean
@ConditionalOnResource(resources = "classpath:application-secret.yml")
public YamlPropertiesFactoryBean yamlPropertiesFactoryBean() {
  YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();
  bean.setResources(new ClassPathResource("application-secret.yml"));
  return bean;
}

@Bean
@ConditionalOnBean(YamlPropertiesFactoryBean.class)
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(final YamlPropertiesFactoryBean yamlPropertiesFactoryBean) {
  PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
  Properties secretProperties = yamlPropertiesFactoryBean.getObject();
  if (secretProperties != null) {
    log.info("apply application-secret.yml");
    configurer.setProperties(secretProperties);
  }
  return configurer;
}
classpath의 yaml 파일을 읽어서 PropertySourcesPlaceholderConfigurer에 적용하는 코드이다.
configurer를 Bean으로 등록하면, BeanFactoryPostProcessor에서 설정파일 로드 및 주입작업을 진행한다.
BeanFactoryPostProcessor?
bean 생성 완료 전 개입가능한 processor
BeanPostProcessor보다 먼저 수행되며 특정 bean 생성 시점에 트리거링 됨
일반적으로 Dynamic 하게 bean을 생성 또는 변경하기 위해 사용됨

문제점 발생

@Value 등 Bean 생성 시점이 Processor보다 이후 life-cycle을 가진 부분에서는 문제가 없었으나
PropertySource load 순서가 보장될 수 없는 상황이 발생

문제 상황:
 - Actuator 설정을 전체 Multi-project에서 공유하기 위해 적용했지만, Actuator 라이브러리에서 management application properties를 주입받는 시점이 PropertySourcesPlaceholderConfigurer 보다 이전에 동작함.
따라서 PropertySourcesPlaceholderConfigurer로 actuator Property 주입이 불가

 


EnvironmentPostProcessor

package org.springframework.boot.env;

@FunctionalInterface
public interface EnvironmentPostProcessor {
  void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);
}
Spring env를 구성할 수 있는 Processor이다.
주요 메서드인 postProcessEnvironment()의 invoke 시점은 ApplicationContext가 재생성(refresh)하는 시점이다.
따라서 spring run부터 ~ context 구성 전에 어딘가에서 실행되기 때문에, lib의 properties 사용시점 전에 주입이 보장될 수 있다.

구현

PropertiesApplier

interface PropertiesApplier extends EnvironmentPostProcessor {
  @Override
  default void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application){
    try {
      environment
          .getPropertySources()
          .addFirst(this.getTargetProperties());
    } catch (IOException e) {
      // no logger loaded yet...
      e.printStackTrace(System.out);
    }
  }

  PropertySource<?> getTargetProperties() throws IOException;

  default PropertySource<?> getTargetProperties(final String fileName) throws IOException {
    List<PropertySource<?>> secretPropertySources = new YamlPropertySourceLoader()
        .load("classpath:" + fileName, new ClassPathResource(fileName));
    if (secretPropertySources == null || secretPropertySources.isEmpty()) {
      throw new IOException("fail to find " + fileName);
    }
    System.out.println(((OriginTrackedMapPropertySource) secretPropertySources.get(0)).getSource());
    return secretPropertySources.get(0);
  }
}
EnviromentPostProcessor를 상속받는 interface로 공통 함수를 구현했다.
전체 flow는
1. load property
2. add property
이며, 공통 함수로 classpath에서 file 가져오는 메서드가 있다.

Implements

// load secret properties from classpath
public class SecretPropertiesApplier implements PropertiesApplier {
  @Override
  public PropertySource<?> getTargetProperties() throws IOException {
    return this.getTargetProperties("application-secret.yml");
  }
}

// load actuator properties from classpath
public class ActuatorPropertiesApplier implements PropertiesApplier {
  @Override
  public PropertySource<?> getTargetProperties() throws IOException {
    return this.getTargetProperties("application-actuator.yml");
  }
}
application-secret.yml 및 application-actuator.yml을 classpath에서 읽어서 적용되도록 구현

resources/META-INF/spring.factories

org.springframework.boot.env.EnvironmentPostProcessor=\
com.pbear.lib.properties.ActuatorPropertiesApplier,\
com.pbear.lib.properties.SecretPropertiesApplier
EnviromentPostProcessor를 적용하기 위해서는 spring.factories에 명시를 해줘야 한다.
implements 한 두 class의 full path를 명시하여 적용.

 


Conclusion + Next..

Spring Property 주입은 매우 다양한 방법을 제공하고 있다.
덕분에 큰 작업 없이, 원하는 시점에 property 주입이 가능했다.
Spring에서는 기본적으로 EnviromentPostProcessor를 통한 property 주입을 추천하고 있다.
현재로 기본 LIB 구성은 완료되었다.
이후 공통부분이 나올 때마다 LIB를 업데이트하려고 한다.

다음 작업 예정은 starter library들이다.
작업 예정으로는
1. pbear-starter-webflux: webclient를 설정 및 build 하여 bean으로 등록, webflux관련 기본 로직 관련 starter
2. pbear-starter-r2dbc: r2dbc connection factory를 build하여 bean 등록 starter
3. pbear-starter-kafka: kafka Topic에 대한 정보를 제공해 주는 starter
4. pbear-starter-redis: Redis connection factory 및 기타 template 설정 starter
5. pbear-starter-mongodb: ...
6. pbear-starter-websocket: ...
7. ...
등 있다...
재사용성이 높은 모듈로 만드는 것이 목표
-> starter library만 implement 하면, 기본적인 접속정보 주입 등은 완료되도록.. + custom config에 열려있는 구조


xx. annotation 기반의 openfeign도 고려 중

기본 starter 구성 후 기본적인 구성인
account 관리 서버, gateway, access-control server (OAuth) 등 작업하고자 한다.
해당 서버들이 24년 첫 프로젝트가 될 예정

 


REFERENCE

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.html

https://www.baeldung.com/spring-boot-environmentpostprocessor

 

 

 

 

반응형