Property Binding

스프링부트 프로젝트를 생성하면 외부 프로퍼티 설정파일을 만들게 된다.(application.yml, application.properties ...) 여기 정의한 키-값들이 결국 어플리케이션이 기동되면서 클래스로 바인딩 되는데 이 과정에 대해 알아본다.

 

먼저 클래스에서 외부 프로퍼티 값들을 가져오는 방식을 살펴보자. 아래 3가지를 이용할 수 있다.

  • @Value : 소스코드에서 바로 프로퍼티에 주입받아 사용
  • Environment : 외부설정이 바인딩 된 Environment 빈을 주입받아 사용
  • @ConfigurationProperties : 외부설정이 바인딩 될 Bean(객체)을 생성하여 사용

가장 빈번하게 사용하는 방식은 @Value("${property}") 방식일 듯 싶지만 불러올 프로퍼티의 갯수가 많거나, 계층형의 구조를 가지고 있는 경우 등에는 적절하지 않다. 또한 프로퍼티명이나 타입을 직접 입력해야 하므로 오류에 대한 여지도 있다. @Value와 Environment 빈에 대한 예시는 아래와 같다.

application.properties에 다음과 같이 프로퍼티 키-값 정의 : yangs.name=brand

@Component
public class SimpleRunner implements ApplicationRunner {
    @Autowired
    Environment environment; // Environment 타입의 빈 주입

    @Value("${yangs.name}") // ${}내부에 작성된 프로퍼티키에 해당하는 값을 세팅
    String name1; 

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("Value 애노테이션의 yangs.name : " + name1);

        String name2 = environment.getProperty("yangs.name");
        System.out.println("Environment bean의 yangs.name : " + name2);
    }
}

// 아래는 콘솔 출력결과
Value 애노테이션의 yangs.name : brand

Environment bean yangs.name : brand


간단한 몇 개의 값이라면 @Value를 사용하여 바인딩할 수 있지만, 검증이나 Type-safe, 계층형 구조 등을 처리할 수 있는 @ConfigurationProperties를 이용한 방법이 있다,

application.yml의 예시와 이 값을 @ConfigurationProperties를 어떻게 사용하여 바인딩 하는지 예제로 살펴본다.


application.yml 예시

일반적으로 프로퍼티명은 아래 is-main-tester처럼 소문자와 대쉬(-)를 구분자로 하는 명칭이 권장된다.
프로퍼티명 앞의 대쉬(-)는 리스트형을 나타낼때 사용한다.

test:
  name: tester
  cnt: 99
  is-main-tester: true
  sub-testers:
    - name: sub-tester1
       cnt: 11
       is-main-tester: false
    - name: sub-tester2
       cnt: 22
       is-main-tester: false


@ConfigurationProperties(prefix = “test”) 로 프로퍼티클래스에 값 바인딩하기

프로퍼티가 맵핑될 클래스(아래의 TesterProperties)를 작성하고 @ConfigurationProperties을 마킹한다. 이 클래스는 프로퍼티값이 맵핑될 클래스라는 의미로 애노테이션에는 프로퍼티명의 prefix(아래의 prefix = "test")를 지정할 수 있다.
프로퍼티 파일에서 리스트형으로 작성한 sub-testers를 맵핑할 수 있도록 List<Subster> subTesters 로 선언한 것을 볼 수 있고,
작성된 위의 프로퍼티 파일은 kebab case(- 가 구분자)로 작성하였으나 클래스 변수명은 camel case이다.
스프링부트에서는 프로퍼티명의 camel case, kebab case, underscore notation 간의 변환을 모두 지원한다.
subTesters - sub-testers - sub_testers 모두 변환하여 바인딩이 가능하다.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

// 읽어들인 프로퍼티 중 해당 prefix의 프로퍼티값을 바인딩한다.
@ConfigurationProperties(prefix = "test")
public class TesterProperties {
    private String name;
    private int cnt;
    private boolean isMainTester;
    private List<SubTester> subTesters = new ArrayList<>();

    // Getter, Setter, ToString 생략
    
    static class SubTester {
        private String name;
        private int cnt;
        private boolean isMainTester;
            
        // Getter, Setter, ToString 생략
    }
}

@EnableConfigurationProperties(TesterProperties.class)로 프로퍼티클래스 사용하기

바인딩이 완료된 TesterProperties를 빈으로 등록하여 사용하기 위해 @EnableConfigurationProperties를 마킹한다.
주로 프로퍼티 값을 사용할 @Configuration 파일에 마킹하며, @ConfigurationProperties가 마킹된 클래스를 빈으로 등록해주는 역할이다. 만일 프로퍼티 클래스가 다수라면 @ConfigurationPropertiesScan({"base.package1", "base.package2"}) 으로 여러 패키지를 스캔하여 등록할 수도 있다.
프로퍼티가 제대로 바인딩 됬는지 테스트를 위해 ApplicationRunner를 작성 후 콘솔로그를 찍어본다.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@EnableConfigurationProperties(TesterProperties.class)
public class PropertyRunner implements ApplicationRunner {
    private final TesterProperties testerProperties; // 값이 바인딩된 빈이 의존성 주입된다.

