SpringBoot - 외부설정과 프로필(2)

Kwon Yongho·2023년 7월 4일
0

Spring

목록 보기
34/37
post-thumbnail
  1. 외부 설정 사용
  2. 외부설정과 프로필2 - YAML
  3. 외부설정과 프로필2 - @Profile

1. 외부 설정 사용

springboot-external-read 프로젝을 새로 만들었습니다.

1-1. Environment

다양한 외부 설정 읽기
스프링은 Environment 는 물론이고 Environment를 활용해서 더 편리하게 외부 설정을 읽는 방법들을 제공한다.

스프링이 지원하는 다양한 외부 설정 조회 방법

  • Environment
  • @Value - 값 주입
  • @ConfigurationProperties - 타입 안전한 설정 속성

예제
MyDataSource

package hello.datasource;

import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.time.Duration;
import java.util.List;

@Slf4j
@Data
public class MyDataSource {

    private String url;
    private String username;
    private String password;
    private int maxConnection;
    private Duration timeout;
    private List<String> options;

    public MyDataSource(String url, String username, String password, int maxConnection, Duration timeout, List<String> options) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.maxConnection = maxConnection;
        this.timeout = timeout;
        this.options = options;
    }

    @PostConstruct
    public void init() {
        log.info("url={}", url);
        log.info("username={}", username);
        log.info("password={}", password);
        log.info("maxConnection={}", maxConnection);
        log.info("timeout={}", timeout);
        log.info("options={}", options);
    }
    
}
  • url, username, password : 접속 url, 이름, 비밀번호
  • maxConnection : 최대 연결 수
  • timeout : 응답 지연시 타임아웃
  • options : 연결시 사용하는 기타 옵션들

application.properties

my.datasource.url=local.db.com
my.datasource.username=local_user
my.datasource.password=local_pw
my.datasource.etc.max-connection=1
my.datasource.etc.timeout=3500ms
my.datasource.etc.options=CACHE,ADMIN

properties 캐밥 표기법
properties는 자바의 낙타 표기법(maxConnection)이 아니라 소문자와 - (dash)를 사용하는 캐밥 표기법(max-connection)을 주로 사용한다.

MyDataSourceEnvConfig

package hello.config;

import hello.datasource.MyDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

import java.time.Duration;
import java.util.List;

public class MyDataSourceEnvConfig {

    private final Environment env;


    public MyDataSourceEnvConfig(Environment env) {
        this.env = 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");
        int 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); // List 변환 [A,B]
        return new MyDataSource(url, username, password, maxConnection, timeout, options);
    }
}
  • 외부 속성을 읽어서 앞서 만든 MyDataSource에 값을 설정하고 스프링 빈으로 등록
  • Environment를 사용하면 외부 설정의 종류와 관계없이 코드 안에서 일관성 있게 외부 설정을 조회할 수 있다.
  • Environment.getProperty(key, Type)를 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다.

ExternalReadApplication

package hello;

import hello.config.MyDataSourceEnvConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@Import(MyDataSourceEnvConfig.class)
@SpringBootApplication(scanBasePackages = "hello.datasource")
public class ExternalReadApplication {

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

}
  • @SpringBootApplication(scanBasePackages = "hello.datasource")
    • 예제는 @Import로 설정 정보를 계속 바꿀 것이기 때문에 설정 정보를 바꾸면서 사용하기 위해 hello.config의 위치를 피해서 컴포넌트 스캔 위치를 설정했다.

실행 결과

단점
Environment를 직접 주입받고, env.getProperty(key)를 통해서 값을 꺼내는 과정을 반복해야 한다는 점이다.

1-2. @Value

@Value를 사용하면 외부 설정값을 편리하게 주입받을 수 있다.
(@Value도 내부에서는 Environment를 사용한다.)

MyDataSourceValueConfig

package hello.config;

import hello.datasource.MyDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.List;

@Slf4j
@Configuration

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);
    }

    @Bean
    public MyDataSource myDataSource2(
            @Value("${my.datasource.url}") String url,
            @Value("${my.datasource.username}") String username,
            @Value("${my.datasource.password}") String password,
            @Value("${my.datasource.etc.max-connection:10}") int maxConnection, // 기본 값 설정
            @Value("${my.datasource.etc.timeout}") Duration timeout,
            @Value("${my.datasource.etc.options}") List<String> options) {
        return new MyDataSource(url, username, password, maxConnection, timeout, options);
    }
}
  • @Value${}를 사용해서 외부 설정의 키 값을 주면 원하는 값을 주입 받을 수 있다.
  • @Value는 필드에 사용할 수도 있고, 파라미터에 사용할 수도 있다.
    • myDataSource1()은 필드에 주입 받은 설정값을 사용한다.
    • myDataSource2()는 파라미터를 통해서 설정 값을 주입 받는다.

기본값
예) @Value("${my.datasource.etc.max-connection:10}"): key가 없는 경우 10을 사용한다.

@Import 변경 후 실행
my.datasource.etc.max-connection 주석처리

단점

  • @Value를 사용하는 방식도 좋지만, @Value 로 하나하나 외부 설정 정보의 키 값을 입력받고, 주입 받아와야 하는 부분이 번거롭다.
  • 설정 데이터를 보면 하나하나 분리되어 있는 것이 아니라 정보의 묶음으로 되어 있다.

1-3. @ConfigurationProperties 시작

스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공한다. 이것을 타입 안전한 설정 속성(Type-safe Configuration Properties)이라 한다.

MyDataSourcePropertiesV1

package hello.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {

    private String url;
    private String username;
    private String password;
    private Etc etc = new Etc();

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

}
  • 외부 설정을 주입 받을 객체를 생성한다. 그리고 각 필드를 외부 설정의 키 값에 맞추어 준비한다.
  • @ConfigurationProperties이 있으면 외부 설정을 주입 받는 객체라는 뜻이다. 여기에 외부 설정 KEY의 묶음 시작점인 my.datasource를 적어준다.

설정 속성을 실제 어떻게 사용하는지 확인해보자.
MyDataSourceConfigV1

package hello.config;

import hello.datasource.MyDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
public class MyDataSourceConfigV1 {

    private final MyDataSourcePropertiesV1 properties;


    public MyDataSourceConfigV1(MyDataSourcePropertiesV1 properties) {
        this.properties = properties;
    }

    @Bean
    public MyDataSource dataSource() {
        return new MyDataSource(
                properties.getUrl(),
                properties.getUsername(),
                properties.getPassword(),
                properties.getEtc().getMaxConnection(),
                properties.getEtc().getTimeout(),
                properties.getEtc().getOptions());
    }
    
}
  • @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
    • 스프링에게 사용할 @ConfigurationProperties를 지정해주어야 한다. 이렇게 하면 해당 클래스는 스프링 빈으로 등록되고, 필요한 곳에서 주입 받아서 사용할 수 있다.
  • private final MyDataSourcePropertiesV1 properties설정 속성을 생성자를 통해 주입 받아서
    사용한다.

@Import 바꾸고 실행

타입 안전
maxConnection=kwonyongho로 입력하고 실행

숫자를 입력하는 곳에 문자를 입력하는 문제를 방지해준다. 그래서 타입 안전한 설정 속성이라고 한다.

application.properties에 필요한 외부 설정을 추가하고, @ConfigurationProperties를 통해서 MyDataSourcePropertiesV1에 외부 설정의 값들을 설정했다. 그리고 해당 값들을 읽어서 MyDataSource를 만들었다.

문제

  • MyDataSourcePropertiesV1은 스프링 빈으로 등록된다. 그런데 Setter를 가지고 있기 때문에 누군가 실수로 값을 변경하는 문제가 발생할 수 있다. 여기에 있는 값들은 외부 설정값을 사용해서 초기에만 설정되고, 이후에는 변경하면 안된다.
  • 이럴 때 Setter를 제거하고 대신에 생성자를 사용하면 중간에 데이터를 변경하는 실수를 근본적으로 방지할 수 있다.

