Skip to content

refactor: improve Redis GeoHash code with event-driven pattern#85

Merged
kangminhyuk1111 merged 1 commit into
mainfrom
refactor/redis-geo-index-improvements
Jan 27, 2026
Merged

refactor: improve Redis GeoHash code with event-driven pattern#85
kangminhyuk1111 merged 1 commit into
mainfrom
refactor/redis-geo-index-improvements

Conversation

@kangminhyuk1111

@kangminhyuk1111 kangminhyuk1111 commented Jan 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • 트랜잭션 정합성 개선: @TransactionalEventListener 도입으로 DB 커밋 후 Redis 업데이트
  • 페이지네이션 구현: offset + limit 파라미터 지원으로 위치 기반 조회 페이지네이션
  • 벌크 연산 성능 개선: Redis Pipeline 사용으로 초기화/동기화 성능 향상
  • 예외 처리 강화: RedisGeoIndexException 추가로 명확한 에러 핸들링
  • 결과 순서 보장: Redis 거리순 조회 후 MySQL 조회 결과를 Redis 순서로 재정렬
  • 메모리 효율화: findSearchableMatchIds() 메서드 추가로 ID만 조회

Changes

New Files (5)

  • RedisGeoIndexException.java - Redis 예외 클래스
  • MatchGeoIndexEvent.java - sealed interface
  • MatchCreatedEvent.java - 매치 생성 이벤트
  • MatchRemovedFromGeoIndexEvent.java - 매치 제거 이벤트
  • MatchGeoIndexEventListener.java - 트랜잭션 이벤트 리스너

Modified Files (12)

  • MatchGeoIndexPort.java - GeoIndexEntry record, addMatchesBulk(), offset 파라미터
  • MatchRepositoryPort.java - findSearchableMatchIds() 추가
  • SpringDataMatchRepository.java - ID 조회 쿼리 추가
  • MatchJpaAdapter.java - findSearchableMatchIds() 구현
  • MatchGeoRedisAdapter.java - 예외 처리, 벌크 연산, offset 지원
  • MatchCreator/Canceller/Reactivator/StatusUpdater.java - 이벤트 발행으로 전환
  • MatchFinder.java - 페이지네이션 + 순서 보장
  • GeoDataInitializer.java - 벌크 연산 적용
  • GeoDataSyncScheduler.java - ID 조회 + 벌크 연산 적용

Test plan

  • ./gradlew test 모든 테스트 통과 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

새로운 기능

  • 위치 기반 매치 검색에 페이지네이션 지원
  • 벌크 작업으로 매치 데이터 처리 성능 향상

개선 사항

  • 지오인덱싱 오류 처리 강화
  • 이벤트 기반 매치 상태 관리 시스템 도입
  • 검색 기능 안정성 개선

✏️ Tip: You can customize this high-level summary in your review settings.

- Add @TransactionalEventListener for transaction consistency
- Implement pagination with offset support in geo queries
- Add Redis Pipeline for bulk operations (GeoDataInitializer)
- Add exception handling with RedisGeoIndexException
- Preserve Redis distance order when fetching from MySQL
- Add findSearchableMatchIds() for memory efficiency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jan 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

이 변경 사항은 매치 지오 인덱싱을 동기식 직접 호출에서 비동기 이벤트 기반 아키텍처로 전환합니다. Redis 지오 인덱스 작업에 대한 대량 연산 지원, 페이지네이션 기능, 그리고 새로운 예외 처리를 추가합니다.

Changes

