external-read 프로젝트 사용

0. 외부 설정 사용 - Environment

스프링에서 다양한 방법으로 외부 설정을 조회할 수 있다.

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

먼저 Evironment 로 조회하는 방법이다.

MyDataSource

package hello.datasource;


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

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

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

}

해당 클래스를 빈으로 등록하여 확인할 것이다. @PostConstruct 에서 확인을 위해 설정된 값을 출력한다.

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 )을 주로 사용한다. 참고로 이곳에 자바의 낙타 표기법을 사용한다고 해서 문제가 되는 것은 아니다. 스프링은 properties 에 캐밥 표기법을 권장한다.

MyDataSourceEnvConfig

package hello.config;

import hello.datasource.MyDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

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

@Slf4j
@Configuration
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);

        return new MyDataSource(url, username, password, maxConnection, timeout, options);
    }
}
  • MyDataSource 를 스프링 빈으로 등록하는 자바 설정이다
  • Environment.getProperty(key, Type) 를 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다. (스프링 내부 변환기가 작동한다.)
    • env.getProperty("my.datasource.etc.max-connection", Integer.class) : 문자 -> 숫자로 변환 env.getProperty("my.datasource.etc.timeout", Duration.class) : 문자 -> Duration (기간) 변환 env.getProperty("my.datasource.etc.options", List.class) : 문자 -> List 변환 ( A,B -> [A,B] )

ExternalReadApplication - 수정

package hello;

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

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

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

}
  • 설정 정보를 빈으로 등록해서 사용하기 위해 @Import(MyDataSourceEnvConfig.class) 를 추가했다.
  • @SpringBootApplication(scanBasePackages = "hello.datasource")
    • 예제에서는 @Import 로 설정 정보를 계속 변경할 예정이므로, 설정 정보를 바꾸면서 사용하기 위해 hello.config 의 위치를 피해서 컴포넌트 스캔 위치를 설정했다.

실행 결과

url=local.db.com
username=local_user
password=local_pw
maxConnection=1
timeout=PT3.5S
options=[CACHE, ADMIN]

이 방식의 단점은 Environment 를 직접 주입받고, env.getProperty(key) 를 통해서 값을 꺼내는 과정을 반복해야 한다는 점이다. 스프링은 @Value 를 통해서 외부 설정값을 주입 받는 더욱 편리한 기능을 제공한다.

1. 외부설정 사용 - @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 myDataSource() {
        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:2}") 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 는 필드에 사용할 수도 있고, 파라미터에 사용할 수도 있다.

기본값

만약 키를 찾지 못할 경우 코드에서 기본값을 사용하려면 다음과 같이 : 뒤에 기본값을 적어주면 된다.

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

ExternalReadApplication - 수정

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=CASHE,ADMIN

실행 결과

url=local.db.com
username=local_user
password=local_pw
maxConnection=1
timeout=PT3.5S
options=[CACHE, ADMIN]

2. 외부설정 사용 - @ConfigurationProperties 시작

Type-safe Configuration Properties

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

  1. 객체를 사용하면 타입을 사용할 수 있다. 따라서 실수로 잘못된 타입이 들어오는 문제도 방지할 수 있다.
  2. 객체를 통해서 활용할 수 있는 부분들이 많아진다. 쉽게 이야기해서 외부 설정을 자바 코드로 관리할 수 있는 것이다.

MyDataSourcePropertiesV1

package hello.datasource;

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;

    @Data
    public static class Etc{
        private int maxConnection;
        private Duration timeout;
        private List<String> options = new ArrayList<>();
    }
}
  • @ConfigurationProperties 이 있으면 외부 설정을 주입 받는 객체라는 뜻이다. 여기에 외부 설정 KEY 의 묶음 시작점인 my.datasource 를 적어준다.
  • 기본 주입 방식은 자바빈 프로퍼티 방식이다. Getter , Setter 가 필요하다. (롬복의 @Data 에 의해 자동 생성된다.)

MyDataSourceConfigV1

package hello.config;

import hello.datasource.MyDataSource;
import hello.datasource.MyDataSourcePropertiesV1;
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 myDataSource(){
        return new MyDataSource(
                properties.getUrl(),
                properties.getUsername(),
                properties.getPassword(),
                properties.getEtc().getMaxConnection(),
                properties.getEtc().getTimeout(),
                properties.getEtc().getOptions()
        );
    }
}
  • @EnableConfigurationProperties(MyDataSourcePropertiesV1.class) : 스프링에게 사용할 @ConfigurationProperties 를 지정해주어야 한다. 이렇게 하면 해당 클래스는 스프링 빈으로 등록되고, 필요한 곳에서 주입 받아서 사용할 수 있다.

ExternalReadApplication - 수정

package hello;

import hello.config.MyDataSourceConfigV1;
import hello.config.MyDataSourceEnvConfig;
import hello.config.MyDataSourceValueConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Import;

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

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

실행 결과