1-4. @ConfigurationProperties 생성자

@ConfigurationPropertiesGetter, Setter를 사용하는 자바빈 프로퍼티 방식이 아니라 생성자를 통해서 객체를 만드는 기능도 지원한다.

MyDataSourcePropertiesV2

package hello.config;

import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

import java.time.Duration;
import java.util.List;

@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, @DefaultValue 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;
        public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }
    
}
  • 생성자를 만들어 두면 생성자를 통해서 설정 정보를 주입한다.
  • @Getter 롬복이 자동으로 getter를 만들어준다.
  • @DefaultValue : 해당 값을 찾을 수 없는 경우 기본값을 사용한다.
    • @DefaultValue Etc etc
      • etc를 찾을 수 없을 경우 Etc객체를 생성하고 내부에 들어가는 값은 비워둔다. (null, 0 )
    • @DefaultValue("DEFAULT") List<String> options
      • options를 찾을 수 없을 경우 DEFAULT라는 이름의 값을 사용한다.

MyDataSourceConfigV2

package hello.config;

import hello.datasource.MyDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV2.class)
public class MyDataSourceConfigV2 {
    
    private final MyDataSourcePropertiesV2 properties;
    
    public MyDataSourceConfigV2(MyDataSourcePropertiesV2 properties) {
        this.properties = properties;
    }
    
    @Bean
    public MyDataSource dataSource() {
        return new MyDataSource(
                properties.getUrl(),
                properties.getUsername(),
                properties.getPassword(),
                properties.getEtc().getMaxConnection(),
                properties.getEtc().getTimeout(),
                properties.getEtc().getOptions());
    }
}
  • MyDataSourcePropertiesV2를 적용하고 빈을 등록

@Import 변경 후 실행

1-5. @ConfigurationProperties 검증

  • 만약 숫자의 법위나 문자의 길이 같은 부분을 검증 하려면 어떻게 할까?
  • 개발자가 직접 하나하나 검증 코드를 작성해도 되지만, 자바에는 자바 빈 검증기(java bean validation)이라는 훌륭한 표준 검증기가 제공된다.
  • @ConfigurationProperties은 자바 객체이기 때문에 스프링이 자바 빈 검증기를 사용할 수 있도록 지원한다.
  • 자바 빈 검증기를 사용하려면 spring-boot-starter-validation이 필요하다.

MyDataSourcePropertiesV3

package hello.config;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import org.hibernate.validator.constraints.time.DurationMax;
import org.hibernate.validator.constraints.time.DurationMin;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import java.time.Duration;
import java.util.List;

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

    @NotEmpty
    private String url;

    @NotEmpty
    private String username;

    @NotEmpty
    private String password;

    private Etc etc;

    public MyDataSourcePropertiesV3(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;
        
        public Etc(int maxConnection, Duration timeout, List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }
}
  • Spring MVC에서 학습 했던 내용이다.
  • @NotEmpty url , username , password는 항상 값이 있어야 한다. 필수 값이 된다.
  • @Min(1) @Max(999) maxConnection : 최소 1 , 최대 999 의 값을 허용한다.
  • @DurationMin(seconds = 1) @DurationMax(seconds = 60) : 최소 1, 최대 60초를 허용한다.

@Import 변경 후 실행

maxConnection=0 설정, timeout=100 설정

ConfigurationProperties 장점

  • 외부 설정을 객체로 편리하게 변환해서 사용할 수 있다.
  • 외부 설정의 계층을 객체로 편리하게 표현할 수 있다.
  • 외부 설정을 타입 안전하게 사용할 수 있다.
  • 검증기를 적용할 수 있다.

2. YAML

