본문 바로가기

🧑🏻‍💻 Dev/SpringBoot

[Spring] application.yml 설정값 가져오기

application.properties로 사용하는 분들도 계시지만 방법은 동일하기 때문에 저는 application.yml 기준으로 혼자 복습하며 기록해보려고 합니다.

 

만약 A, B, C라는 클래스가 존재할 때, 이 3개의 클래스 내부에서 companyName이라는 값을 모두 같은 "Fast Company"라는 값을 사용한다고 가정을 해봅시다.

 

이때 만약 회사 이름이 변경되어서 "Slow Company"로 바뀌었다고 생각해 봅시다. 그럼 우리는 A, B, C 클래스로 모두 이동해서 "Fast Company"라는 값을 "Slow Company"로 모두 변경해줘야 합니다.

 

물론 클래스가 3개 밖에 없어서 간단하게 할 수 있지만, 만약 클래스가 1000개가 넘는다고 생각하면 어떨까요? 무려 1000번을 변경해줘야 합니다. 이때 이런 공통적인 값들을 properties이나 yml(yaml) 파일에서 설정 파일의 값만 바꿔주면 모든 클래스에 적용시켜 줄 수 있습니다.

 

이렇게 클래스에서 프로퍼티 값을 가져오는 방법에는 여러 가지가 존재하는데, 코드를 작성하며 한번 확인해 봅시다.

 

일단 먼저 application.yml에 우리가 사용할 설정 값을 입력해 둡니다.

# application.yml

company:
  name: "Fast Company"

 

 

1. @Value("${}")

제일 먼저 등장하는 것은 @Value 어노테이션입니다. SpEL(Spring Expression Langauge)로 내가 설정한 property 이름을 표현해 주면 해당 값을 가져올 수 있는 방법입니다.

 

A class에서는 @Value를 이용해서 application.yml의 company.name을 가져와볼 것입니다.

// A.java

@Component
public class A {

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

    public A() {

    }

    public void printCompanyName() {
        System.out.println("A class = " + companyName);
    }
}

클래스 A를 @Component를 이용해서 빈으로 등록합니다. 그리고 @Value를 이용해서 우리가 원하는 설정 값을 가져와서 companyName에 넣을 것이고, main 함수가 있는 클래스에서 printCompanyName()을 이용해서 값이 잘 입력되었는지 확인해 볼 것입니다.

 

main에서는 생성자 주입을 통해서 A, B, C 클래스 빈에 대해서 DI를 받을 것이고, 모든 생성자 주입이 끝난 후에 실행시켜 줄 것이기 때문에 @PostConstruct를 사용할 것입니다. 아래 코드에 해당 과정이 작성되어 있습니다.

// MainApplication.java

@SpringBootApplication
public class MainApplication {

    private final A a;
    private final B b;
    private final C c;
    
    public MainApplication(A a, B b, C c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

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

    @PostConstruct
    public void init() {
        a.printCompanyName();
        b.printCompanyName();
        c.printCompanyName();
    }

}

코드를 실행시켜 보면 콘솔에 찍히는 값은 다음과 같습니다. 나머지 로그들은 모두 제외했습니다. 간단하게 출력 값만 적어봤습니다. 

A class = Fast Company
B class = null
C class = null

결과를 보면 A class에서 Fast Company라는 값을 제대로 가져온 것을 확인할 수 있습니다. B와 C는 아직 아무 설정도 안 해줬기 때문에 null이 나오는 것입니다. 

 

A class를 조금 더 멋스럽게 변경해 보겠습니다. 필드에 주입하는 것이 아닌 우리는 생성자를 사용해서 필드값을 초기화하는 방법을 더 많이 사용하기 때문에 이렇게 변경해보겠습니다.

// A.java

@Component
public class A {

    private String companyName;

    public A(@Value("${company.name}") String companyName) {
        this.companyName = companyName;
    }

    public void printCompanyName() {
        System.out.println("A class = " + companyName);
    }
}

매우 간단하게 해냈습니다. 필드 값 위에 입력하는 것이 아닌 생성자의 매개변수에 @Value를 붙여서 넣어주면 됩니다. 조금 멋있어졌군요.

 

 

 

2. Enviroment Bean (ApplicationContext)

두 번째 방법은 Enviroment 빈을 사용해서 프로퍼티 값을 가져오는 방법입니다. Spring Bean으로 등록되어 있는 Enviroment Bean을 주입받아서 getProperty()를 이용하여 해당 프로퍼티 값을 가져올 수 있는 방법입니다. 

 

B class는 이 방법을 사용해서 프로퍼티 값을 한번 가져와보겠습니다.

// B.java

@Component
public class B {

    private String companyName;

    private final Environment environment; // 생성자 주입을 통해서 DI

    public B(Environment environment) {
        this.environment = environment;
        companyName = this.environment.getProperty("company.name");
    }

    public void printCompanyName() {
        System.out.println("B class = " + companyName);
    }
}

 

org.springframework.core.env에 있는 Enviroment를 생성자를 통해서 의존성 주입을 받아옵니다. 그런 다음 Enviroment의 getProperty() 메서드를 이용하여 원하는 프로퍼티 값을 가져와서 companyName에 초기화해 줬습니다.

 

다시 main 메서드를 돌려서 출력 값을 확인해 봅니다.

A class = Fast Company
B class = Fast Company
C class = null

B class에도 값이 잘 가져와진 것을 볼 수 있습니다. 위에서 작성한 Enviroment를 사용하는 코드는 직접 ApplicationContext를 가져와서 사용해 볼 수도 있습니다. 코드를 한번 리펙토링 해보겠습니다.

// B.java

@Component
public class B {

    private String companyName;

