“Ansible이 설치되지 않은 환경에서 앱이 바로 죽는다.”
프로젝트를 진행하던 중 AnsibleProperties라는 설정 Bean이 여러 서비스에 주입되어 있었다.
문제는 Ansible이 설치되어 있지 않거나 설정 값이 비어 있을 경우, Spring이 애플리케이션을 띄우는 도중 컨텍스트 로딩 에러를 발생시키며 바로 종료된다는 점이었다.
Spring 프로젝트를 하다 보면 .properties, .yml, .env 등의 설정 파일에 환경별 민감 정보나 외부 연결 값을 저장하는 경우가 많다.
이러한 설정 주입은 배포 환경이나 운영 환경이 달라질 때 필수적이지만, 특정 환경에서 값이 누락되면 예상치 못한 런타임 오류로 이어질 수 있다.
특히 MSA 구조에서는 하나의 설정 주입이 실패하면 그 설정을 의존하는 모든 서비스가 연쇄적으로 장애를 일으킬 수도 있다.
이 문제는 설정 파일에만 국한되지 않는다. 예를 들어,
이런 상황에서도 동일한 문제가 반복된다.
결국 “내가 필요한 기능은 일부일 뿐인데, 하나의 Bean이 전체 서비스를 멈추게 된다.”
이것이 바로 환경 의존적인 Bean 주입 구조의 대표적인 함정이다.
Spring은 @Component, @Service, @ConfigurationProperties 등 애노테이션이 붙은 클래스라면
조건과 상관없이 무조건 Bean으로 등록한다.
그런데 이 과정에서 바인딩할 설정 값이 없거나, 초기화 시 외부 시스템에 접근하는 코드가 있다면?
➡️ 컨텍스트 초기화 단계에서 예외 발생 → 앱 전체 기동 실패
이는 “Fail-fast” 구조로 빠른 에러 감지는 가능하지만,
모듈성이 중요한 시스템에서는 “필요 없는 환경에서까지 Bean을 강제 등록”해버리는 문제가 된다.
“필요할 때만 Bean을 등록하자.”
Spring은 @Conditional 애노테이션을 통해 Bean 등록 여부를 제어할 수 있다.
직접 Condition 인터페이스를 구현하면 된다.
public class ApiKeyCondition implements Condition {
@Override
public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) {
String key = ctx.getEnvironment().getProperty("external.api.key");
return key != null && !key.isBlank();
}
}
@Bean
@Conditional(ApiKeyCondition.class)
public ExternalApiClient realClient() {
return new RealExternalApiClient();
}
이제 external.api.key가 존재할 때만 Bean이 등록된다.
없다면 Bean 자체가 생성되지 않는다.
“Real이 없을 땐 안전한 No-Op로 대체하자.”
@Configuration
public class ApiClientConfig {
@Bean
@Conditional(ApiKeyCondition.class)
public ExternalApiClient realClient() {
return new RealExternalApiClient();
}
@Bean
@ConditionalOnMissingBean(ExternalApiClient.class)
@Primary
public ExternalApiClient noOpClient() {
return () -> log.info("API Key 미설정 → 요청 스킵");
}
}
주입받는 쪽에서는 전혀 신경 쓸 필요가 없다.
@Service
@RequiredArgsConstructor
public class ReportService {
private final ExternalApiClient apiClient;
public void sendReport() {
apiClient.send(); // Real 또는 No-Op 중 하나가 자동 실행
}
}
환경에 따라 실제 호출이든, 아무 동작도 안 하든,
애플리케이션은 절대 멈추지 않는다.
실무에서는 Fallback을 다양한 복원 전략으로 확장할 수 있다.
| 유형 | 설명 | 예시 |
|---|---|---|
| 🧩 No-Op | 아무 동작도 안 함, Null-safe | Ansible 미설치 시 스킵 |
| ⏱️ Timeout | 일정 시간 초과 시 기본 응답 반환 | Resilience4j, Hystrix |
| 💾 Cache / Graceful Degrade | 외부 시스템 불가 시 캐시 데이터 반환 | Redis → InMemory |
| 🧪 Stub / Mock | 개발 환경에서 실제 호출 대신 모의 응답 | Dev용 Mock API |
| 🔁 Hybrid | No-Op → Cache → Queue 순서로 점진적 복구 | 장애 복원형 설계 |
Fallback의 핵심은 “기능 축소는 허용하되, 시스템 중단은 금지”다.
Fail-fast보다 Fail-safe한 시스템을 지향해야 한다.
Spring의 @Conditional은 “언제 Bean을 등록할지”를 결정하고,
Fallback 패턴은 “등록되지 않았을 때 어떻게 대응할지”를 책임진다.
Ansible처럼 환경 의존성이 강한 시스템은 물론,
외부 API, 클라우드, 캐시, 메시징 등 다양한 영역에서도
이 두 가지를 조합하면 죽지 않는 애플리케이션을 만들 수 있다.
장애는 피할 수 없지만, 죽지 않는 구조는 설계할 수 있다.