Skip to content

[deploy] issue pin, festival pin redis cache write 파트 구현 배포#137

Merged
selnem merged 3 commits into
prodfrom
develop
Jun 18, 2026
Merged

[deploy] issue pin, festival pin redis cache write 파트 구현 배포#137
selnem merged 3 commits into
prodfrom
develop

Conversation

@selnem

@selnem selnem commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

[deploy] issue pin, festival pin redis cache write 파트 구현 배포

selnem added 3 commits June 18, 2026 12:28
BE PinGeoRedisService와 동일 규약의 PinGeoRedisPublisher를 추가하고,
DB commit 성공 직후 geo:pins·pin:info를 갱신한다.
이슈 핀 create/update와 축제 배치 import 경로에 연동해
지도 핀 조회 API Cache-Aside 캐시 정합성을 맞춘다.
DB commit 이후 캐시 동기화 실패 시에도 API가 500으로 실패하지 않도록
좌표 파싱·region 조회·Redis 적재 예외를 warning 로그로만 남긴다.
[FEAT] AI 핀 생성·축제 적재 후 Redis GEO 캐시 동기화
@selnem selnem self-assigned this Jun 18, 2026
@selnem selnem added the 🚀 deploy 배포 label Jun 18, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Redis-based GEO cache publisher (PinGeoRedisPublisher) to synchronize pin location data for both festival and issue pins. It integrates this publisher into FestivalEventIngestService and IssueService to update Redis upon pin creation or modification, and adds corresponding unit tests. The review feedback focuses on performance optimizations: addressing potential N+1 database queries during batch imports in FestivalEventIngestService by pre-fetching region mappings, and utilizing Redis pipelines in add_pin and remove_pin to minimize network round trips.

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.

errors: list[dict[str, Any]] = []
items: list[FestivalBatchItemResult] = []
pin_ids: list[int] = []
geo_cache_items: list[PinGeoCacheItem] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

💡 배치 작업 시 N+1 쿼리 최적화 제안

대량의 페스티벌 데이터를 배치 임포트할 때, 각 핀마다 LocationRepo.get_region_by_id를 호출하면 핀 개수만큼 DB 조회가 발생합니다.
이를 방지하기 위해 배치 시작 시점에 전체 지역 정보를 미리 조회하여 캐시 맵(region_map)을 구성한 뒤 하위 메서드로 전달하는 것을 권장합니다.

추천 흐름:

  1. 여기서 region_map을 빌드합니다.
  2. _insert_festival_pin_update_festival_pin 호출 시 region_map을 인자로 전달합니다.
Suggested change
geo_cache_items: list[PinGeoCacheItem] = []
geo_cache_items: list[PinGeoCacheItem] = []
region_map = {}
try:
locations = await self._location_repo.get_all_ordered()
region_map = {loc.location_id: loc.region for loc in locations}
except Exception:
region_map = {}

