Developer.

3. Spring Boot Custom Property

어플리케이션 정보를 properties 에서 key-value 로 지정할 수 있는 property 는 두 종류로 나눌 수 있다:

  • built-in property: Spring Boot 에서 자동으로 제공하는 property
  • custom property: 사용자가 임의로 정하는 property, 필요한 만큼 사용이 가능

이전까지는 built-in property 들을 보았지만, 이제부터는 custom property 들을 본다.

📂 목차


📚 본문

앞서서 Environment 인스턴스에 바인딩 되는 프로퍼티들을 출력해보았는데 Environment 인스턴스를 자동주입(@Autowired)하고 프로퍼티 값들을 읽고 사용할 수 있기 때문에 어디서든 Environment 를 주입 받아서 프로퍼티를 읽어올 수 있다.

여기서 Environment 는 런타임에 한해서만 존재되는 인스턴스, 빈이다. 이렇듯 properties 를 파일로 선언하여 Env 로 가져오면 편리하지만 몇 가지 단점이 있다:

  • 프로퍼티 값의 타입 안전성(type-safety)이 보장되지 않음(URL인지 아닌지, 이메일 주소 인지 아닌지) -> 런타임 에러가 발생 우려
  • 프로퍼티 값을 일정 단위로 묶어서 읽을 수 없고, @Value 애너테이션이나 스프링의 Environment 인스턴스를 사용해서 하나하나 개별적으로만 읽을 수 있음

이를 방지하기 위해 커스텀 프로퍼티를 사용해 type-safetyvaliation, 그리고 여러 개를 한꺼번에 가져올 수 있게 해야 한다.

이에 대한 방법은 여러가지 이다.


@ConfigurationProperties 를 사용하여 커스텀 프로퍼티 정의

@ConfigurationProperties 애너테이션을 사용하면 특정 prefix 에 대한 프로퍼티 정보를 담는 클래스를 만들 수 있고, 이를 통해 값을 보장하고 유효성을 검증한다. @Value 애너테이션을 사용하거나 Environment 빈을 자동 주입 받지 않아도 되는 방법이다.

우선 custom property 로 쓸 것들을 application.properties 에 넣어주자

app.sbip.ct.name=StudyApplication
app.sbip.ct.ip=127.0.0.1
app.sbip.ct.port=9090
app.sbip.ct.security.enabled=true
app.sbip.ct.security.token=abc123
app.sbip.ct.security.roles=USER,ADMIN

위 property 는 전부 app.sbip.ct 의 prefix를 가진다. CustomProperties.class 하나를 생성하여 다음을 입력하자. 여기서는 Getter, Setter 등등의 보일러플레이트 코드(boilerplate code)의 불편성을 위해 Lombok 을 사용했다.

import lombok.*;
import org.springframework.boot.context.properties.*;

import java.util.*;

@AllArgsConstructor
@Getter
@ToString
@ConfigurationProperties(prefix="app.sbip.ct")
public class CustomProperties {
    private final String name;
    private final String ip;
    private final String port;
    private final Security security;

    @AllArgsConstructor
    @Getter
    @ToString
    public static class Security {
        private final boolean enabled;
        private final String token;
        private final List<String> roles;
    }
}

위처럼 app.sbip.ct 인 프로퍼티만 읽을 수 있고, 더 세부적으로 .으로 나뉘었다면, 그 안에 public static class를 선언하여 할 수도 있다(public static 으로 nested class 를 선언하는 이유)

지금은 Lombok 을 써서 인자들을 가져오지만 원한다면 생성자에서 검증하는 코드를 넣을 수도 있다.

@EnableConfigurationProperties 로 Bean 등록하기

기본적으로 ConfigurationProperties 에는 Bean 으로 등록하는 메커니즘이 없다.
다음 CustomService 클래스를 만들어 Bean이 자동 주입되는지 살펴보자.

import com.example.study.properties.*;
import lombok.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;

@Service
@Getter
public
class CustomService {
    private final CustomProperties customProperties;

    @Autowired
    public CustomService(CustomProperties customProperties) {
        this.customProperties = customProperties;
    }
}

이제 아래와 같이 작성한다.

@EnableConfigurationProperties(CustomProperties.class)
public class DemoApplication {
    ...
    public static void main(String[] args)  {
        ...
        CustomService customService = applicationContext.getBean(CustomService.class);
		System.out.println(customService.getCustomProperties().toString());
    }
}

SpringApplication 을 밑과 같이 작성하여 실행해보고 실행하면

public class DemoApplication {
    ...
    public static void main(String[] args)  {
        SpringApplication springApplication = new SpringApplication(DemoApplication.class);
        ConfigurableApplicationContext applicationContext = springApplication.run(args);
        CustomService customService = applicationContext.getBean(CustomService.class);
		System.out.println(customService.getCustomProperties().toString());
    }
}

Consider defining a bean of type 'com.example.study.properties.CustomProperties' in your configuration.

와 같은 메시지를 볼 수 있다.

@ConfigurationProperties 자체에 아래와 같이 interface 가 구현되고 있다.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {

이는 그냥 프로퍼티를 만들고 어플리케이션에 적용을 안한 것과 같다. 선언한 CustomProperties를 적용하고 싶은 Application의 클래스에 @EnableConfigurationProperties 애너테이션을 사용하여 Bean 으로 등록할 수 있다.

@EnableConfigurationProperties(CustomProperties.class)
public class DemoApplication {
    ...
    public static void main(String[] args)  {
        ...
        CustomService customService = applicationContext.getBean(CustomService.class);
		System.out.println("\n\n\ngetCustomProperties");
		System.out.println(customService.getCustomProperties().toString());
		System.out.println("\n\n\n");
    }
}

이제 Bean 이 제대로 불러와짐을 알 수 있고, 출력 또한 잘 되는 것을 볼 수 있다.


🔗 출처


📁 관련 글


✒️ 용어

보일러플레이트 코드

항상 비슷한 형태로 여기저기 반복적으로 작성해야 하는 코드이며 기능 구현과는 큰 관련이 없지만, 기능이나 구현에 있어서 자주 사용하여 필수적으로 작성해야하는 코드를 말한다. 예시로는 생성자, Getter, Setter 등이 있다.

Lombok

보일러플레이트 코드를 줄일 수 있도록 애너테이션을 지원해주는 패키지이다. 의존성 추가를 통해 사용가능하다.

dependencies {
    compileOnly 'org.projectlombok:lombok:(최신 버전)'
    annotationProcessor 'org.projectlombok:lombok(최신 버전)'
}

따로 Lombok 을 정리한 글이다.