[문제 상황]
Spring 애플리케이션에서 특정 환경에서만 빈(Bean)을 등록하도록 @Conditional을 활용했는데, 내부적으로 static boolean 값을 참조하는 Condition 구현체가 있었다.
하지만, 이 boolean 값이 @PostConstruct로 초기화되기 때문에 빈이 생성되는 시점에서 값이 초기화되지 않아 애플리케이션이 정상적으로 뜨지 않는 문제가 발생했다.
1. 문제의 코드를 살펴보자 (예시)
@Configuration
public class AppConfig {
@Bean
@Conditional(MyCondition.class) // 특정 조건을 만족할 때만 빈 등록
public MyService myService() {
return new MyService();
}
}
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return MyStaticConfig.isEnabled; // static boolean 값 참조
}
}
@Component
public class MyStaticConfig {
public static boolean isEnabled;
@PostConstruct
public void init() {
isEnabled = true; // 빈 생성 이후 초기화
}
}
우선 구글링을 해서 문제 발생 원인을 살펴보았다.
- @Conditional(MyCondition.class)는 빈을 생성하기 전에 실행됨
- 그런데, MyCondition에서 참조하는 MyStaticConfig.isEnabled 값은 @PostConstruct로 초기화됨 → 이 시점엔 값이 false이므로 조건이 만족되지 않아 빈이 등록되지 않음
- 결과적으로 MyService가 등록되지 않아 애플리케이션 실행 실패
2. 해결 방법 (수정된 코드)
이 문제를 해결하기 위해 @ConditionalOnExpression을 사용하여 스프링 환경 변수로 조건을 판별하도록 변경
@Configuration
public class AppConfig {
@Bean
@ConditionalOnExpression("${feature.enabled:true}") // SpEL을 사용한 조건 평가
public MyService myService() {
return new MyService();
}
}
# application.properties
feature.enabled=true
해결 방법은 다음과 같다.
- @ConditionalOnExpression("${feature.enabled:true}")을 사용하여 환경 변수로 조건을 판별
- 실행 시점에 @PostConstruct와 관계없이 값을 결정할 수 있음
- 빈 생성 타이밍 문제를 해결하고 애플리케이션이 정상적으로 실행됨
3. 더 살펴보기
구글링으로 문제는 해결을 했지만, 코드만 본다면 이런 의문이 생겼다.
@Conditional 로는 빈 등록 시 myService가 생성되는데, @ConditionalOnExpression은 언제 조건을 평가해서 객체를 생성할까? @PostConstruct 이후인가?
그래서 정리해보았다.
3-1. @Conditional vs @ConditionalOnExpression의 빈 생성 시점 차이
1. @Conditional(MyCondition.class)
- 빈이 등록되기 전에 Condition.matches() 메서드를 실행해서 조건을 확인
- 조건을 만족하지 않으면 아예 빈을 등록하지 않음
- 즉, 빈이 생성되기 전에 실행됨
2. @ConditionalOnExpression("${feature.enabled:true}")
- 스프링 컨텍스트가 프로퍼티 값을 로딩한 이후에 평가됨
- 즉, application.properties 또는 환경 변수에서 값을 읽어와 조건을 판별
- @PostConstruct보다는 이전에 실행됨 → 하지만 @Conditional(MyCondition.class)보다는 나중에 실행됨
더 이해하기 위해 다음과 같은 질문으로 이어갔다.
그러면 @PostConstruct에서 enabled = true로 판별하는게 아니라 @ConditionalOnExpression에서 작성한 feature.enabled:true 이걸 판별하는거네. feature.enabled는 어디에 작성할 수 있을까?
3-2. feature.enabled가 설정될 수 있는 위치
1. application.properties 또는 application.yml 파일에서 설정
# application.properties
feature.enabled=true
# application.yml
feature:
enabled: true
이렇게 설정하면 @ConditionalOnExpression("${feature.enabled:true}")가 이 값을 참조
2. JVM 환경 변수로 설정 (프로그램 실행 시 설정 가능)
# bash
-Dfeature.enabled=true
Spring Boot는 JVM 옵션으로 설정된 값을 자동으로 읽어옴
3. 운영 환경에서 시스템 환경 변수로 설정
# bash
export FEATURE_ENABLED=true
이 경우, @ConditionalOnExpression이 System.getenv("FEATURE_ENABLED") 값을 읽어 판단
@PostConstruct에서 enabled = true 로 설정하는 것이 아니라, feature.enabled 값이 이미 설정된 상태에서 @ConditionalOnExpression이 이를 읽어 빈을 등록할지 판별한다.
즉, 빈이 등록될 때 feature.enabled 값이 이미 존재해야 한다.
만약 설정이 없다면 기본값 true를 사용 ("${feature.enabled:true}")
이어서 드는 의문
그러면 저 feature.enabled는 코드에서 수정이 가능한가?
3-3. feature.enabled 값은 코드에서 수정할 수 있는가?
feature.enabled는 Spring의 환경 변수로 설정되므로, 일반적인 코드에서 직접 수정할 수는 없다.
하지만, Spring의 Environment 객체를 사용하면 런타임에서 값을 변경할 수 있다.
1. Spring 코드에서 feature.enabled 값을 읽는 방법
코드에서 설정 값을 가져올 때는 @Value 또는 Environment 객체를 사용함.
- @Value를 사용하는 방법
@Value("${feature.enabled:true}")
private boolean featureEnabled;
이렇게 하면 application.properties에 설정된 값을 자동으로 주입받을 수 있음.
- Environment 객체를 사용하는 방법
@Autowired
private Environment environment;
public void checkFeatureEnabled() {
boolean isEnabled = Boolean.parseBoolean(environment.getProperty("feature.enabled", "true"));
System.out.println("Feature Enabled: " + isEnabled);
}
하지만 이 방식으로는 값만 읽을 수 있고, 수정은 불가능하다.
2. 코드에서 feature.enabled 값을 수정하는 방법
Spring에서는 환경 변수를 동적으로 변경하는 것이 원칙적으로 어렵지만, ConfigurableEnvironment와 MutablePropertySources를 사용하면 가능하다.
- feature.enabled 값 변경하기
@Autowired
private ConfigurableEnvironment environment;
public void updateFeatureFlag(boolean newValue) {
MutablePropertySources propertySources = environment.getPropertySources();
Map<String, Object> map = new HashMap<>();
map.put("feature.enabled", newValue);
propertySources.addFirst(new MapPropertySource("dynamicFeatureToggle", map));
System.out.println("Feature flag updated to: " + newValue);
}
이렇게 하면 코드에서 feature.enabled 값을 실행 중에 동적으로 변경이 가능하다.
3. 하지만 @ConditionalOnExpression은 변경된 값을 즉시 반영하지 않는다.
코드에서 feature.enabled 값을 변경해도, 이미 등록된 빈에는 즉시 반영되지 않는다.
왜?
@ConditionalOnExpression은 애플리케이션이 시작될 때 한 번 평가된다.
- 해결 방법
1. 애플리케이션을 재시작해야 변경 사항이 반영된다.
2. Spring Cloud Config, Refresh Scope 같은 기능을 활용하면 런타임에서 변경이 가능하다.
- @RefreshScope를 사용하면 동적으로 값을 반영할 수 있다.
- @RefreshScope 활용 (Spring Cloud 필요)
@RefreshScope
@Component
public class FeatureConfig {
@Value("${feature.enabled:true}")
private boolean featureEnabled;
public boolean isFeatureEnabled() {
return featureEnabled;
}
}
@RefreshScope를 사용하면 Spring Actuator의 /actuator/refresh API를 호출하여 변경된 값을 반영이 가능하다.
4. 결론
[3-1]
- @Conditional(MyCondition.class)는 빈 등록 전 실행되므로, 참조하는 값이 초기화되지 않았을 경우 문제 발생
- @ConditionalOnExpression("${feature.enabled:true}")은 환경 변수가 로드된 후 평가되므로 보다 안정적으로 동작
- @PostConstruct는 빈이 생성된 이후 실행되므로, 그 전에 실행되는 @Conditional과 충돌할 수 있음
>> 즉, @ConditionalOnExpression은 @PostConstruct 실행 이전에 조건을 평가하지만, @Conditional보다는 나중에 실행
[3-2]
- @PostConstruct에서 enabled 값을 설정하는 것이 아니라, feature.enabled를 설정한 후 @ConditionalOnExpression에서 이를 참조하여 빈을 등록하는 방식으로 변경된 것이다.
[3-3]
- feature.enabled는 코드에서 기본적으로 수정할 수 없음.
- 하지만 ConfigurableEnvironment와 MutablePropertySources를 사용하면 동적으로 변경 가능.
- 그러나 @ConditionalOnExpression은 애플리케이션 시작 시 한 번만 평가하므로, 변경 사항이 바로 반영되지 않음.
- 즉시 반영하려면 @RefreshScope 같은 추가적인 Spring Cloud 기능이 필요
'백엔드 개발 > Spring&JPA' 카테고리의 다른 글
[리팩토링] HTTP Client 및 RestTemplate을 OpenFeign으로 전환하기 + Resilience4j 서킷 브레이커 패턴 적용 (0) | 2025.04.06 |
---|---|
[트러블슈팅] PostgreSQL에서 한국어 정렬(ORDER BY) 문제 해결 방법 - Collation (0) | 2025.02.04 |
[Spring Data JPA] 영속성 컨텍스트 (PersistenceContext) (0) | 2024.08.30 |
[Spring Data JPA] p6spy 커스텀 포맷, 로그 파일, 로그 레벨 설정 (0) | 2024.08.29 |
[Spring Data JPA] Spring boot 3에 p6spy 적용하기 (4) | 2024.08.28 |