Cohort / 파일(들) 변경 사항 요약
Repository 및 Port 인터페이스
backend/src/main/java/com/hoops/match/application/port/out/MatchRepositoryPort.java, backend/src/main/java/com/hoops/match/application/port/out/MatchGeoIndexPort.java, backend/src/main/java/com/hoops/match/adapter/out/persistence/SpringDataMatchRepository.java, backend/src/main/java/com/hoops/match/adapter/out/persistence/MatchJpaAdapter.java
새로운 메서드 추가: findSearchableMatchIds()는 메모리 효율적인 ID 조회 제공; addMatchesBulk() 대량 작업 지원; findMatchIdsWithinRadius()에 오프셋 파라미터 추가로 페이지네이션 지원; GeoIndexEntry 레코드 타입 신규 추가
Redis 어댑터 및 예외 처리
backend/src/main/java/com/hoops/match/adapter/out/redis/MatchGeoRedisAdapter.java, backend/src/main/java/com/hoops/match/adapter/out/redis/exception/RedisGeoIndexException.java
RedisGeoIndexException 신규 예외 클래스 추가; 모든 지오 인덱스 메서드에 try-catch 기반 예외 처리 추가; addMatchesBulk() 메서드로 Redis 파이프라이닝을 통한 대량 추가 구현; findMatchIdsWithinRadius() 서명에 오프셋 파라미터 추가
이벤트 기반 지오 인덱싱 아키텍처
backend/src/main/java/com/hoops/match/application/event/MatchCreatedEvent.java, backend/src/main/java/com/hoops/match/application/event/MatchRemovedFromGeoIndexEvent.java, backend/src/main/java/com/hoops/match/application/event/MatchGeoIndexEvent.java, backend/src/main/java/com/hoops/match/application/event/MatchGeoIndexEventListener.java
새로운 sealed 인터페이스 MatchGeoIndexEvent 정의; MatchCreatedEventMatchRemovedFromGeoIndexEvent 레코드 타입 신규 추가; 트랜잭션 커밋 후 처리하는 MatchGeoIndexEventListener 컴포넌트 신규 추가
서비스 계층 이벤트 기반 리팩토링
backend/src/main/java/com/hoops/match/application/service/MatchCreator.java, backend/src/main/java/com/hoops/match/application/service/MatchCanceller.java, backend/src/main/java/com/hoops/match/application/service/MatchStatusUpdater.java, backend/src/main/java/com/hoops/match/application/service/MatchReactivator.java
직접적인 MatchGeoIndexPort 호출을 ApplicationEventPublisher를 통한 이벤트 발행으로 대체; 동기식 지오 인덱스 업데이트를 비동기 이벤트 기반 처리로 전환
스케줄러 대량 연산 최적화
backend/src/main/java/com/hoops/match/application/scheduler/GeoDataInitializer.java, backend/src/main/java/com/hoops/match/application/scheduler/GeoDataSyncScheduler.java
요소별 추가 로직을 addMatchesBulk() 단일 호출로 대체; GeoIndexEntry 리스트 생성 및 스트림 매핑 사용; findSearchableMatchIds()로 MySQL의 검색 가능한 매치 ID 조회
매치 검색 및 페이지네이션
backend/src/main/java/com/hoops/match/application/service/MatchFinder.java
페이지네이션 오프셋 계산 추가 (offset = page * size); findMatchIdsWithinRadius()에 오프셋 및 제한값 전달; Redis 거리 순서에 따른 결과 재정렬을 위해 ID→Match 맵 기반 두 단계 검색 구현

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Service as MatchCreator/<br/>MatchReactivator
    participant DB as Match Database
    participant EventPublisher as ApplicationEventPublisher
    participant Listener as MatchGeoIndexEventListener
    participant Port as MatchGeoIndexPort
    participant Redis as Redis Geo Index

    User->>Service: Create/Reactivate Match
    Service->>DB: Save Match
    DB-->>Service: Match Saved
    Service->>EventPublisher: publishEvent(MatchCreatedEvent)
    EventPublisher->>Listener: handleMatchCreated() [AFTER_COMMIT]
    Listener->>Port: addMatch(matchId, lon, lat)
    Port->>Redis: GEOADD + Handle with try-catch
    Redis-->>Port: Success/RedisGeoIndexException
    Port-->>Listener: Complete
    
    Note over User,Redis: Alternative Flow: Match Cancellation
    User->>Service: Cancel/Start Match
    Service->>DB: Update Match Status
    DB-->>Service: Status Updated
    Service->>EventPublisher: publishEvent(MatchRemovedFromGeoIndexEvent)
    EventPublisher->>Listener: handleMatchRemovedFromGeoIndex() [AFTER_COMMIT]
    Listener->>Port: removeMatch(matchId)
    Port->>Redis: ZREM + Handle with try-catch
    Redis-->>Port: Success/RedisGeoIndexException
    Port-->>Listener: Complete
