
나는 프로젝트에서 MongoDB를 보조 데이터 저장소로 사용하는 구조를 가지고 있다. 하지만 MongoDB의 역할은 언제까지나 검색을 빠르게 하는데 그 중점이 있기 때문에, MongoDB에 문제가 있더라도 MySQL을 통한 조회를 자동으로 수행할 수 있어야 한다.
처음에는 “MongoDB 조회 결과가 비어 있으면 MySQL로 조회한다”는 식의 단순 fallback 구조를 사용했다.
하지만 곧 문제가 생겼다.
MongoDB 자체에 접근할 수 없는 상황(예: 로컬에서 Mongo 미구동, 서버 연결 실패 등)이 발생하면
아예 MongoSocketOpenException이 터지면서 프로그램이 애초에 실행조차 되지 않았다.
즉,“결과가 없을 때 MySQL로 fallback”은 되었지만, “MongoDB 연결 자체가 불가능할 때 fallback”은 되지 않았다.
이건 진정한 의미의 fallback이 아니었다.
내가 원하는 건 다음 두 가지였다.
1. 로컬 환경에서는 MongoDB 설정이 없어도 시스템이 정상적으로 돌아가야 한다.
2. 운영 환경에서도 MongoDB 연결 실패 시, 장애 없이 MySQL 로직만으로 서비스가 유지되어야 한다.
먼저 MongoDB와 MySQL의 연결 유무에 따라 Repository를 교체할 수 있도록 설계했다.
기본 인터페이스를 정의하고 (ArtistMongoQueryRepository) 이를 두 가지 구현체로 나누었다:
실제 MongoDB용 구현체 → ArtistMongoQueryRepositoryImpl
Mongo 미구동 시 대체용 → NoOpArtistMongoQueryRepository
로컬 개발 환경에서는 Mongo를 실행하지 않기 때문에, @Profile("local") 설정으로 NoOp 버전이 자동으로 등록되도록 했다.
이 단계에서 1번 목표(로컬 개발 환경 호환성)는 해결됐다.
하지만 여전히 운영 환경에서 Mongo 연결 실패 시 예외가 발생했다.
Spring이 MongoTemplate을 초기화하려다 연결 타임아웃으로 죽는 것이다.
운영 환경에서도 연결 실패를 감지하고 자동으로 NoOp로 대체하기 위해 Mongo 설정 클래스를 직접 만들었다.
핵심은 isMongoAvailable() 메서드다.
MongoDB에 ping 명령을 보내서 연결 여부를 확인해, 성공시 실제 MongoDB 구현체 버전의 빈을 등록하고, 실패시 NoOp 버전의 빈을 등록한다.
@Slf4j
@Configuration
public class MongoConfig {
private boolean isMongoAvailable(MongoTemplate mongoTemplate) {
try {
mongoTemplate.getDb().runCommand(new org.bson.Document("ping", 1));
log.info("MongoDB 연결 성공");
return true;
} catch (Exception e) {
log.warn("MongoDB 연결 실패 — NoOp 리포지토리로 대체됩니다. 이유: {}", e.getMessage());
return false;
}
}
@Bean
public SongMongoCommandRepository songMongoCommandRepository(MongoTemplate mongoTemplate,
SongMongoDelegateRepository delegate) {
if (isMongoAvailable(mongoTemplate)) {
return new SongMongoCommandRepositoryImpl(delegate);
}
return new NoOpSongMongoCommandRepository();
}
@Bean
public SongMongoQueryRepository songMongoQueryRepository(MongoTemplate mongoTemplate) {
if (isMongoAvailable(mongoTemplate)) {
return new SongMongoQueryRepositoryImpl(mongoTemplate);
}
return new NoOpSongMongoQueryRepository();
}
@Bean
public ArtistMongoCommandRepository artistMongoCommandRepository(MongoTemplate mongoTemplate,
AritstMongoDelegateRepository delegate) {
if (isMongoAvailable(mongoTemplate)) {
return new ArtistMongoCommandRepositoryImpl(delegate);
}
return new NoOpArtistMongoCommandRepository();
}
@Bean
public ArtistMongoQueryRepository artistMongoQueryRepository(MongoTemplate mongoTemplate) {
if (isMongoAvailable(mongoTemplate)) {
return new ArtistMongoQueryRepositoryImpl(mongoTemplate);
}
return new NoOpArtistMongoQueryRepository();
}
}
이 방식을 적용하기 위해서는 @Repository와 @Profile 애노테이션을 구현체에서 제거해야 한다.
그 이유는 다음과 같다.
@Repository가 붙어 있으면 Spring이 자동으로 빈을 등록하므로, 수동으로 등록하려는 @Configuration 설정과 충돌할 수 있다.
@Profile도 제거해야 MongoConfig에서 직접 환경에 따라 수동 제어가 가능하다.
즉, 모든 Mongo 관련 빈 등록은 MongoConfig에서만 담당하도록 역할을 명확히 분리했다.
MongoDB 연결 상태에 따라 올바른 빈이 등록되어 동작하는 것을 확인했다.

MongoDB 연결 정보가 부재할 경우, ping 명령이 실패하고 Spring은 Noop(빈 동작) 구현체를 주입하여 애플리케이션을 정상적으로 가동한다. Mongo 관련 로직은 단순히 무시되고, 자동으로 MySQL을 통한 조회를 수행하게 된다.

MongoDB 연결이 성공하면, Spring이 자동으로 실제 MongoTemplate 기반 구현체를 주입하여 Mongo 관련 로직이 정상적으로 동작한다.
fallback은 시스템 의존성 자체가 실패했을 때도 견고하게 동작하는 구조여야 한다. 이렇게 스프링의 DI 구조를 활용하면 외부 시스템에 대한 fallback 아키텍처를 구현할 수 있다. 이 방식은 Mongo뿐 아니라 Redis, Kafka, ElasticSearch 등 외부 의존성이 있는 모든 모듈에 같은 패턴으로 적용 가능하다.