[학습기록] Spring 애플리케이션 설정 관리: (1) @ConfigurationProperties를 사용하는 이유
Spring 애플리케이션에서는 application.properties 또는 application.yml(이하, “설정 파일”)의 값을 코드에 주입해서 사용하는 경우가 자주 있습니다. 설정의 개수가 많지 않거나, 구조화되어 있지 않다면 간단하게 @Value 어노테이션을 사용하면 됩니다.
하지만 설정의 개수가 많아지거나 계층 구조가 복잡해지기 시작하면 @Value를 계속 사용하는 것은 유지보수 측면에서 불리할 수 있습니다.
1. 애플리케이션의 설정
아래와 같은 정보들은 설정 파일에 정의하고, 애플리케이션 실행 시점에 코드에 주입해서 사용하는 것이 일반적입니다. 애플리케이션의 실행 환경이나 운영 정책 등이 변경되더라도 코드를 직접 수정하지 않고, 유연하게 대응하기 위해서입니다.
- 외부 연동
- API URL
- 타임아웃 값
- 연결 재시도 횟수
- 기능 동작
- 특정 기능의 활성화 여부
- 기능별 동작 옵션
- 환경 정보
- 개발/스테이징/운영 환경별 구동 정보
- 로그 레벨
- 임계값(threshold)
2. 설정 파일
설정 파일에는 애플리케이션의 실행 환경이나 운영 정책 등 어떻게 동작해야 하는지를 결정할 값들이 정의됩니다.
예시) application.properties
feature.logging.enabled=true
external.api.timeout=3000
예시) application.yml
feature:
logging:
enabled: true
external:
api:
timeout: 3000
두 파일은 표현 방식은 다르지만 동일한 설정 정보를 정의합니다. 프로젝트의 특성이나 팀의 선호에 따라서 한 가지 유형을 선택해서 사용하면 됩니다. 개인적으로는 계층 구조가 드러나는 application.yml 파일을 사용하는 것을 더 선호하는 편입니다.
3. 설정값을 코드에 주입하는 가장 간단한 방법, @Value
@Value은 매우 직관적입니다. 설정 파일에 정의된 값과 코드 내부에 선언된 변수를 직접 연결해 주기 때문입니다.
사용 방법은 다음과 같습니다.
@Value("${설정키:기본값}")
예를 들어 설정 파일에서 external.api.timeout의 값을 가져와 코드에 주입하려면 다음과 같이 작성할 수 있습니다.
@Value("${external.api.timeout:5000}")
private int timeout;
만약, 설정 파일에서 값을 찾을 수 없다면 기본값이 변수에 주입됩니다.
4. @Value의 한계
하지만 애플리케이션의 규모가 커지면 설정의 개수도 많아지고 계층 구조도 복잡해지게 됩니다. 이런 상황에서 @Value를 사용하면 설정 키가 여러 곳에 흩어지고, 설정 구조를 관리하기 어려워질 수 있습니다.
API로 외부 시스템과 연동하기 위해 url, timeout, retry를 설정해야 하는 상황을 가정해보도록 하겠습니다.
application.yml
external:
api:
url: https://example.com
timeout: 3000
retry: 3
@Value를 사용한다면 설정값을 하나씩 개별적으로 주입해야 합니다. 결국, 코드가 복잡해지면서 유지보수를 어렵게 만드는 요인으로 작용하게 됩니다.
@Component
public class ExternalApiClient {
@Value("${external.api.url}")
private String url;
@Value("${external.api.timeout:3000}")
private int timeout;
@Value("${external.api.retry:3}")
private int retry;
}
또한 동일한 설정값을 주입하는 코드가 여러 클래스에 중복될 수도 있습니다.
@Component
public class ExternalApiWebClient {
@Value("${external.api.url}")
private String url;
@Value("${external.api.timeout:3000}")
private int timeout;
@Value("${external.api.retry:3}")
private int retryCount;
}
@Component
public class ExternalApiBatchClient {
@Value("${external.api.url}")
private String url;
@Value("${external.api.timeout:3000}")
private int timeout;
@Value("${external.api.retry:3}")
private int retryCount;
}
이처럼 동일한 설정 키가 여러 클래스에 흩어져 있으면 키 이름이나 구조가 변경될 때 관련 코드를 모두 찾아 수정해야 합니다. 반면 관련 설정을 별도의 객체로 묶으면 설정 구조와 변경 지점을 하나의 클래스에 집중시킬 수 있습니다.
5. 설정을 객체로 관리하는 @ConfigurationProperties
결국, 이런 경우에는 관련 설정을 하나의 객체로 묶어 바인딩하는 방법이 효과적입니다. 스프링에서는 @ConfigurationProperties 어노테이션을 사용하면 설정을 객체 단위로 바인딩할 수 있습니다. 즉, 설정 파일의 구조가 그대로 코드 내부의 필드 구조로 표현되는 것입니다.
앞에서 사용했던 설정 파일의 내용을 다시 한번 살펴보겠습니다.
external:
api:
url: https://example.com
timeout: 3000
retry: 3
관련 설정을 하나의 객체로 바인딩하려면 @ConfigurationProperties를 선언한 클래스를 작성하면 됩니다.
@ConfigurationProperties(prefix = "external.api")
public class ExternalApiProperties {
private String url;
private int timeout;
private int retry;
// getter
// setter
}
prefix = "external.api"는 설정 파일의 external.api 하위 설정값을 ExternalApiProperties의 필드에 바인딩하겠다는 의미입니다. 이렇게 하면 url, timeout, retry가 개별 설정값으로 흩어지지 않고 하나의 의미 있는 설정 객체로 묶입니다. 따라서 기능별 설정 구조를 직관적으로 파악하고, 변경 지점을 한곳에서 관리할 수 있습니다.
다만 @ConfigurationProperties를 선언한 것만으로는 설정 객체가 자동으로 Spring Bean(이하, “Bean”)으로 등록되지 않습니다. 다른 클래스에서 설정 객체를 주입받아 사용하려면 별도의 Bean 등록 과정이 필요합니다. 아래 코드는 ExternalApiProperties가 Bean으로 등록되었다는 것을 전제로 합니다.
@RequiredArgsConstructor
@Service
public class ExternalApiClient {
private final ExternalApiProperties properties;
public void call() {
System.out.println(properties.getUrl());
}
}
설정도 애플리케이션의 중요한 구성 요소입니다. 따라서 단순히 값을 주입하는 데 그치지 않고, 관련 설정을 어떤 단위로 묶을지와 향후 변경 가능성까지 고려하여 설계하는 것이 좋습니다.
6. 다음 포스팅
이번 포스팅에서는 @ConfigurationProperties를 이용해 설정을 객체로 관리하는 방법을 살펴봤습니다. 다음 포스팅에서는 설정 객체를 Bean으로 등록하고, @ConditionalOnProperty를 이용해 설정값에 따라 기능을 활성화하는 방법을 알아보겠습니다.