Loading
sequenceDiagram
    actor Scheduler
    participant Port1 as MatchRepositoryPort
    participant Port2 as MatchGeoIndexPort
    participant MySQL as MySQL Database
    participant Redis as Redis Geo Index

    Scheduler->>Port1: findSearchableMatchIds()
    MySQL-->>Port1: List of IDs
    Port1-->>Scheduler: mysqlIds
    
    Scheduler->>Port2: findAllMatchIds()
    Redis-->>Port2: List of IDs
    Port2-->>Scheduler: redisIds
    
    Scheduler->>Scheduler: missingIds = mysqlIds - redisIds
    
    Scheduler->>Port1: findMatchesByIds(missingIds)
    MySQL-->>Port1: Match Objects
    Port1-->>Scheduler: matches
    
    Scheduler->>Scheduler: entries = mapToGeoIndexEntry(matches)
    
    Scheduler->>Port2: addMatchesBulk(entries)
    Port2->>Redis: PIPELINE (Multiple GEOADD)
    Redis-->>Port2: RedisGeoIndexException on error
    Port2-->>Scheduler: Bulk Operation Complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 이벤트의 속삭임, 지오 인덱스를 춤추게 하고
대량 연산의 힘으로 Redis를 가르쳐
매치들이 거리와 함께 춤을 춘다네
비동기의 우아함, 우리의 새 길
페이지마다 정렬된 순서를 따르며

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경 사항을 정확하게 반영하고 있습니다. 이벤트 기반 패턴 도입과 Redis GeoHash 코드 개선이라는 핵심 목표를 명확하게 나타냅니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kangminhyuk1111
kangminhyuk1111 merged commit 0ce4116 into main Jan 27, 2026
1 of 2 checks passed
@kangminhyuk1111
kangminhyuk1111 deleted the refactor/redis-geo-index-improvements branch January 27, 2026 08:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In
`@backend/src/main/java/com/hoops/match/adapter/out/redis/exception/RedisGeoIndexException.java`:
- Around line 10-17: The ERROR_CODE constant in RedisGeoIndexException does not
follow the project's error-code pattern; update the constant name and value to a
compliant pattern (e.g., use INVALID_GEO_INDEX or DUPLICATE_GEO_INDEX depending
on the failure semantics) and update the RedisGeoIndexException constructors to
use that new constant (symbol: ERROR_CODE in class RedisGeoIndexException).
Ensure the chosen pattern matches the guide (INVALID_* for validation/format
errors or ALREADY_*/DUPLICATE_* for existence conflicts) and adjust only the
constant value/name so the existing super(...) calls remain correct.

In
`@backend/src/main/java/com/hoops/match/adapter/out/redis/MatchGeoRedisAdapter.java`:
- Around line 80-108: The method findMatchIdsWithinRadius is doing both fetching
and mapping and exceeds preferred length—extract the Redis query and the
result-mapping into two helpers (e.g., a private method
fetchGeoResults(BigDecimal longitude, BigDecimal latitude, double radiusKm, int
fetchCount) that calls redisTemplate.opsForGeo().radius(GEO_KEY, circle, args)
and returns GeoResults<...>, and a private method
mapGeoResultsToIds(GeoResults<RedisGeoCommands.GeoLocation<String>> results, int
offset, int limit) that handles null-check, skipping, limiting and uses
parseMatchId). Replace the inline logic in findMatchIdsWithinRadius to call
these two helpers and keep exception handling as-is.
- Around line 44-65: The addMatchesBulk method is too long and nested; extract
the pipeline logic into a small helper to reduce length and nesting. Create a
private helper (e.g., buildGeoAddPipeline or executeGeoAddPipeline) that accepts
List<GeoIndexEntry> and performs the redisTemplate.executePipelined call using
GEO_KEY, GeoIndexEntry.longitude()/latitude(), and
formatMatchId(entry.matchId()), then call that helper from addMatchesBulk and
preserve the same logging and exception handling (throw RedisGeoIndexException
on failure). Ensure helper is small and focused so addMatchesBulk only validates
entries, delegates to the helper, logs success, and handles exceptions.

In
`@backend/src/main/java/com/hoops/match/application/event/MatchGeoIndexEventListener.java`:
- Around line 21-31: The AFTER_COMMIT event handlers in
MatchGeoIndexEventListener (methods handleMatchCreated and
handleMatchRemovedFromGeoIndex) must not let exceptions propagate to callers;
wrap the calls to matchGeoIndex.addMatch(...) and matchGeoIndex.removeMatch(...)
in try/catch, log the error with context (including event.matchId()), and
instead of throwing re-publish a retry/compensation event or enqueue a job
(e.g., publish a MatchGeoIndexRetryEvent or push to a retry queue) so DB changes
remain committed and geo-index updates can be retried/compensated
asynchronously.
🧹 Nitpick comments (5)
backend/src/main/java/com/hoops/match/application/event/MatchRemovedFromGeoIndexEvent.java (1)

8-10: matchId null 방지 가드 추가를 권장합니다.
Line 8-10: null ID로 이벤트가 발행되면 리스너에서 NPE 위험이 있어 생성 시 방어하는 편이 안전합니다.

♻️ 제안 diff
 package com.hoops.match.application.event;
 
+import java.util.Objects;
+
 /**
  * 매치 Geo Index 제거 이벤트
  * 트랜잭션 커밋 후 Redis Geo Index에서 매치 제거
  * 매치 취소, 시작 등으로 검색 대상에서 제외될 때 발생
  */
 public record MatchRemovedFromGeoIndexEvent(
         Long matchId
 ) implements MatchGeoIndexEvent {
+    public MatchRemovedFromGeoIndexEvent {
+        Objects.requireNonNull(matchId, "matchId");
+    }
 }
backend/src/main/java/com/hoops/match/application/service/MatchCreator.java (1)

36-74: createMatch 메서드 길이가 코딩 가이드라인을 초과합니다.

현재 메서드가 약 38줄로, 가이드라인에서 권장하는 20줄 제한을 초과합니다. 객체 생성 로직을 별도의 private 메서드로 분리하는 것을 고려해 주세요.

♻️ 리팩토링 제안
 `@Override`
 public Match createMatch(CreateMatchCommand command) {
     MatchSchedule schedule = new MatchSchedule(
             command.matchDate(),
             command.startTime(),
             command.endTime()
     );

     policyValidator.validateCreateMatch(schedule, command.maxParticipants());
     validateNoOverlappingHosting(command.hostId(), schedule);

-    HostInfoResult hostInfo = hostInfoPort.getHostInfo(command.hostId());
-    LocationInfoResult locationInfo = locationInfoPort.getLocationInfo(command.locationId());
-
-    MatchHost host = new MatchHost(hostInfo.hostId(), hostInfo.nickname());
-    MatchLocation location = new MatchLocation(
-            locationInfo.latitude(),
-            locationInfo.longitude(),
-            locationInfo.address()
-    );
-
-    Match match = Match.create(
-            host,
-            command.title(),
-            command.description(),
-            location,
-            schedule,
-            command.maxParticipants()
-    );
+    Match match = buildMatch(command, schedule);

     Match savedMatch = matchRepository.save(match);

     eventPublisher.publishEvent(new MatchCreatedEvent(
             savedMatch.getId(),
             savedMatch.getLongitude(),
             savedMatch.getLatitude()
     ));

     return savedMatch;
 }

+private Match buildMatch(CreateMatchCommand command, MatchSchedule schedule) {
+    HostInfoResult hostInfo = hostInfoPort.getHostInfo(command.hostId());
+    LocationInfoResult locationInfo = locationInfoPort.getLocationInfo(command.locationId());
+
+    MatchHost host = new MatchHost(hostInfo.hostId(), hostInfo.nickname());
+    MatchLocation location = new MatchLocation(
+            locationInfo.latitude(),
+            locationInfo.longitude(),
+            locationInfo.address()
+    );
+
+    return Match.create(
+            host,
+            command.title(),
+            command.description(),
+            location,
+            schedule,
+            command.maxParticipants()
+    );
+}
backend/src/main/java/com/hoops/match/application/service/MatchReactivator.java (1)

30-34: 재활성화 시 MatchCreatedEvent 사용은 의미적으로 모호할 수 있습니다.

기능적으로는 정상 동작하지만, 매치 재활성화에 MatchCreatedEvent를 사용하는 것은 이벤트의 의미를 혼란스럽게 할 수 있습니다. MatchAddedToGeoIndexEvent와 같이 geo-index 추가 의도를 명확히 표현하는 이벤트 이름을 고려해 보세요.

backend/src/main/java/com/hoops/match/application/scheduler/GeoDataSyncScheduler.java (2)

30-71: syncGeoData 메서드가 길고 여러 책임을 가지고 있습니다.

현재 메서드가 약 40줄로 가이드라인(20줄)을 초과하며, "누락된 매치 추가"와 "고아 매치 제거"라는 두 가지 책임을 수행합니다. SRP 원칙에 따라 분리를 권장합니다.