    private final ApplicationContext applicationContext;

    public B(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        companyName = this.applicationContext.getEnvironment().getProperty("company.name");
    }

    public void printCompanyName() {
        System.out.println("B class = " + companyName);
    }
}

main을 다시 재시작해보니 출력되는 값은 동일하게 성공했습니다. 하지만 뭔가 코드 가독성면에서는 별로 좋아 보이진 않는다는 생각이 드는 코드입니다.

 

 

 

3. Configuration Properties

다음 방법은 자바 클래스로 따로 분리해서 값을 가져올 수 있는 방법입니다. @Value를 사용했을 때와는 다른 점은 클래스로 값을 매핑해서 가져올 수 있기 때문에 Type Safe 합니다. 그리고 각 프로퍼티 값에 대한 Meta 데이터를 작성할 수 있습니다. 

 

사용하는 방법은 @ConfigurationProperties("") 애노테이션을 사용하여 작성할 수 있습니다. 해당 클래스를 Bean으로 등록해서 사용할 수 있게 하기 위해서는 @Configuration 애노테이션을 사용해야 합니다. 코드를 먼저 작성해 보겠습니다.

 

 

# @Configuration 애노테이션 사용

// CompanyProperties.java

@ConfigurationProperties("company")
@Configuration
public class CompanyProperties {

    private String name;

    public String getName() {
        return name;
    }

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

@ConfigurationProperties("company")를 이용하여 나는 application.yml에서 company라는 프로퍼티 값을 가져와서 매핑한다는 것을 의미한다.

 

@Configuration을 붙여주는 이유는 CompanyProperties 클래스를 스프링 빈으로서 등록하기 위함이다. 그래야 다른 곳에서 DI 받아서 사용할 수 있기 때문이다.

 

private String name에 company.name을 가져와서 값을 초기화시키는 것이다. 이때 우리는 setter를 정의해 주면 된다. getter는 외부에서 해당 값을 사용하기 위해서는 필요하다.

 

이제 스프링 빈으로 등록된 CompanyProperties를 주입받아서 C class에서 사용해 볼 것이다.

// C.java

@Component
public class C {

    private CompanyProperties companyProperties;

    public C(CompanyProperties companyProperties) {
        this.companyProperties = companyProperties;
    }

    public void printCompanyName() {
        System.out.println("C class = " + companyProperties.getName());
    }
}

생성자 주입을 통해서 CompanyProperties를 주입받습니다. 그런 후 getName()을 통해서 해당 프로퍼티 값을 받아서 printCompanyName()에 넣어줍니다. 이제 main을 재시작해서 출력값을 확인해 보겠습니다.

A class = Fast Company
B class = Fast Company
C class = Fast Company

C class도 적절하게 값을 받아오는 것을 확인할 수 있습니다.

 

위에서 작성한 코드를 조금 리펙토링 해보겠습니다.

 

CompanyProperties에서 @Configuration을 떼어보겠습니다. 어 그럼 Bean으로 등록되지 않아서...주입을 받아오지 못하지 않나?라는 의문이 들어야 합니다.

 

그래서 MainApplication로 가서 @ConfigurationPropertiesScan 애노테이션을 붙여줍니다. @ConfigurationProperties가 붙은 클래스를 자동으로 Scan 하여 빈으로 등록해 줍니다.

 

 

# @Configuration을 빼고, @ConfigurationPropertiesScan을 사용

// CompanyProperties.java

@ConfigurationProperties("company")
public class CompanyProperties {

    private String name;

    public String getName() {
        return name;
    }

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

@ConfigurationPropertiesScan
@SpringBootApplication
public class MainApplication {

    /* 코드 생략 */

}

이렇게 설정해 두고 다시 main을 재시작해보면 적절하게 잘 출력되는 것을 확인해 볼 수 있습니다.

 

하지만 여기서 조금 생각해 볼 것이 있습니다. CompanyProperties를 보면 setName()을 통해서 해당 값을 초기화합니다. 이렇게 되면 언제든 setter를 통해서 값을 변경할 수 있다는 것은 조금 위험한 내용인 것 같습니다. 필드를 final로 선언해서 생성자를 통해서 해당 값을 초기화할 수 있도록 코드를 리펙토링 해봅시다. 이때는 @ConstructorBinding 애노테이션을 사용합니다. 생성자를 통해 프로퍼티 값을 바인딩받을 수 있도록 해주는 애노테이션입니다.

 

 

# Setter를 빼고, @ConstructorBinding을 사용하여 생성자 바인딩

// CompanyProperties.java

@ConstructorBinding
@ConfigurationProperties("company")
public class CompanyProperties {

    private final String name;

    public CompanyProperties(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

main을 다시 재시작해봅니다. 그럼 값이 적절하게 출력되는 것을 볼 수 있습니다. 리펙토링 성공이네요. 이렇게 불변의 값으로 설정하고 싶다면 @ConstructorBinding을 사용해서 해당 값을 다루는 것이 좋습니다.

 

 

 

이제 코드를 완성했으니 우리가 생각했던 시나리오대로 "Fast Company"를 "Slow Company"로 바꿔서 모든 출력값이 정상적으로 변경되는지 확인해 봅시다.

# application.yml

company:
  name: "Slow Company"

콘솔 출력값

마지막은 캡처로 깔끔하게 확인해 봤습니다. 모든 출력값이 정상적으로 Slow Company로 변경되었습니다. 좋습니다~ 🙄

 

이번 정리 글에서는 많은 애노테이션이 새롭게 나와서 하나씩 정리해 보는 시간을 가지는 것이 좋을 거 같습니다. 프로젝트에서 이 내용을 써보게 되는 날이 왔으면 좋겠습니다.