YAML(YAML Ain't Markup Language)은 사람이 읽기 좋은 데이터 구조를 목표로 한다. 확장자는
yaml, yml이다. 주로 yml을 사용한다.

application.properties 예시

environments.dev.url=https://dev.example.com
environments.dev.name=Developer Setup
environments.prod.url=https://another.example.com
environments.prod.name=My Cool App

application.yml

environments:
 dev:
 	url: "https://dev.example.com"
 	name: "Developer Setup"
 prod:
 	url: "https://another.example.com"
 	name: "My Cool App"
  • YAML은 space(공백)로 계층 구조를 만든다. space는 1칸을 사용해도 되는데, 보통 2칸을 사용한다.
    일관성있게 사용하지 않으면 읽기 어렵거나 구조가 깨질 수 있다.
  • 구분 기호로 :를 사용한다. 만약 값이 있다면 이렇게 key: value : 이후에 공백을 하나 넣고 값을 넣어주면 된다.

properties -> yml 바꾸기(application.properteis -> application_backup.properties로 변경)

  • application.properties , application.yml을 같이 사용하면 application.properties
    우선권을 가진다.
  • 이것을 둘이 함께 사용하는 것은 일관성이 없으므로 권장하지 않는다.
  • 참고로 실무에서는 설정 정보가 많아서 보기 편한 yml 을 선호한다.

application.yml

my:
  datasource:
    url: local.db.com
    username: local_user
    password: local_pw
    etc:
      max-connection: 1
      timeout: 60s
      options: LOCAL, CACHE

정상작동 하는 것을 확인 할 수 있다.

3. @Profile

프로필과 외부 설정을 사용해서 각 환경마다 설정값을 다르게 적용하는 것은 이해했다.
그런데 설정값이 다른 정도가 아니라 각 환경마다 서로 다른 빈을 등록해야 한다면 어떻게 해야할까?

application.yml 수정

my:
  datasource:
    url: local.db.com
    username: local_user
    password: local_pw
    etc:
      max-connection: 1
      timeout: 60s
      options: LOCAL, CACHE
---
spring:
    config:
      activate:
        on-profile: dev
my:
  datasource:
    url: dev.db.com
    username: dev_user
    password: dev_pw
    etc:
      maxConnection: 10
      timeout: 60s
      options: DEV, CACHE
---
spring:
  config:
    activate:
      on-profile: prod
my:
  datasource:
    url: prod.db.com
    username: prod_user
    password: prod_pw
    etc:
      maxConnection: 50
      timeout: 10s
      options: PROD, CACHE

결제 기능 추가

  • 로컬 환경: 가짜 결제 기능
  • 운영 환경: 실제 결제 기능

간단히 LocalPayClientProdPayClinet를 구현하였고 log로 남길 수 있게 하였다.

PayConfig

package hello.pay;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Slf4j
@Configuration
public class PayConfig {

    @Bean
    @Profile("default")
    public LocalPayClient localPayClient() {
        log.info("LocalPayClient 빈 등록");
        return new LocalPayClient();
    }

    @Bean
    @Profile("prod")
    public ProdPayClient prodPayClient() {
        log.info("ProdPayClient 빈 등록");
        return new ProdPayClient();
    }
}
  • @Profile애노테이션을 사용하면 해당 프로필이 활성화된 경우에만 빈을 등록한다.
    • default 프로필(기본값)이 활성화 되어 있으면 LocalPayClient를 빈으로 등록한다.
    • prod 프로필이 활성화 되어 있으면 ProdPayClient를 빈으로 등록한다

OrderRunner

package hello.pay;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OrderRunner implements ApplicationRunner {

    private final OrderService orderService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        orderService.order(1000);
    }
}

ApplicationRunner인터페이스를 사용하면 스프링은 빈 초기화가 모두 끝나고 애플리케이션 로딩이
완료되는 시점에 run(args) 메서드를 호출해준다.

프로필 없이 실행

prod 프로필 실행
--spring.profiles.active=prod프로필 활성화 적용

@Profile

  • @Profile은 특정 조건에 따라서 해당 빈을 등록할지 말지 선택한다. 어디서 많이 본 것 같지 않은가? 바로 @Conditional이다. 코드를 보면 @Conditional(ProfileCondition.class)를 확인할 수 있다.

0개의 댓글