♻️ 리팩토링 제안
 public void syncGeoData() {
     log.info("Starting Geo Index sync check...");

     Set<Long> mysqlIds = new HashSet<>(matchRepository.findSearchableMatchIds());
     Set<Long> redisIds = new HashSet<>(matchGeoIndex.findAllMatchIds());

-    // MySQL에는 있지만 Redis에는 없는 매치 추가
-    List<Long> missingIds = mysqlIds.stream()
-            .filter(id -> !redisIds.contains(id))
-            .toList();
-
-    if (!missingIds.isEmpty()) {
-        List<Match> missingMatches = matchRepository.findAllByIds(missingIds);
-        List<GeoIndexEntry> entries = missingMatches.stream()
-                .map(match -> new GeoIndexEntry(
-                        match.getId(),
-                        match.getLongitude(),
-                        match.getLatitude()
-                ))
-                .toList();
-
-        matchGeoIndex.addMatchesBulk(entries);
-
-        for (Long matchId : missingIds) {
-            log.warn("Added missing match to Redis: matchId={}", matchId);
-        }
-    }
-
-    // Redis에는 있지만 MySQL에는 없는 매치 제거
-    int removedCount = 0;
-    for (Long redisId : redisIds) {
-        if (!mysqlIds.contains(redisId)) {
-            matchGeoIndex.removeMatch(redisId);
-            removedCount++;
-            log.warn("Removed orphan match from Redis: matchId={}", redisId);
-        }
-    }
+    int addedCount = addMissingMatches(mysqlIds, redisIds);
+    int removedCount = removeOrphanMatches(mysqlIds, redisIds);

     log.info("Geo Index sync completed. Added: {}, Removed: {}", addedCount, removedCount);
 }
+
+private int addMissingMatches(Set<Long> mysqlIds, Set<Long> redisIds) {
+    List<Long> missingIds = mysqlIds.stream()
+            .filter(id -> !redisIds.contains(id))
+            .toList();
+
+    if (missingIds.isEmpty()) {
+        return 0;
+    }
+
+    List<Match> missingMatches = matchRepository.findAllByIds(missingIds);
+    List<GeoIndexEntry> entries = missingMatches.stream()
+            .map(match -> new GeoIndexEntry(
+                    match.getId(),
+                    match.getLongitude(),
+                    match.getLatitude()
+            ))
+            .toList();
+
+    matchGeoIndex.addMatchesBulk(entries);
+    missingIds.forEach(id -> log.warn("Added missing match to Redis: matchId={}", id));
+    return missingIds.size();
+}
+
+private int removeOrphanMatches(Set<Long> mysqlIds, Set<Long> redisIds) {
+    List<Long> orphanIds = redisIds.stream()
+            .filter(id -> !mysqlIds.contains(id))
+            .toList();
+
+    orphanIds.forEach(id -> {
+        matchGeoIndex.removeMatch(id);
+        log.warn("Removed orphan match from Redis: matchId={}", id);
+    });
+    return orphanIds.size();
+}

60-68: 고아 매치 제거에도 bulk 연산 적용을 고려해 주세요.

누락된 매치 추가는 addMatchesBulk를 사용하지만, 고아 매치 제거는 개별 호출(removeMatch)을 사용합니다. 일관성과 성능을 위해 removeMatchesBulk 메서드 추가를 고려해 보세요.

Comment on lines +10 to +17
private static final String ERROR_CODE = "REDIS_GEO_INDEX_ERROR";

public RedisGeoIndexException(String message) {
super(ERROR_CODE, message);
}