url=local.db.com
username=local_user
password=local_pw
maxConnection=1
timeout=PT3.5S
options=[CACHE, ADMIN]
  • type-safe : ConfigurationProperties 를 사용하면 타입 안전한 설정 속성을 사용할 수 있다. maxConnection=abc 가 들어온다면 int 가 아니므로 에러가 발생한다.

@ConfigurationPropertiesScan

@ConfigurationProperties 를 하나하나 직접 등록할 때는 @EnableConfigurationProperties 를 사용한다.

@ConfigurationProperties 를 특정 범위로 자동 등록할 때는 @ConfigurationPropertiesScan 을 사용하면 된다. 이 때 Config 파일의 @EnableConfigurationProperties 는 빼면 된다.

@ConfigurationPropertiesScan 을 사용한 main 메서드

package hello;

import hello.config.MyDataSourceConfigV1;
import hello.config.MyDataSourceEnvConfig;
import hello.config.MyDataSourceValueConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Import;

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

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

@ConfigurationPropertiesScan 을 사용한 MyDataSourceConfigV1

package hello.config;

import hello.datasource.MyDataSource;
import hello.datasource.MyDataSourcePropertiesV1;
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 myDataSource(){
        return new MyDataSource(
                properties.getUrl(),
                properties.getUsername(),
                properties.getPassword(),
                properties.getEtc().getMaxConnection(),
                properties.getEtc().getTimeout(),
                properties.getEtc().getOptions()
        );
    }
}
  • @EnableConfigurationProperties(MyDataSourcePropertiesV1.class) 은 사용할 필요없다.

문제

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

3. 외부설정 사용 - @ConfigurationProperties 생성자

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

MyDataSourcePropertiesV2

package hello.datasource;

import lombok.Data;
import lombok.Generated;
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.ArrayList;
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 = new ArrayList<>();

        public Etc(int maxConnection, Duration timeout, @DefaultValue("Default") List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }
}
  • 내부 클래스도 생성자를 만들어줘야한다.
  • @DefaultValue : 해당 값을 찾을 수 없는 경우 기본값을 사용한다.
  • @DefaultValue Etc etc : etc 를 찾을 수 없을 경우 Etc 객체를 생성하고 내부에 들어가는 값은 비워둔다. ( null , 0 )
  • @DefaultValue("DEFAULT") List options : options 를 찾을 수 없을 경우 DEFAULT 라는 이름의 값을 사용한다.

참고 @ConstructorBinding

스프링 부트 3.0 이전에는 생성자 바인딩 시에 @ConstructorBinding 애노테이션을 필수로 사용해야 했다.

스프링 부트 3.0 부터는 생성자가 하나일 때는 생략할 수 있다. 생성자가 둘 이상인 경우에는 사용할 생성자에 @ConstructorBinding 애노테이션을 적용하면 된다.

MyDataSourceConfigV2

package hello.config;

import hello.datasource.MyDataSource;
import hello.datasource.MyDataSourcePropertiesV1;
import hello.datasource.MyDataSourcePropertiesV2;
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 myDataSource(){
        return new MyDataSource(
                properties.getUrl(),
                properties.getUsername(),
                properties.getPassword(),
                properties.getEtc().getMaxConnection(),
                properties.getEtc().getTimeout(),
                properties.getEtc().getOptions()
        );
    }
}
  • MyDataSourcePropertiesV2 를 적용하고 빈을 등록한다. 기존 코드와 크게 다르지 않다.

ExternalReadApplication - 수정

package hello;

import hello.config.MyDataSourceConfigV1;
import hello.config.MyDataSourceConfigV2;
import hello.config.MyDataSourceEnvConfig;
import hello.config.MyDataSourceValueConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Import;

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

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

실행 결과

url=local.db.com
username=local_user
password=local_pw
maxConnection=1
timeout=PT3.5S
options=[CACHE, ADMIN]

* Setter 가 없으므로 개발자가 중간에 실수로 값을 변경하는 문제가 발생하지 않는다.

문제

타입과 객체를 통해서 숫자에 문자가 들어오는 것 같은 기본적인 타입 문제들은 해결이 되었다. 그런데 타입은 맞는데 숫자의 범위가 기대하는 것과 다르면 어떻게 될까? 예를 들어서 max-conneciton 의 값을 0 으로 설정하면 커넥션이 하나도 만들어지지 않는 심각한 문제가 발생한다고 가정해보자. max-conneciton 은 최소 1 이상으로 설정하지 않으면 애플리케이션 로딩 시점에 예외를 발생시켜서 빠르게 문제를 인지할 수 있도록 해야 한다.

4. 외부설정 사용 - @ConfigurationProperties 검증

@ConfigurationProperties 은 자바 객체이기 때문에 스프링이 자바 빈 검증기를 사용할 수 있도록 지원한다

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation' //추가

MyDataSourcePropertiesV3

