[DEPLOY] production-release(v.0.3.3)#377
Conversation
…kPopulate 내 isReady 설정
…nchronizationManager.afterCommit 접근 적용
…te() 완료 시 SET geo:pins:ready 1을 다른 인스턴스에 전달하며, 삭제도 함께 함
[MERGE] 통합 배포 PR
There was a problem hiding this comment.
Code Review
This pull request introduces a Redis GEO caching mechanism for map pins, including initializer and scheduler components to manage cache population and nightly rebuilds. It also integrates cache-aside logic for bounding box queries and clustering, adds an admin endpoint to monitor cache status, and updates the user solver repository query. The reviewer feedback highlights three key areas for improvement: addressing a startup race condition in the initializer using a Redis lock, optimizing performance by reducing frequent ZCARD calls in isGeoSetReady(), and preventing duplicate scheduler runs due to clock drift by letting the rebuild lock expire naturally instead of deleting it immediately.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| public class PinGeoRedisInitializer { | ||
|
|
||
| private final PinLocationRepository pinLocationRepository; | ||
| private final PinGeoRedisService pinGeoRedisService; | ||
|
|
||
| @Async | ||
| @Order(1) | ||
| @EventListener(ApplicationReadyEvent.class) | ||
| public void initialize() { | ||
| try { | ||
| // geo:pins:ready 키가 존재하면 다른 인스턴스가 이미 캐시를 적재한 상태입니다. | ||
| // 롤링 배포 등 순차 기동 환경에서 불필요한 DB 조회와 Redis 중복 적재를 방지합니다. | ||
| if (pinGeoRedisService.isGeoSetReady()) { | ||
| log.info("[Redis GEO] 캐시가 이미 초기화되어 있어 초기화를 건너뜁니다."); | ||
| return; | ||
| } | ||
| log.info("[Redis GEO] 캐시 초기화 시작 ..."); | ||
| List<MapPinView> views = pinLocationRepository.findAllActivePins(); | ||
| pinGeoRedisService.bulkPopulate(views); | ||
| log.info("[Redis GEO] 캐시 초기화 완료: {}개 핀 적재", views.size()); | ||
| } catch (Exception e) { | ||
| log.warn("[Redis GEO] 캐시 초기화 실패 (서버는 정상 기동, DB 폴백 사용): {}", e.getMessage()); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
여러 인스턴스가 동시에 기동되는 환경(예: 무중단 배포 중 다중 컨테이너 기동)에서 isGeoSetReady() 체크와 bulkPopulate() 실행 사이에 레이스 컨디션(Race Condition)이 발생할 수 있습니다. 두 인스턴스가 거의 동시에 isGeoSetReady()를 false로 판단하면, 둘 다 bulkPopulate()를 호출하게 되고 내부적으로 clearGeoKeys()가 실행되어 캐시가 비워지거나 데이터가 유실되는 문제가 발생할 수 있습니다.\n\n이를 방지하기 위해 초기화 시점에도 Redis 분산 락을 활용하여 단 하나의 인스턴스만 초기화 작업을 수행하도록 보장하는 것이 안전합니다.
public class PinGeoRedisInitializer {\n\n private final PinLocationRepository pinLocationRepository;\n private final PinGeoRedisService pinGeoRedisService;\n private final org.springframework.data.redis.core.RedisTemplate<String, String> redisTemplate;\n\n @Async\n @Order(1)\n @EventListener(ApplicationReadyEvent.class)\n public void initialize() {\n try {\n if (pinGeoRedisService.isGeoSetReady()) {\n log.info(\"[Redis GEO] 캐시가 이미 초기화되어 있어 초기화를 건너뜁니다.\");\n return;\n }\n\n Boolean acquired = redisTemplate.opsForValue().setIfAbsent(\"lock:geo:init\", \"1\", java.time.Duration.ofMinutes(5));\n if (!Boolean.TRUE.equals(acquired)) {\n log.info(\"[Redis GEO] 다른 인스턴스가 이미 초기화를 진행 중이므로 건너뜁니다.\");\n return;\n }\n\n try {\n log.info(\"[Redis GEO] 캐시 초기화 시작 ...\");\n List<MapPinView> views = pinLocationRepository.findAllActivePins();\n pinGeoRedisService.bulkPopulate(views);\n log.info(\"[Redis GEO] 캐시 초기화 완료: {}개 핀 적재\", views.size());\n } finally {\n redisTemplate.delete(\"lock:geo:init\");\n }\n } catch (Exception e) {\n log.warn(\"[Redis GEO] 캐시 초기화 실패 (서버는 정상 기동, DB 폴백 사용): {}\", e.getMessage());\n }\n }\n}| public boolean isGeoSetReady() { | ||
| if (isReady) { | ||
| // 로컬 플래그가 true 라도 재구성 중 GEO Set 이 비어 있으면 DB 폴백합니다. | ||
| return getGeoPinCount() > 0; | ||
| } | ||
| // 로컬 플래그 미설정 → 다른 인스턴스가 이미 적재했는지 Redis 글로벌 키로 확인합니다. | ||
| try { | ||
| if (Boolean.TRUE.equals(redisTemplate.hasKey(GEO_READY_KEY)) && getGeoPinCount() > 0) { | ||
| isReady = true; | ||
| return true; | ||
| } | ||
| } catch (Exception e) { | ||
| log.warn("Redis GEO 상태 확인 실패: {}", e.getMessage()); | ||
| } | ||
| return false; | ||
| } |
There was a problem hiding this comment.
현재 isGeoSetReady() 메서드는 로컬 isReady 플래그가 true이더라도 매 요청마다 Redis에 ZCARD 명령(getGeoPinCount())을 동기적으로 호출하여 데이터 존재 여부를 확인하고 있습니다.\n\n이 방식은 캐시 재구성 중 DB 폴백을 정확하게 처리할 수 있지만, 트래픽이 몰릴 때 모든 지도 조회 API 요청마다 Redis 네트워크 왕복(RTT)이 발생하여 성능 저하 및 Redis 부하를 유발합니다.\n\n개선 방안:\n로컬에서 isReady 상태를 판단할 때, Redis 조회 결과를 일정 시간(예: 5초~10초) 동안 로컬 캐싱하거나, 캐시 재구성 시작 시 Redis Pub/Sub 등을 통해 다른 인스턴스에 알리는 방식을 고려해 주세요. 최소한 매번 ZCARD를 호출하는 대신 로컬 플래그를 최대한 신뢰하고, 주기적으로만 Redis 상태를 동기화하는 것이 효율적입니다.
| } catch (Exception e) { | ||
| log.error("[Redis GEO] 야간 캐시 재구성 실패: {}", e.getMessage(), e); | ||
| } finally { | ||
| // 정상·비정상 종료 모두 락 즉시 해제 (다음 예약 실행까지 기다릴 필요 없음) | ||
| redisTemplate.delete(REBUILD_LOCK_KEY); | ||
| } | ||
| } |
There was a problem hiding this comment.
야간 배치 스케줄러가 실행된 후 finally 블록에서 락을 즉시 해제(redisTemplate.delete)하고 있습니다. 다중 서버 환경에서 서버 간의 미세한 시간 차이(Clock Drift)가 존재할 경우, 먼저 시작한 서버가 작업을 빠르게 끝내고 락을 해제하면, 약간 늦게 기동된 다른 서버가 다시 락을 획득하여 중복으로 캐시 재구성을 실행할 수 있습니다.\n\n야간 배치는 하루에 한 번만 실행되므로, finally 블록에서 락을 수동으로 삭제하지 않고 REBUILD_LOCK_TTL (10분) 동안 자연 만료되도록 두는 것이 안전합니다. 이렇게 하면 시간 차이가 수 초~수 분 나더라도 중복 실행을 확실히 방지할 수 있습니다.
} catch (Exception e) {\n log.error(\"[Redis GEO] 야간 캐시 재구성 실패: {}\", e.getMessage(), e);\n }\n }\n}
🔗 Related Issue
✨ 작업 개요
찐막 v.0.3.3 버전 배포합니다~!
체크리스트
📷 이미지 첨부 (선택)
🧐 집중 리뷰 요청