public RedisGeoIndexException(String message, Throwable cause) {
super(ERROR_CODE, message, cause);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

에러 코드 패턴이 가이드라인에 맞지 않습니다.
Line 10-17: ERROR_CODE가 허용 패턴(NOT_FOUND, ALREADY_/DUPLICATE_, NOT_HOST/NOT_PARTICIPANT, INVALID_*)에 맞지 않습니다. 패턴에 맞는 코드로 변경이 필요합니다. 코딩 가이드라인에 따라.

🐛 제안 diff
-    private static final String ERROR_CODE = "REDIS_GEO_INDEX_ERROR";
+    private static final String ERROR_CODE = "INVALID_REDIS_GEO_INDEX";
🤖 Prompt for AI Agents
In
`@backend/src/main/java/com/hoops/match/adapter/out/redis/exception/RedisGeoIndexException.java`
around lines 10 - 17, The ERROR_CODE constant in RedisGeoIndexException does not
follow the project's error-code pattern; update the constant name and value to a
compliant pattern (e.g., use INVALID_GEO_INDEX or DUPLICATE_GEO_INDEX depending
on the failure semantics) and update the RedisGeoIndexException constructors to
use that new constant (symbol: ERROR_CODE in class RedisGeoIndexException).
Ensure the chosen pattern matches the guide (INVALID_* for validation/format
errors or ALREADY_*/DUPLICATE_* for existence conflicts) and adjust only the
constant value/name so the existing super(...) calls remain correct.

Comment on lines +44 to 65
public void addMatchesBulk(List<GeoIndexEntry> entries) {
if (entries.isEmpty()) {
return;
}

Long removed = redisTemplate.opsForZSet().remove(GEO_KEY, member);
log.debug("Removed match from geo index: matchId={}, removed={}", matchId, removed);
try {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
byte[] keyBytes = GEO_KEY.getBytes(StandardCharsets.UTF_8);
for (GeoIndexEntry entry : entries) {
connection.geoCommands().geoAdd(
keyBytes,
new Point(entry.longitude().doubleValue(), entry.latitude().doubleValue()),
formatMatchId(entry.matchId()).getBytes(StandardCharsets.UTF_8)
);
}
return null;
});
log.info("Bulk added {} matches to geo index", entries.size());
} catch (Exception e) {
throw new RedisGeoIndexException("Failed to bulk add matches to geo index", e);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

addMatchesBulk 메서드를 분리해 길이/중첩을 줄이세요.
현재 메서드가 20줄을 넘고 중첩 단계가 많습니다. 파이프라인 실행을 헬퍼로 분리하면 규칙을 충족합니다.

♻️ 제안 수정
 public void addMatchesBulk(List<GeoIndexEntry> entries) {
     if (entries.isEmpty()) {
         return;
     }
 
     try {
-        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
-            byte[] keyBytes = GEO_KEY.getBytes(StandardCharsets.UTF_8);
-            for (GeoIndexEntry entry : entries) {
-                connection.geoCommands().geoAdd(
-                        keyBytes,
-                        new Point(entry.longitude().doubleValue(), entry.latitude().doubleValue()),
-                        formatMatchId(entry.matchId()).getBytes(StandardCharsets.UTF_8)
-                );
-            }
-            return null;
-        });
+        executeBulkGeoAdd(entries);
         log.info("Bulk added {} matches to geo index", entries.size());
     } catch (Exception e) {
         throw new RedisGeoIndexException("Failed to bulk add matches to geo index", e);
     }
 }
+
+private void executeBulkGeoAdd(List<GeoIndexEntry> entries) {
+    redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
+        byte[] keyBytes = GEO_KEY.getBytes(StandardCharsets.UTF_8);
+        for (GeoIndexEntry entry : entries) {
+            connection.geoCommands().geoAdd(
+                    keyBytes,
+                    new Point(entry.longitude().doubleValue(), entry.latitude().doubleValue()),
+                    formatMatchId(entry.matchId()).getBytes(StandardCharsets.UTF_8)
+            );
+        }
+        return null;
+    });
+}
코딩 가이드라인 기준입니다.
🤖 Prompt for AI Agents
In
`@backend/src/main/java/com/hoops/match/adapter/out/redis/MatchGeoRedisAdapter.java`
around lines 44 - 65, The addMatchesBulk method is too long and nested; extract
the pipeline logic into a small helper to reduce length and nesting. Create a
private helper (e.g., buildGeoAddPipeline or executeGeoAddPipeline) that accepts
List<GeoIndexEntry> and performs the redisTemplate.executePipelined call using
GEO_KEY, GeoIndexEntry.longitude()/latitude(), and
formatMatchId(entry.matchId()), then call that helper from addMatchesBulk and
preserve the same logging and exception handling (throw RedisGeoIndexException
on failure). Ensure helper is small and focused so addMatchesBulk only validates
entries, delegates to the helper, logs success, and handles exceptions.