package hello.datasource;

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.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;
import java.time.Duration;
import java.util.ArrayList;
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 = new ArrayList<>();

        public Etc(int maxConnection, Duration timeout, @DefaultValue("Default") List<String> options) {
            this.maxConnection = maxConnection;
            this.timeout = timeout;
            this.options = options;
        }
    }
}
  • @NotEmpty, @Min, @Max, @DurationMin 등으로 검증한다.

MyDataSourceConfigV3

package hello.config;

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

@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV3.class)
public class MyDataSourceConfigV3 {

    private final MyDataSourcePropertiesV3 properties;

    public MyDataSourceConfigV3(MyDataSourcePropertiesV3 properties) {
        this.properties = properties;
    }

    @Bean
    public MyDataSource myDataSource(){
        return new MyDataSource(
                properties.getUrl(),
                properties.getUsername(),
                properties.getPassword(),
                properties.getEtc().getMaxConnection(),
                properties.getEtc().getTimeout(),
                properties.getEtc().getOptions()
        );
    }
}
  • MyDataSourceConfigV3 은 기존 코드와 크게 다르지 않다.

ExternalReadApplication - 수정

package hello;

import hello.config.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Import;

@SpringBootApplication(scanBasePackages = "hello.datasource")
@Import(MyDataSourceConfigV3.class)
@ConfigurationPropertiesScan
public class ExternalReadApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExternalReadApplication.class, args);
    }
}
  • @Import(MyDataSourceConfigV3.class) 를 추가한다.

실행

  • maxConnection=0 으로 설정한 예

  • Property: my.datasource.etc.maxConnection
     Value: "0"
     Origin: class path resource [application.properties] - 4:34
     Reason: 1 이상이어야 합니다
    

ConfigurationProperties 장점

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

5. @Profile

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

예를 들어서 결제 기능을 붙여야 하는데, 로컬 개발 환경에서는 실제 결제가 발생하면 문제가 되니 가짜 결제 기능이 있는 스프링 빈을 등록하고, 운영 환경에서는 실제 결제 기능을 제공하는 스프링 빈을 등록한다고 가정해보자.

PayClient (인터페이스)

package hello.pay;

public interface PayClient {
    void pay(int money);
}

LocalPayClient

package hello.pay;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LocalPayClient implements PayClient{

    @Override
    public void pay(int money) {
        log.info("LocalPayClient.pay: {}", money);
    }
}
  • 로컬 개발 환경에서는 실제 결제를 하지 않는다.

ProdPayClient

package hello.pay;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ProdPayClient implements PayClient{
    @Override
    public void pay(int money) {
        log.info("ProdPayClient.pay: {}", money);
    }
}
  • 운영 환경에서는 실제 결제를 시도한다.

OrderService

package hello.pay;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final PayClient payClient;

    public void order(int money){
        payClient.pay(money);
    }
}
  • 상황에 따라서 LocalPayClient 또는 ProdPayClient 를 주입받는다.

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 Bean Created");
        return new LocalPayClient();
    }

    @Bean
    @Profile("prod")
    public ProdPayClient prodPayClient(){
        log.info("ProdPayClient Bean Created");
        return new ProdPayClient();
    }
}

@Profile 애노테이션을 사용하면 해당 프로필이 활성화된 경우에만 빈을 등록한다.

  • default 프로필(기본값)이 활성화 되어 있으면 LocalPayClient 를 빈으로 등록한다.
  • prod 프로필이 활성화 되어 있으면 ProdPayClient 를 빈으로 등록한다.

RunOrder

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 RunOrder implements ApplicationRunner {

    private final OrderService orderService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        orderService.order(1000);
    }
}
  • ApplicationRunner 인터페이스를 사용하면 스프링은 빈 초기화가 모두 끝나고 애플리케이션 로딩이 완료되는 시점에 run(args) 메서드를 호출해준다.

ExternalReadApplication 변경

package hello;

import hello.config.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Import;

@SpringBootApplication(scanBasePackages = {"hello.datasource", "hello.pay"})
@Import(MyDataSourceConfigV3.class)
public class ExternalReadApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExternalReadApplication.class, args);
    }
}
  • 실행하기 전에 컴포넌트 스캔 부분에 hello.pay 패키지를 추가한다.

실행

프로필 없이 실행 (default 프로필 사용)

No active profile set, falling back to 1 default profile: "default"
LocalPayClient 빈 등록
...
로컬 결제 money=1000

prod 프로필 실행

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

The following 1 profile is active: "prod"
ProdPayClient 빈 등록
...
운영 결제 money=1000
  • prod 프로필이 사용되면 ProdPayClient 가 빈으로 등록되는 것을 확인할 수 있다.

@Profile의 정체

package org.springframework.context.annotation;
...
@Conditional(ProfileCondition.class)
public @interface Profile {
	String[] value();
}

@Profile 은 특정 조건에 따라서 해당 빈을 등록할지 말지 선택하는 @Conditional 을 사용한다. 코드를 보면 @Conditional(ProfileCondition.class) 를 확인할 수 있다.

댓글남기기