    public PropertyRunner(TesterProperties testerProperties) {
        this.testerProperties = testerProperties;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 바인딩이 제대로 되었는지 콘솔 출력
        System.out.println("=================================");
        System.out.println("TesterProperties : " + testerProperties);
        System.out.println("=================================");
    }
}

=================================
TesterProperties : TesterProperties{name='tester', cnt=99, isMainTester=true, 
subTesters=[SubTester{name='sub-tester1', cnt=11, isMainTester=false}, 
                    SubTester{name='sub-tester2', cnt=22, isMainTester=false}]}
=================================

String, int, boolean 및 List 형까지 바인딩이 제대로 된 것을 확인할 수 있다. 혹여 바인딩시 Validation을 하고 싶다면 아래와 같이 스프링의 @Validated 와 JSR-303에 정의된 javax.validation 애노테이션을 사용할 수 있다.

@ConfigurationProperties(prefix = "test")
@Validated // 검증대상임을 표시
public class TesterProperties {
    @NotNull
    private String name;
    @Max(90)
    private int cnt;
    private boolean isMainTester;
    @Valid // 내부 프로퍼티에 검증이 적용되려면 @Valid를 마킹한다.
    private List<SubTester> subTesters = new ArrayList<>();

    static class SubTester {
        @NotNull
        private String name;
        @Min(20)
        private int cnt;
        private boolean isMainTester;
    }
    
    // Getter, Setter, ToString 생략
}

요약하면

  • 외부에서 설정된 프로퍼티 파일을 코드에서 사용하는 방법은 여러가지가 있다.
  • 만일 읽어올 프로퍼티가 많거나 계층형이라면 객체에 바인딩하여 사용하는 것을 고려하자.
  • 프로퍼티값을 객체에 바인딩하여 빈으로 등록 후 사용을 위해서는 @ConfigurationProperties(prefix = "test"), @EnableConfigurationProperties, @ConfigurationPropertiesScan({"base.package1", "base.package2"}) 같은 애노테이션을 조합한다.
  • 프로퍼티 바인딩시 검증을 위해서는 @Validated와 javax.validation 애노테이션을 사용한다.

SpringApplication이 시작한 시점에 특정 코드를 실행하고자 한다면, ApplicationRunner나 CommandLineRunner를 활용할 수 있다.
두 개의 Runner 인터페이스 중 하나를 선택하여 implements하여 메서드를 오버라이드하고, 빈으로 등록해주면 사전 준비는 완료된다.


ApplicationRunner

run(ApplicationArguments args) 메서드 하나만 오버라이드하면 되며 파라미터인 ApplicationArguments에 바인딩된 어플리케이션 아규먼트들에 접근이 가능하다.
ApplicationArguments는 여러가지 편리한 메서드를 제공하고 있다. 그러므로 아래에서 설명할 CommandLineRunner보다 사용이 권장된다. 아래는 커맨드로 Option, Non-Option Argument를 설정 후 기동 했을 때의 테스트 코드이다.

어플리케이션 기동 커맨드 : java -jar MySpringBootApp.jar alone1 alone2 --key1=value1 --key2=value2

  @Component
  public class SimpleApplicationRunner implements ApplicationRunner {
      @Override
      public void run(ApplicationArguments args) {
          System.out.println("======================================================");
          System.out.println("ApplicationRunner - ApplicationArguments ");
          System.out.println("NonOption Arguments : " + args.getNonOptionArgs());
          System.out.println("Option Arguments Names : " + args.getOptionNames());
          System.out.println("key1의 value : " + args.getOptionValues("key1"));
          System.out.println("key2의 value : " + args.getOptionValues("key2"));
          System.out.println("======================================================");
      }
  }
======================================================
ApplicationRunner - ApplicationArguments
NonOption Arguments : [alone1, alone2]
Option Arguments Names : [key1, key2]
key1의 value : [value1]
key2의 value : [value2]
======================================================

CommandLineRunner

ApplicationRunner와 기능은 같지만 전달되는 아규먼트가 더 불친절하다. 그리고 Option과 Non-Option 아규먼트의 구분도 없고 아규먼트도 단순 배열로 전달된다.
위와 동일한 커맨드로 기동 했을 때의 코드와 로그이다. 

어플리케이션 기동 커맨드 : java -jar MySpringBootApp.jar alone1 alone2 --key1=value1 --key2=value2

  @Component
  public class SimpleCommandLineRunner implements CommandLineRunner {
      @Override
      public void run(String... args) throws Exception {
          System.out.println("======================================================");
          System.out.println("CommandLineRunner - String... ");
          for(String s : args){
              System.out.println("argument : " + s);
          }
          System.out.println("======================================================");
      }
  }
======================================================
CommandLineRunner - String...
argument : alone1
argument : alone2
argument : --key1=value1
argument : --key2=value2
======================================================

 

SpringApplication은 main() 메서드에서 시작되는 스프링 어플리케이션의 초기 설정을 제어할 수 있는 클래스이다.
일반적으로는 main() 메서드 안에서 어플리케이션의 실행을 SpringApplication.run() 메서드에게 위임하거나, SpringApplication 객체를 만들어 run() 메서드를 실행할 수 있다.

public static void main(String[] args) {
    SpringApplication.run(MySpringConfiguration.class, args); // static 메서드로 기동
}
public static void main(String[] args) {
    SpringApplication app = new SpringApplication(App.class); // 객체를 생성하여 기동
    app.run(args);
}

 

SpringApplication이 초기화 되면서 어플리케이션이 구동될 때 발생하는 이벤트나 에러시 동작하는 FailureAnalyzer 등에 대해 먼저 알아본다. 


FailureAnalyzers

어플리케이션 실행시 오류가 발생한 경우에 동작하며, 에러에 대한 내용과 에러를 고치기 위한 Action을 출력한다.
이미 많은 FailureAnalyzers가 등록되어 있고, 추가도 가능하다.

***************************
APPLICATION FAILED TO START
***************************

Description:
Embedded servlet container failed to start. Port 8080 was already in use.

Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

Application Events and Listeners

스프링에서 제공하는 ContextRefreshedEvent 외에도 스프링부트의 SpringApplication은 추가적인 어플리케이션 이벤트들을 보낸다.
어플리케이션 기동이 시작되면 각 단계에 따라서 ApplicationStartingEvent를 시작으로 이벤트들이 발생하고, 발생된 이벤트들은 이벤트리스너에 전달된다. 각 단계별로 어떤 이벤트가 발생하는지는 아래 스프링부트 공식문서를 참고하자.

https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners

 

이벤트리스너가 빈으로 등록되어 있고, 리스너의 표적 이벤트가 발생하면 자동으로 처리메서드가 실행된다.
아래 코드와 로그는 빈으로 등록된 ApplicationStartedEvent리스너가 어플리케이션 기동시 출력된 로그이다.
SampleListener 클래스가 @Component 애노테이션으로 빈등록 되었으며, 기동시 ApplicationStartedEvent 이벤트가 발생하면서 자동으로 onApplicationEvent() 메서드가 호출된다.

@Component // 빈으로 등록한다.
public class SampleListener implements ApplicationListener<ApplicationStartedEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
        System.out.println("ApplicationStartedEvent가 발생!");
    }
}

어플리케이션 기동시 마지막에 콘솔로그가 출력된다. (ApplicationStartedEvent가 기동시 제일 마지막에 발생하기 때문이다.)

2019-12-26 16:09:49.823 INFO 4212 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-12-26 16:09:49.828 INFO 4212 --- [ main] com.yang.wind.App : Started App in 2.1 seconds (JVM running for 3.235)
ApplicationStartedEvent가 발생!

하지만 ApplicationStartingEvent의 경우 ApplicationContext가 초기화 되기 전에 발생하는 이벤트기 때문에, 빈 등록으로는 리스너가 동작하지 않는다.(ApplicationContext에 빈이 등록되는데, 이 과정보다 먼저 이벤트가 발생하므로 이벤트 발생시점에는 리스너가 있는지도 모르는 상태이다.) 이런 경우에는 SpringApplication addListeners() 메서드로 리스너를 추가하면 된다.
아래 코드는 SpringApplication.run()으로 바로 시작하지 않고, 객체 생성을 통해 설정을 추가했다.

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(App.class);
        app.addListeners(new SampleListener()); // 리스너를 코드로 추가한다.
        app.run(args);
    }
}
// 이미 SpringApplication에 리스너를 등록했으므로 빈등록이 필요없다.
public class SampleListener implements ApplicationListener<ApplicationStartingEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartingEvent applicationStartingEvent) {
        System.out.println("ApplicationStartingEvent 발생!");
    }
}

어플리케이션 기동시 처음에 콘솔로그가 출력된다. 로그를 복붙하다보니 좀 깨지지만 그건 중요한게 아니다.

ApplicationStartingEvent 발생!

  .   ____          _            __ _ _

 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )

  '  |____| .__|_| |_|_| |_\__, | / / / /

 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::        (v2.2.0.RELEASE)

....


WebApplicationType

SpringApplication에서는 ApplicationContext의 타입이 내부적으로 확정된다.
3개의 타입이 존재하는데 다음과 같이 자동설정되며 SpringApplication의 setWebApplicationType() 메서드를 이용하여 코드에서 특정타입으로 오버라이드가 가능하다.

  • WebApplicationType.SERVLET : Spring MVC가 존재하면 설정됨
  • WebApplicationType.REACTIVE : Spring MVC가 없고, Spring WebFlux가 존재하면 설정됨
  • WebApplicationType.NONE : 위의 경우에 해당이 안되면 설정됨. 어플리케이션이 기동 되었다가 바로 종료된다.
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(App.class);
        // ApplicationType 설정을 오버라이드 한다.
        app.setWebApplicationType(WebApplicationType.SERVLET);
        app.run(args);
    }
}

+ Recent posts