Comment on lines +80 to +108
public List<Long> findMatchIdsWithinRadius(BigDecimal longitude, BigDecimal latitude, double radiusKm, int offset, int limit) {
try {
Point center = new Point(longitude.doubleValue(), latitude.doubleValue());
Distance radius = new Distance(radiusKm, RedisGeoCommands.DistanceUnit.KILOMETERS);
Circle circle = new Circle(center, radius);

// Redis GEORADIUS는 offset을 지원하지 않으므로, offset + limit 만큼 조회 후 skip
int fetchCount = offset + limit;

RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.sortAscending()
.limit(fetchCount);

GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(GEO_KEY, circle, args);

if (results == null) {
return List.of();
}

return results.getContent().stream()
.skip(offset)
.limit(limit)
.map(result -> parseMatchId(result.getContent().getName()))
.toList();
} catch (Exception e) {
throw new RedisGeoIndexException("Failed to find matches within radius", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

조회 로직을 헬퍼로 분리해 규칙을 맞춰주세요.
현재 메서드가 20줄을 넘고 단계가 많습니다. 조회/매핑 분리로 가독성과 규칙 준수를 확보할 수 있습니다.

♻️ 제안 수정
 public List<Long> findMatchIdsWithinRadius(BigDecimal longitude, BigDecimal latitude, double radiusKm, int offset, int limit) {
     try {
-        Point center = new Point(longitude.doubleValue(), latitude.doubleValue());
-        Distance radius = new Distance(radiusKm, RedisGeoCommands.DistanceUnit.KILOMETERS);
-        Circle circle = new Circle(center, radius);
-
-        // Redis GEORADIUS는 offset을 지원하지 않으므로, offset + limit 만큼 조회 후 skip
-        int fetchCount = offset + limit;
-
-        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
-                .newGeoRadiusArgs()
-                .sortAscending()
-                .limit(fetchCount);
-
-        GeoResults<RedisGeoCommands.GeoLocation<String>> results =
-                redisTemplate.opsForGeo().radius(GEO_KEY, circle, args);
-
-        if (results == null) {
-            return List.of();
-        }
-
-        return results.getContent().stream()
-                .skip(offset)
-                .limit(limit)
-                .map(result -> parseMatchId(result.getContent().getName()))
-                .toList();
+        GeoResults<RedisGeoCommands.GeoLocation<String>> results =
+                queryRadius(longitude, latitude, radiusKm, offset, limit);
+        return toMatchIds(results, offset, limit);
     } catch (Exception e) {
         throw new RedisGeoIndexException("Failed to find matches within radius", e);
     }
 }
+
+private GeoResults<RedisGeoCommands.GeoLocation<String>> queryRadius(
+        BigDecimal longitude, BigDecimal latitude, double radiusKm, int offset, int limit) {
+    Point center = new Point(longitude.doubleValue(), latitude.doubleValue());
+    Distance radius = new Distance(radiusKm, RedisGeoCommands.DistanceUnit.KILOMETERS);
+    Circle circle = new Circle(center, radius);
+    int fetchCount = offset + limit;
+    RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
+            .newGeoRadiusArgs()
+            .sortAscending()
+            .limit(fetchCount);
+    return redisTemplate.opsForGeo().radius(GEO_KEY, circle, args);
+}
+
+private List<Long> toMatchIds(GeoResults<RedisGeoCommands.GeoLocation<String>> results, int offset, int limit) {
+    if (results == null) {
+        return List.of();
+    }
+    return results.getContent().stream()
+            .skip(offset)
+            .limit(limit)
+            .map(result -> parseMatchId(result.getContent().getName()))
+            .toList();
+}
코딩 가이드라인 기준입니다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public List<Long> findMatchIdsWithinRadius(BigDecimal longitude, BigDecimal latitude, double radiusKm, int offset, int limit) {
try {
Point center = new Point(longitude.doubleValue(), latitude.doubleValue());
Distance radius = new Distance(radiusKm, RedisGeoCommands.DistanceUnit.KILOMETERS);
Circle circle = new Circle(center, radius);
// Redis GEORADIUS는 offset을 지원하지 않으므로, offset + limit 만큼 조회 후 skip
int fetchCount = offset + limit;
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.sortAscending()
.limit(fetchCount);
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(GEO_KEY, circle, args);
if (results == null) {
return List.of();
}
return results.getContent().stream()
.skip(offset)
.limit(limit)
.map(result -> parseMatchId(result.getContent().getName()))
.toList();
} catch (Exception e) {
throw new RedisGeoIndexException("Failed to find matches within radius", e);
}
public List<Long> findMatchIdsWithinRadius(BigDecimal longitude, BigDecimal latitude, double radiusKm, int offset, int limit) {
try {
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
queryRadius(longitude, latitude, radiusKm, offset, limit);
return toMatchIds(results, offset, limit);
} catch (Exception e) {
throw new RedisGeoIndexException("Failed to find matches within radius", e);
}
}
private GeoResults<RedisGeoCommands.GeoLocation<String>> queryRadius(
BigDecimal longitude, BigDecimal latitude, double radiusKm, int offset, int limit) {
Point center = new Point(longitude.doubleValue(), latitude.doubleValue());
Distance radius = new Distance(radiusKm, RedisGeoCommands.DistanceUnit.KILOMETERS);
Circle circle = new Circle(center, radius);
int fetchCount = offset + limit;
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.sortAscending()
.limit(fetchCount);
return redisTemplate.opsForGeo().radius(GEO_KEY, circle, args);
}
private List<Long> toMatchIds(GeoResults<RedisGeoCommands.GeoLocation<String>> results, int offset, int limit) {
if (results == null) {
return List.of();
}
return results.getContent().stream()
.skip(offset)
.limit(limit)
.map(result -> parseMatchId(result.getContent().getName()))
.toList();
}
🤖 Prompt for AI Agents
In
`@backend/src/main/java/com/hoops/match/adapter/out/redis/MatchGeoRedisAdapter.java`
around lines 80 - 108, The method findMatchIdsWithinRadius is doing both
fetching and mapping and exceeds preferred length—extract the Redis query and
the result-mapping into two helpers (e.g., a private method
fetchGeoResults(BigDecimal longitude, BigDecimal latitude, double radiusKm, int
fetchCount) that calls redisTemplate.opsForGeo().radius(GEO_KEY, circle, args)
and returns GeoResults<...>, and a private method
mapGeoResultsToIds(GeoResults<RedisGeoCommands.GeoLocation<String>> results, int
offset, int limit) that handles null-check, skipping, limiting and uses
parseMatchId). Replace the inline logic in findMatchIdsWithinRadius to call
these two helpers and keep exception handling as-is.

Comment on lines +21 to +31
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleMatchCreated(MatchCreatedEvent event) {
log.debug("Handling MatchCreatedEvent: matchId={}", event.matchId());
matchGeoIndex.addMatch(event.matchId(), event.longitude(), event.latitude());
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleMatchRemovedFromGeoIndex(MatchRemovedFromGeoIndexEvent event) {
log.debug("Handling MatchRemovedFromGeoIndexEvent: matchId={}", event.matchId());
matchGeoIndex.removeMatch(event.matchId());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

AFTER_COMMIT 예외 전파로 요청 실패가 발생할 수 있습니다.
커밋 후 예외가 전파되면 호출자는 실패로 인식하지만 DB는 이미 반영됩니다. 예외를 내부에서 처리하고 재시도/보상 경로로 분리하는 방식을 고려하세요.

🐛 제안 수정
 `@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT)
 public void handleMatchCreated(MatchCreatedEvent event) {
-    log.debug("Handling MatchCreatedEvent: matchId={}", event.matchId());
-    matchGeoIndex.addMatch(event.matchId(), event.longitude(), event.latitude());
+    try {
+        log.debug("Handling MatchCreatedEvent: matchId={}", event.matchId());
+        matchGeoIndex.addMatch(event.matchId(), event.longitude(), event.latitude());
+    } catch (Exception e) {
+        log.error("Failed to update geo index for created match: matchId={}", event.matchId(), e);
+        // TODO: 재시도/보상 경로 연계
+    }
 }
 
 `@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT)
 public void handleMatchRemovedFromGeoIndex(MatchRemovedFromGeoIndexEvent event) {
-    log.debug("Handling MatchRemovedFromGeoIndexEvent: matchId={}", event.matchId());
-    matchGeoIndex.removeMatch(event.matchId());
+    try {
+        log.debug("Handling MatchRemovedFromGeoIndexEvent: matchId={}", event.matchId());
+        matchGeoIndex.removeMatch(event.matchId());
+    } catch (Exception e) {
+        log.error("Failed to update geo index for removed match: matchId={}", event.matchId(), e);
+        // TODO: 재시도/보상 경로 연계
+    }
 }
🤖 Prompt for AI Agents
In
`@backend/src/main/java/com/hoops/match/application/event/MatchGeoIndexEventListener.java`
around lines 21 - 31, The AFTER_COMMIT event handlers in
MatchGeoIndexEventListener (methods handleMatchCreated and
handleMatchRemovedFromGeoIndex) must not let exceptions propagate to callers;
wrap the calls to matchGeoIndex.addMatch(...) and matchGeoIndex.removeMatch(...)
in try/catch, log the error with context (including event.matchId()), and
instead of throwing re-publish a retry/compensation event or enqueue a job
(e.g., publish a MatchGeoIndexRetryEvent or push to a retry queue) so DB changes
remain committed and geo-index updates can be retried/compensated
asynchronously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant