WEB/Spring Boot

외부설정과 프로필

Tony Lim 2023. 5. 14. 16:36

개발 db와 운영 db url 달라 빌드를 2번해서 배포하는 경우 같은 소스코드에서 나온 빌드 결과물인지 검증하기 어렵다.

빌드는 한번만하고 각 환경에 맞추어 실행 시점에 외부 설정값을 주입하는 방법이 좋다.

빌드를 한번하고 외부 설정을 주입하는 방식 , 새로운 환경이 추가되어도 손쉽게 적용할 수 있다.

public class CommandLineBean {

    private final ApplicationArguments arguments;

    @PostConstruct
    public void init() {
        log.info("source {}", List.of(arguments.getSourceArgs()));
        log.info("optionNames {}", arguments.getOptionNames());
        Set<String> optionNames = arguments.getOptionNames();
        for (String optionName : optionNames) {
            log.info("option args {}={}",optionName,arguments.getOptionValues(optionName));
        }
    }
}

Command Line Argument도 스프링부트에서 빈으로 만들어서 쉽게 꺼낼수 있게 해놓음

하지만 OS 변수  ,JVM 변수 다 읽는 방법이 다 다르다.

 

 


외부 설정 - 스프링 통합

외부 설정값을 OS 환경변수를 사용하다가 자바 시스템 속성으로 변경하는 경우에 소스코드를 다시 빌드하지 않고 그대로 사용할 수 있다.

스프링은 Environment , PropertySource 라는 추상화로 가능하게 해준다.

 

스프링은 PropertySource라는 추상 클래스를 제공하고 , 각각의 외부 설정을 조회하는 ~~PropertySouce 구현체들을 만들어 두었다.
스프링은 로딩 시점에 필요한 PropertySource들을 생성하고 Environment에서 사용할 수 있게 연결해둔다.

Enviornment를 통해서 특정 외부 설정에 종속되지 않고 , 일관성 있게 key=value 형식의 외부설정에 접근 할 수 있다.

 

겹치는것이 있을때 우선순위 (jvm property , commad line property등 같은 key,value로 등록할떄)

  • 더 유연한 것이 우선권을 가짐. 
    변경하기 어려운 파일보다 실행시 원하는 값을 줄 수 있는 jvm property이 더 우선권을 가짐
  • 범위가 넓은것보다 좁은 것이 우선권을 가짐
    jvm property은 해당 JVM 아넹서 모두 접근할 수 있음.
    반면에 commad line property는 main 의 args를 통해서 들어오기 때문에 접근 범위가 더 좁음

설정 데이터 - 외부 파일

application.properties에 원하는 설정 데이터파일을 key=value 값으로 준비해둔다.
그러면 스프링이 해당 파일을 읽어서 PropertySource 구현체를 제공한다.

url=dev.db.com
username=dev_user
password=dev_pw

그러면 위의 코드 변경없이 Environment에서 다 잘 읽어오게 된다.

하지만 이런 설정파일이 각 서버마다 존재하면 각 서버에서 다 변경을 해줘야한다.
또한 설정 값만 따로 관리되니까 이게 소스코드에 어떤 영향을 끼치는 파악하기 어렵다.

 

application-dev.properties , application-prod.properties 를 한번에 빌드하고 profile을 주입해서 각각 다른 설정 파일들을 읽어오게 변경할 수 있다.

spring.profile.active=dev 하면 applicatino-dev.properties 를 사용하게되고 prod도 마찬가지이다.

url=local.db.com
username=local_user
password=local_pw
#---
spring.config.activate.on-profile=dev
url=dev.db.com
username=dev_user
password=dev_pw
#---
spring.config.activate.on-profile=prod
url=prod.db.com
username=prod_user
password=prod_pw
#--
url=hello.db.com

하나의 파일로 통합도 가능하다.
가장 최상위에 있는것은 아무 profile도 지정해주지 않았을때 사용하게 되는 default 설정이된다.

제일 처음에 default는 기본적으로 읽어들인후에 아래로 내려가면서 profile에 따라 덮어씌울지말지 결정하게 된다.

dev,prod profile을 다 지정해줘도 url의 경우 맨 아래에 있는 default로 덮어씌워지게된다.

 

설정 데이터(application.properties)
OS환경변수
자바시스템속성
커맨드 라인 옵션 인수
@TestPropertySource(테스트에서 사용함)

아래로 내려갈수록 우선순위가 높음. 변경하기 쉬우니까 (즉 필요할 때마다 변경했을 테니 우선순위가 더 높아야한다.)

 


외부 설정 사용 - Environment

Environment -> @Value -> @ConfigurationProperties 순으로 오른쪽으로 갈 수록 유용하고 쉽게 외부설정을 가져오게 해준다.

my.datasource.url=local.db.com
my.datasource.username=username
my.datasource.password=password
my.datasource.etc.max-connection=1
my.datasource.etc.timeout=3500ms
my.datasource.etc.options=CACHE,ADMIN
public class MyDataSourceEnvConfig {

    private final Environment env;

    @Bean
    public MyDataSource myDataSource() {
        String url = env.getProperty("my.datasource.url");
        String username = env.getProperty("my.datasource.username");
        String password = env.getProperty("my.datasource.password");
        Integer maxConnection = env.getProperty("my.datasource.etc.max-connection", Integer.class);
        Duration timeout = env.getProperty("my.datasource.etc.timeout", Duration.class);
        List<String> options = env.getProperty("my.datasource.etc.options", List.class);

        return new MyDataSource(url,username,password,maxConnection,timeout,options);
    }
}

타입에 대한 변환도 지원한다. 3500ms 는 Duration으로 List도 지원하게된다.


외부설정 사용 - @Value

public class MyDataSourceValueConfig {

    @Value("${my.datasource.url}")
    private String url;
    @Value("${my.datasource.username}")
    private String username;
    @Value("${my.datasource.password}")
    private String password;
    @Value("${my.datasource.etc.max-connection}")
    private int maxConnection;
    @Value("${my.datasource.etc.timeout}")
    private Duration timeout;
    @Value("${my.datasource.etc.options}")
    private List<String> options;

    @Bean
    public MyDataSource myDataSource1() {
        return new MyDataSource(url,username,password,maxConnection,timeout,options );
    }

    public void myDataSource2( @Value("${my.datasource.url}") String url) {
        System.out.println("url = " + url);
    }
}

parameter에서도 잘 작동한다. 마찬가지로 타입(Duration, List등) 변환도 지원한다. 내부적으로 Environment를 사용하기 때문이다.

@Value("${my.datasource.etc.max-connection:2}")

2 와 같이 default 값을 줄 수 있다.

하지만 key 값들을 일일이 다 적어줘야하는것은 마찬가지이다. 


외부설정 사용 - @ConfigurationProperties

type-safe configuration properties

외부 설정을 자바코드로 관리할 수 있게되고 타입을 가질 수 있게된다.

@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {
    
    private String url;
    private String username;
    private String password;
    private Etc etc;
    
    @Data
    public static class Etc {
        private int maxConnection;
        private Duration timeout;
        private List<String> options = new ArrayList<>();
    }
}
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
@RequiredArgsConstructor
public class MyDataSourceConfigV1 {
    
    private final MyDataSourcePropertiesV1 properties;
    
    public void test() {
        String password = properties.getPassword();
        Duration timeout = properties.getEtc().getTimeout();
    }
}

@EnableConfigurationProperties에 빈으로 등록할 클래스를 명시하면 스프링에서 빈으로 만드는 시점에 외부설정 값들을 다 읽어와서 채워넣어서 생성해준다.

또한 타입을 자바단에서 정해놔서 외부설정에 잘못된 타입이 있으면 빈 생성시점에서 오류가 난다.

@ConfigurationPropertiesScan({"hello"})
public class ExternalReadApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExternalReadApplication.class, args);
    }
}

이미 @ConfigurationProperties도 달아줬는데 또 @Enable~ 을 해주는것은 불편하다. 

이를 해결하기위해 @ConfigurationPropertiesScan이  @ConfigurationProperties 을 감지하여 다 빈으로 등록을 해준다. @Enable ~ 할 필요가 없어진다.

하지만 현재 Setter가 존재하여 제약이 부족하다.


@ConfigurationProperties 생성자

@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {

    private String url;
    private String username;
    private String password;
    private Etc etc;

    public MyDataSourcePropertiesV2(String url, String username, String password, Etc etc) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.etc = etc;
    }

    @Getter
    public static class Etc {
        private int maxConnection;
        private Duration timeout;
        private List<String> options = new ArrayList<>();

        public Etc(int maxConnection, Duration timeout, List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }
}

생성자주입으로도 값을 동일하게 넣어서줘서 빈으로 만들어주게 된다.

@인자 앞에 @DefaultValue Etc etc 이런식으로 넣어주면 외부설정 값이 존재하지 않아도 기본 객체를 만들어서 null, 0값들을 채워넣어준다.

또한 생성자가 클래스당 지금처럼 1개이면 상관없지만 2개 이상이면 @어떤 생성자로 바인딩할것인지 @ConstructorBinding을 써줘야한다.

하지만 max-connection =0 으로 해놓으면 db connection이 맺어지지 않으니 validation을 하고싶다.


@ConfigurationProperties 검증

java bean validation이라는 표준 검증기가 제공된다.

@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {

    @NotEmpty
    private String url;
    @NotEmpty
    private String username;
    private String password;
    private Etc etc;

    public MyDataSourcePropertiesV2(String url, String username, String password, Etc etc) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.etc = etc;
    }

    @Getter
    public static class Etc {
        @Min(1) 
        @Max(999)
        private int maxConnection;
        
        @DurationMin(seconds = 1)
        @DurationMax(seconds = 60)
        private Duration timeout;
        private List<String> options = new ArrayList<>();

        public Etc(int maxConnection, Duration timeout, List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }
}

@Profile

설정값이 다른 정도가 아니라 각 환경마다 아예 서로 다른 빈을 등록하고 싶을 때 쓸 수 있는 기능이다.

@Configuration
public class PayConfig {

    @Bean
    @Profile("default")
    public LocalPayClient localPayClient() {
        System.out.println("PayConfig.localPayClient");
        return new LocalPayClient();
    }

    @Profile("prod")
    @Bean ProdPayClient prodPayClient() {
        System.out.println("PayConfig.prodPayClient");
        return new ProdPayClient();
    }
}

spring.profile.active 값에 따라서 어떤 bean을 등록할지 결정할 수 있다.

@Conditional 을 쓰고있고 ProfileCondition 구현체를 통해 어떤 빈을 등록할지 구분하는 로직이 존재한다.