외부설정파일이란

애플리케이션에서 사용하는 설정 값들을 애플리케이션 외부나 내부에 설정할 수 있는 파일로 대표적인 설정파일은 application.properties가 있다.

@Component
public class SampleListener implements ApplicationRunner {

    @Value("${jch.name}")
    private String name;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("-------------");
        System.out.println(name);
        System.out.println("-------------");
    }
}

외부설정파일의 값은 @Value를 이용해서 접근할 수 있다.

우선순위

  1. 유저 홈 디렉토리에 있는 spring-boot-dev-tools.properties
  2. 테스트에 있는 @TestPropertySource
  3. @SpringBootTest 애노테이션의 properties 애트리뷰트
  4. 커맨드 라인 아규먼트
  5. SPRING_APPLICATION_JSON (환경 변수 또는 시스템 프로티) 에 들어있는 프로퍼티
  6. ServletConfig 파라미터
  7. ServletContext 파라미터
  8. java:comp/env JNDI 애트리뷰트
  9. System.getProperties() 자바 시스템 프로퍼티
  10. OS 환경 변수
  11. RandomValuePropertySource
  12. JAR 밖에 있는 특정 프로파일용 application properties
  13. JAR 안에 있는 특정 프로파일용 application properties
  14. JAR 밖에 있는 application properties
  15. JAR 안에 있는 application properties
  16. @PropertySource
  17. 기본 프로퍼티 (SpringApplication.setDefaultProperties)
# main/resource/application.properties
jch.name = jcheolho
# test/resource/application.properties
jch.name = test

테스트용 프로퍼티를 별도로 정의하기 위해서는 test/resource 폴더에 외부설정파일을 생성한다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {

    @Autowired
    Environment environment;

    @Test
    public void contextLoads() {
        assertThat(environment.getProperty("jch.name"))
                .isEqualTo("test");
    }
}

테스트코드에서는 자동설정된 스프링 Environment 빈을 사용하여 프로퍼티에 접근할 수 있다. 앱이 실행될 때 main의 프로퍼티파일을 먼저 읽어오고 test의 프로퍼티파일로 덮어씌우기 때문에 jch.name 프로퍼티는 test가 된다.

# main/resource/application.properties
jch.name = jcheolho
jch.age = ${random.int}
# test/resource/application.properties
jch.name = test

실습을 위한 application.properties를 작성한다.

참고로, 포트에서는 랜덤값을 사용할 때 0을 지정해야한다. ${random.int}는 말 그대로 랜덤값을 의미하므로 가용 가능한 포트인지를 고려하지 않는다.

@Component
public class SampleListener implements ApplicationRunner {

    @Value("${jch.name}")
    private String name;

    @Value("${jch.age}")
    private int age;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("-------------");
        System.out.println(name);
        System.out.println(age);
        System.out.println("-------------");
    }
}

애플리케이션에서 @Value를 이용해 프로퍼티를 주입하고 출력한다. 위 코드는 애플리케이션에서는 정상적으로 작동하지만 테스트코드를 실행하면 이야기가 달라진다.

# main/resource/application.properties
jch.name = jcheolho
jch.age = ${random.int}
# test/resource/application.properties
jch.name = test
jch.age = ${random.int}

테스트코드에서도 해당 러너가 실행되는데, 테스트 리소스에 있는 프로퍼티에는 jch.age를 입력하지 않아서 오류가 발생한다. 이는 앞서 설명한 덮어씌워지는 현상때문인데, 이를 피하기 위해서는 테스트 리소스에도 jch.age를 추가해야한다.

# main/resource/application.properties
jch.name = jcheolho
jch.age = ${random.int}
jch.fullName = ${jch.name} Jung

이번엔 jch.fullName을 추가해보자. 이미 정의된 프로퍼티${} 플레이스홀더를 이용하여 재사용할 수 있다.

@Component
public class SampleListener implements ApplicationRunner {

    @Value("${jch.fullName}")
    private String name;

    @Value("${jch.age}")
    private int age;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("-------------");
        System.out.println(name);
        System.out.println(age);
        System.out.println("-------------");
    }
}

마찬가지로 앱을 실행할 땐 문제가 없다. 하지만 테스트 리소스에는 jch.fullName 프로퍼티가 없기 때문에 다시 에러가 발생한다. 간단하게 에러를 없애는 방법테스트 리소스에서 덮어씌워지는 해당 파일을 삭제해버리는 것이다. 덮어씌우지 않기 때문에 SampleListener에서 에러가 발생하지 앟는다.

@RunWith(SpringRunner.class)
@SpringBootTest(properties = { "jch.name=testjch", "jch.age=20" })
public class ApplicationTest {

    @Autowired
    Environment environment;

    @Test
    public void contextLoads() {
        assertThat(environment.getProperty("jch.name"))
                .isEqualTo("testjch");
    }
}

필요하다면 테스트코드에서는 우선순위가 높은 방식으로 프로퍼티를 재정의하면 된다. 만약 관리해야할 프로퍼티가 많다면 별도의 파일로 관리하는 것이 좋다.

# test.properties
jch.name=testjch
jch.age=20
@RunWith(SpringRunner.class)
@TestPropertySource("classpath:/test.properties")
@SpringBootTest
public class ApplicationTest {

    @Autowired
    Environment environment;

    @Test
    public void contextLoads() {
        assertThat(environment.getProperty("jch.name"))
                .isEqualTo("testjch");
    }
}

@TestPropertySource에 해당 파일을 지정해주면 된다. @TestPropertySource가 application.properties보다 우선순위가 더 높기때문에 해당 프로퍼티를 사용한다.

application.properties 우선 순위

application.properties라는 이름의 파일이 여러 폴더에 존재하는 경우. 프로퍼티 우선 순위는 다음과 같다.

  1. file:./config/
  2. file:./
  3. classpath:/config/
  4. classpath:/

높은 properties 파일의 프로퍼티 값으로 낮은 properties 파일의 프로퍼티 값을 덮어씌운다. 높은 properties 파일의 프로퍼티가 존재하지 않는 경우는 덮어씌우지 않는다.

외부설정 Bean 등록

properties의 키-값을 Bean으로 등록하여 타입-세이프하게 만들 수 있다.

# main/resource/application.properties
jch.name = jcheolho
# 범위를 지정할 수 있다
jch.age = ${random.int(0,100)}
jch.fullName = ${jch.name} Jung
@ConfigurationProperties("jch")
public class JchProperties {

    private String name;

    private int age;

    private String fullName;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }
}

@ConfigurationProperties에 key로 사용할 값을 지정하면된다.

참고로, properties 파일의 자동완성은 메타정보를 기반으로 이루어진다. spring-boot-configuration-processor가 프로젝트를 빌드할 때 이러한 메타정보를 생성해준다. IDE에서 application.properties 파일의 자동완성을 지원한다면, 해당 플러그인을 추가하여 @ConfigurationProperties가 붙어있는 클래스를 이용하여 메타정보를 생성해서 자동완성을 사용할 수 있다.

@EnableConfigurationProperties(JchProperties.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);

        app.run(args);
    }
}

ConfigurationProperties를 처리하는 모듈에서 값을 바인딩할 수 있게 처리한 상태이므로 클래스를 Bean으로 등록해야 한다. 애플리케이션 클래스에서 @EnableConfigurationProperties에 생성한 properties 클래스 목록을 지정해줘야 한다.

스프링부트에서 해당 어노테이션은 자동설정 되어있다. 따라서 @Component를 이용하여 빈으로 등록하기만 하면 된다.

@Component
@ConfigurationProperties("jch")
public class JchProperties {

    private String name;

    private int age;

    private String fullName;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }
}

@Component
public class SampleListener implements ApplicationRunner {

    @Autowired
    JchProperties jchProperties;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("-------------");
        System.out.println(jchProperties.getName());
        System.out.println(jchProperties.getAge());
        System.out.println("-------------");
    }
}

서드파티 설정파일

@SpringBootApplication
public class Application {

    @ConfigurationProperties("server")
    @Bean
    public ServerProperties serverProperties() {
        return new ServerProperties();
    }

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);

        app.run(args);
    }
}

흔하지 않은 경우지만, 서버에 대한 설정이 외부 jar와 같은 서드파티 모듈에 있는 경우 @ConfigurationProperties를 이용하여 빈으로 등록할 수 있다.

융통성있는 바인딩

# main/resource/application.properties
jch.name = jcheolho
# 범위를 지정할 수 있다
jch.age = ${random.int(0,100)}
jch.full_name = ${jch.name} Jung

full_name처럼 camel 케이스로 적지 않아도 바인딩을 해주는 기능융통성있는 바인딩이라고 한다.

융통성있는 타입 컨버전

age는 빈에서 int 타입이지만 설정파일은 값을 모두 문자열로 취급한다. 그러나 애플리케이션을 실행해보면 타입 에러가 나지 않는 것을 확인할 수 있다. 이는 스프링 프레임워크에서 제공하는 컨버전 서비스를 통해서 타입 컨버전이 수행되기 때문에 가능하다. 스프링부트만의 독특한 타입컨버전이 있다.

// application.properties
jch.sessionTimeout = 25 // jch.sessionTimeout = 25s
    
// Bean
@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout = Duration.ofSeconds(30);

@DurationUnit를 이용하여 초단위의 ChronoUnit을 지정하면 Duration으로 타입컨버전이 된다. value에 25s처럼 suffix를 적어주면 @DurationUnit을 지정하지 않아도 컨버전된다.

유효성 검증

spring-boot-starter-web에 포함된 hibernate-validator의 @Validate를 이용하여 빈으로 등록된 설정파일의 유효성 검사를 수행할 수 있다.

// application.properteis
jch.name =

// Bean
@Component
@ConfigurationProperties("jch")
@Validated
public class JchProperties {

    @NotEmpty
    private String name;
    ....
}

@NotEmpty로 지정된 key의 값을 빈 문자열로 전달하면 '반드시 값이 존재하고 길이 혹은 크기가 0보다 커야 합니다.' 와 같은 로그가 출력된다.

해당 포스팅은 스프링 부트 개념과 활용 강의 내용을 토대로 작성하였습니다.