Comment on lines +663 to +681
async def _build_festival_geo_cache_item(
self,
*,
pin_id: int,
location_id: int,
detail_address: str,
pin_point,
) -> PinGeoCacheItem:
lat, lng = wgs84_from_pin_point(pin_point)
region = await self._location_repo.get_region_by_id(location_id) or ""
return PinGeoCacheItem(
pin_id=pin_id,
pin_type=PinType.FESTIVAL.name,
lat=lat,
lng=lng,
detail_address=detail_address,
region=region,
discount=None,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

⚠️ N+1 쿼리 발생 우려 (성능 최적화 필요)

_build_festival_geo_cache_item 메서드 내에서 각 핀마다 self._location_repo.get_region_by_id(location_id)를 호출하여 데이터베이스 조회를 수행하고 있습니다.

import_batchimport_all과 같이 대량의 페스티벌 핀을 배치로 가져오는 과정에서, 등록/수정되는 핀의 개수(N개)만큼 추가적인 단건 SELECT 쿼리가 발생하게 되어 심각한 성능 저하(N+1 Query Problem)를 유발할 수 있습니다.

개선 제안:
_import_handoff_rows 시작 시점에 전체 지역 정보를 한 번에 조회하여 캐시 맵(region_map)을 구성한 뒤, 이를 _insert_festival_pin -> _build_festival_geo_cache_item으로 전달하여 메모리 내에서 조회하도록 개선하는 것을 권장합니다.

    async def _build_festival_geo_cache_item(
        self,
        *,
        pin_id: int,
        location_id: int,
        detail_address: str,
        pin_point,
        region_map: dict[int, str] | None = None,
    ) -> PinGeoCacheItem:
        lat, lng = wgs84_from_pin_point(pin_point)
        if region_map and location_id in region_map:
            region = region_map[location_id] or ""
        else:
            region = await self._location_repo.get_region_by_id(location_id) or ""
        return PinGeoCacheItem(
            pin_id=pin_id,
            pin_type=PinType.FESTIVAL.name,
            lat=lat,
            lng=lng,
            detail_address=detail_address,
            region=region,
            discount=None,
        )

Comment on lines +38 to +68
async def add_pin(
self,
*,
pin_id: int,
pin_type: str,
lat: float,
lng: float,
detail_address: str,
region: str,
discount: str | None = None,
) -> None:
try:
member = str(pin_id)
await self._redis.geoadd(GEO_KEY_ALL, (lng, lat, member))
await self._redis.geoadd(f"{GEO_KEY_PREFIX}{pin_type}", (lng, lat, member))
payload = _build_pin_info_json(
pin_id=pin_id,
pin_type=pin_type,
lat=lat,
lng=lng,
detail_address=detail_address,
region=region,
discount=discount,
)
await self._redis.set(
f"{PIN_INFO_KEY_PREFIX}{pin_id}",
payload,
ex=PIN_INFO_TTL_SECONDS,
)
except Exception as exc:
logger.warning("Redis GEO 핀 추가 실패 pin_id=%s: %s", pin_id, exc)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

⚡ Redis 파이프라인 적용을 통한 성능 개선 제안

add_pin 메서드에서도 geoadd 2회와 set 1회로 총 3번의 Redis 명령을 개별적으로 await 호출하고 있습니다.

이 경우 Redis 서버와의 네트워크 왕복 시간(RTT)이 3배로 늘어나게 됩니다. add_pins_batch 메서드와 마찬가지로 비트랜잭션 파이프라인(pipeline(transaction=False))을 사용하여 3개의 명령을 한 번에 전송하면 단건 추가 시에도 성능을 크게 개선할 수 있습니다.

    async def add_pin(
        self,
        *,
        pin_id: int,
        pin_type: str,
        lat: float,
        lng: float,
        detail_address: str,
        region: str,
        discount: str | None = None,
    ) -> None:
        try:
            member = str(pin_id)
            payload = _build_pin_info_json(
                pin_id=pin_id,
                pin_type=pin_type,
                lat=lat,
                lng=lng,
                detail_address=detail_address,
                region=region,
                discount=discount,
            )
            async with self._redis.pipeline(transaction=False) as pipe:
                pipe.geoadd(GEO_KEY_ALL, (lng, lat, member))
                pipe.geoadd(f"{GEO_KEY_PREFIX}{pin_type}", (lng, lat, member))
                pipe.set(
                    f"{PIN_INFO_KEY_PREFIX}{pin_id}",
                    payload,
                    ex=PIN_INFO_TTL_SECONDS,
                )
                await pipe.execute()
        except Exception as exc:
            logger.warning("Redis GEO 핀 추가 실패 pin_id=%s: %s", pin_id, exc)

Comment on lines +96 to +103
async def remove_pin(self, *, pin_id: int, pin_type: str) -> None:
try:
member = str(pin_id)
await self._redis.zrem(GEO_KEY_ALL, member)
await self._redis.zrem(f"{GEO_KEY_PREFIX}{pin_type}", member)
await self._redis.delete(f"{PIN_INFO_KEY_PREFIX}{pin_id}")
except Exception as exc:
logger.warning("Redis GEO 핀 삭제 실패 pin_id=%s: %s", pin_id, exc)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

⚡ Redis 파이프라인 적용을 통한 성능 개선 제안

remove_pin 메서드에서 zrem 2회와 delete 1회로 총 3번의 Redis 명령을 개별적으로 await 호출하고 있습니다.

이 경우 Redis 서버와의 네트워크 왕복 시간(RTT)이 3배로 늘어나게 됩니다. add_pins_batch 메서드와 마찬가지로 비트랜잭션 파이프라인(pipeline(transaction=False))을 사용하여 3개의 명령을 한 번에 전송하면 성능을 크게 개선할 수 있습니다.

Suggested change
async def remove_pin(self, *, pin_id: int, pin_type: str) -> None:
try:
member = str(pin_id)
await self._redis.zrem(GEO_KEY_ALL, member)
await self._redis.zrem(f"{GEO_KEY_PREFIX}{pin_type}", member)
await self._redis.delete(f"{PIN_INFO_KEY_PREFIX}{pin_id}")
except Exception as exc:
logger.warning("Redis GEO 핀 삭제 실패 pin_id=%s: %s", pin_id, exc)
async def remove_pin(self, *, pin_id: int, pin_type: str) -> None:
try:
member = str(pin_id)
async with self._redis.pipeline(transaction=False) as pipe:
pipe.zrem(GEO_KEY_ALL, member)
pipe.zrem(f"{GEO_KEY_PREFIX}{pin_type}", member)
pipe.delete(f"{PIN_INFO_KEY_PREFIX}{pin_id}")
await pipe.execute()
except Exception as exc:
logger.warning("Redis GEO 핀 삭제 실패 pin_id=%s: %s", pin_id, exc)

@selnem selnem merged commit 21b2842 into prod Jun 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant