
프로젝트에서 Redis의 Key 만료 이벤트를 구독하기 위해 Spring Data Redis의 KeyExpirationEventMessageListener를 사용했습니다.
@Component
class RedisExpiredConsumer(
private val listenerContainer: RedisMessageListenerContainer,
// ... (기타 의존성 주입)
) : KeyExpirationEventMessageListener(listenerContainer) { // ⬅️ 이 클래스를 상속받음
override fun doHandleMessage(message: Message) {
// (키 만료 시 실행될 로직... e.g., 슬랙 알림)
}
}
로컬 환경에서 기능이 동작했지만 AWS ElastiCache(Redis)를 사용하자마자 애플리케이션 실행에 실패하며 다음과 같은 에러가 발생했습니다.
org.springframework.data.redis.RedisCommandExecutionException:
ERR unknown command 'CONFIG', with args beginning with: 'GET' 'notify-keyspace-events'
에러 메시지를 보니 Redis 서버가 CONFIG라는 명령어를 알지 못한다는 것입니다.
CONFIG는 Redis의 표준 명령어인데 unknown command 에러가 뜨는 것이 이상해서 찾아보니 AWS ElastiCache 같은 관리형 서비스는 보안 정책상 서버 설정을 동적으로 변경/조회하는 CONFIG 계열의 명령어를 허용하지 않는다고 한다.(AWS 공식 문서)
CONFIG 명령어는 어디에서 호출되었는가?제 코드 어디에도 CONFIG 명령어를 직접 호출하는 부분은 없었습니다. 원인을 찾기 위해 부모 클래스인 KeyExpirationEventMessageListener와, 그 부모인 KeyspaceEventMessageListener의 내부 코드를 확인했습니다.
범인은 KeyspaceEventMessageListener의 init() 메서드였습니다.
init()은 왜 실행되는가? (SmartLifecycle)KeyspaceEventMessageListener는 Spring의 SmartLifecycle 인터페이스를 구현합니다. Spring Boot는 애플리케이션이 시작될 때(Bean 등록 완료 후), SmartLifecycle을 구현한 Bean들의 start() 또는 init() 메서드를 자동으로 호출합니다.
즉, 제가 RedisExpiredConsumer를 @Component로 등록하자마자, Spring이 부모의 init() 메서드를 자동으로 실행시킨 것입니다.
init()은 무엇을 하는가?다음은 KeyspaceEventMessageListener.java의 문제 코드입니다.
public abstract class KeyspaceEventMessageListener implements MessageListener, SmartLifecycle {
// ...
@Override
public void init() { // ⬅️ 앱 시작 시 자동으로 호출됨
if (this.checkNotifyKeyspaceEvents) { // (1) 이 값이 true(기본값)이면
// (2) 'CONFIG GET'으로 Redis 설정을 "확인"하려 함
String config = this.getNotifyKeyspaceEvents(this.connection);
if (!StringUtils.hasText(config) || !config.contains("E")) {
// (3) 'E' 설정이 없으면 'CONFIG SET'으로 "수정"하려 함
this.connection.setConfig("notify-keyspace-events", config + "E");
}
}
// (4) 실제 리스너를 등록하는 핵심 로직
super.doRegister(this.container);
}
private String getNotifyKeyspaceEvents(RedisConnection connection) {
// 🚨 문제의 코드!
return connection.getConfig("notify-keyspace-events");
}
// ...
}
Spring Data Redis는 개발자의 편의를 위해, 리스너가 시작될 때 CONFIG GET 명령어로 Redis 서버의 notify-keyspace-events 설정값을 미리 확인합니다. 그리고 만약 'E'(Expired) 설정이 빠져있으면 'E'를 추가해 CONFIG SET까지 자동으로 실행해주는 로직이었습니다.
이 편리한 자동 설정 기능이 ElastiCache의 보안 정책과 충돌하여 에러를 일으킨 것입니다.
init() 오버라이딩으로 CONFIG 호출 우회하기저는 이미 ElastiCache의 notify-keyspace-events 설정을 AWS 파라미터 그룹을 통해 Ex로 설정해둔 상태였습니다. 따라서 Spring이 CONFIG 명령어로 이 설정을 '확인'하거나 '수정'해 줄 필요가 없었습니다.
필요한 것은 오직 부모 클래스 init() 메서드 후반부에 있는 super.doRegister(this.container)라는 '실제 리스너 등록' 로직뿐이었습니다.
따라서 RedisExpiredConsumer에서 init() 메서드를 오버라이드(Override)하여, 문제가 되는 CONFIG 확인 로직은 건너뛰고 필요한 리스너 등록 로직만 직접 실행하도록 수정했습니다.
RedisExpiredConsumer.kt (수정된 코드)
@Component
class RedisExpiredConsumer(
private val listenerContainer: RedisMessageListenerContainer,
// ... (기타 의존성 주입)
) : KeyExpirationEventMessageListener(listenerContainer) {
/**
* ElastiCache의 'ERR unknown command 'CONFIG'' 에러 해결.
*
* 부모 클래스(KeyspaceEventMessageListener)의 init()은
* 앱 시작 시 'CONFIG GET' 명령으로 Redis 설정을 '확인'하려 시도함.
* ElastiCache는 이 명령을 허용하지 않으므로, 이 '확인' 로직을 건너뛰고
* 리스너 등록에 필수적인 'doRegister'만 실행하도록 오버라이드함.
*/
override fun init() {
// super.init() (X) <- CONFIG 확인 로직이 포함된 부모의 init 호출 안 함
// 📌 부모의 init()에 있는 'CONFIG' 확인 로직을 건너뛰고,
// 📌 리스너 등록에 필수적인 doRegister()만 직접 호출합니다.
super.doRegister(listenerContainer)
}
override fun doHandleMessage(message: Message) {
// (키 만료 이벤트 처리 로직...)
}
}