11package com .example .log4u .domain .map .service ;
22
3- import java .time .Duration ;
3+ import java .util .ArrayList ;
4+ import java .util .Collections ;
5+ import java .util .HashSet ;
46import java .util .List ;
7+ import java .util .Set ;
8+ import java .util .stream .Collectors ;
9+ import java .util .stream .Stream ;
510
611import org .springframework .stereotype .Service ;
712import org .springframework .transaction .annotation .Transactional ;
813
9- import com .example .log4u .common .redis .RedisDao ;
10- import com .example .log4u .domain .diary .repository .DiaryRepository ;
14+ import com .example .log4u .domain .diary .entity .Diary ;
15+ import com .example .log4u .domain .diary .service .DiaryService ;
16+ import com .example .log4u .domain .diary .service .DiaryGeohashService ;
1117import com .example .log4u .domain .map .dto .response .DiaryClusterResponseDto ;
1218import com .example .log4u .domain .map .dto .response .DiaryMarkerResponseDto ;
19+ import com .example .log4u .domain .map .cache .dao .ClusterCacheDao ;
20+ import com .example .log4u .domain .map .cache .dao .DiaryCacheDao ;
21+ import com .example .log4u .domain .map .cache .RedisTTLPolicy ;
22+ import com .example .log4u .domain .map .exception .InvalidGeohashException ;
23+ import com .example .log4u .domain .map .exception .InvalidMapLevelException ;
1324import com .example .log4u .domain .map .repository .sido .SidoAreasDiaryCountRepository ;
1425import com .example .log4u .domain .map .repository .sido .SidoAreasRepository ;
1526import com .example .log4u .domain .map .repository .sigg .SiggAreasDiaryCountRepository ;
@@ -27,28 +38,118 @@ public class MapService {
2738 private final SidoAreasDiaryCountRepository sidoAreasDiaryCountRepository ;
2839 private final SiggAreasRepository siggAreasRepository ;
2940 private final SiggAreasDiaryCountRepository siggAreasDiaryCountRepository ;
30- private final DiaryRepository diaryRepository ;
31- private final RedisDao redisDao ;
32-
33- public List <DiaryClusterResponseDto > getDiaryClusters (
34- double south , double north , double west , double east , int zoom ) {
35- if (zoom <= 10 ) {
36- return getSidoAreasClusters (south , north , west , east );
37- } else {
38- return getSiggAreasClusters (south , north , west , east );
41+ private final DiaryCacheDao diaryCacheDao ;
42+ private final ClusterCacheDao clusterCacheDao ;
43+ private final DiaryService diaryService ;
44+ private final DiaryGeohashService diaryGeohashService ;
45+
46+ /**
47+ * 캐싱 전략: Look-Aside + Write-Around
48+ * - Redis에서 클러스터 데이터를 먼저 조회 (캐시 HIT 시 바로 응답)
49+ * - 캐시 MISS 시 DB에서 조회 후 Redis에 저장하고 응답
50+ *
51+ * level 기준:
52+ * - level 1: 시/도 단위 클러스터 (sido)
53+ * - level 2: 시/군/구 단위 클러스터 (sigg)
54+ */
55+ @ Transactional (readOnly = true )
56+ public List <DiaryClusterResponseDto > getDiaryClusters (String geohash , int level ) {
57+ validateGeohashLength (geohash , 3 );
58+ return clusterCacheDao .getDiaryCluster (geohash , level )
59+ .orElseGet (() -> {
60+ List <DiaryClusterResponseDto > dbResult = loadClustersFromDb (geohash , level );
61+ clusterCacheDao .setDiaryCluster (geohash , level , dbResult , RedisTTLPolicy .CLUSTER_TTL );
62+ return dbResult ;
63+ });
64+ }
65+
66+ private List <DiaryClusterResponseDto > loadClustersFromDb (String geohash , int level ) {
67+ return switch (level ) {
68+ case 1 -> sidoAreasRepository .findByGeohashPrefix (geohash );
69+ case 2 -> siggAreasRepository .findByGeohashPrefix (geohash );
70+ default -> throw new InvalidMapLevelException ();
71+ };
72+ }
73+
74+ /**
75+ * 캐싱 전략: Look-Aside + Write-Around
76+ *
77+ * [1단계] geohash → diaryIds 조회
78+ * - Redis(Set)에서 diaryIds 조회 (캐시 HIT 시 바로 사용)
79+ * - 캐시 MISS 시 DB 조회 후 Redis에 저장 (Write-Around)
80+ *
81+ * [2단계] diaryId → DiaryMarkerDto 조회
82+ * - Redis(String)에서 Diary DTO 조회 (캐시 HIT 시 바로 사용)
83+ * - 캐시 MISS 시 DB에서 조회 후 Redis에 저장 (Write-Around)
84+ */
85+ @ Transactional (readOnly = true )
86+ public List <DiaryMarkerResponseDto > getDiariesByGeohash (String geohash ) {
87+ validateGeohashLength (geohash , 5 );
88+ Set <Long > diaryIds = loadDiaryIdsFromGeoCache (geohash );
89+ return loadDiaryDtosFromCache (diaryIds );
90+ }
91+
92+ private Set <Long > loadDiaryIdsFromGeoCache (String geohash ) {
93+ Set <Long > cachedIds = diaryCacheDao .getDiaryIdSetFromCache (geohash );
94+ if (!cachedIds .isEmpty ())
95+ return cachedIds ;
96+
97+ List <Long > diaryIds = diaryGeohashService .getDiaryIdsByGeohash (geohash );
98+ diaryCacheDao .cacheDiaryIdSetByGeohash (geohash , diaryIds );
99+ return new HashSet <>(diaryIds );
100+ }
101+
102+ private List <DiaryMarkerResponseDto > loadDiaryDtosFromCache (Set <Long > diaryIds ) {
103+ List <DiaryMarkerResponseDto > cached = getCachedDiaryDtos (diaryIds );
104+ List <Long > missedIds = getCacheMissedIds (diaryIds , cached );
105+ List <DiaryMarkerResponseDto > loaded = loadDiariesAndCache (missedIds );
106+
107+ return Stream .concat (cached .stream (), loaded .stream ())
108+ .toList ();
109+ }
110+
111+ private List <DiaryMarkerResponseDto > getCachedDiaryDtos (Set <Long > ids ) {
112+ List <DiaryMarkerResponseDto > cached = new ArrayList <>();
113+ for (Long id : ids ) {
114+ DiaryMarkerResponseDto dto = diaryCacheDao .getDiaryFromCache (id );
115+ if (dto != null ) cached .add (dto );
39116 }
117+ return cached ;
40118 }
41119
42- private List <DiaryClusterResponseDto > getSidoAreasClusters (double south , double north , double west , double east ) {
43- return sidoAreasRepository .findSidoAreaClusters (south , north , west , east );
120+ private List <Long > getCacheMissedIds (Set <Long > allIds , List <DiaryMarkerResponseDto > cachedDtos ) {
121+ Set <Long > cachedIds = cachedDtos .stream ()
122+ .map (DiaryMarkerResponseDto ::diaryId )
123+ .collect (Collectors .toSet ());
124+
125+ return allIds .stream ()
126+ .filter (id -> !cachedIds .contains (id ))
127+ .collect (Collectors .toList ());
44128 }
45129
46- private List <DiaryClusterResponseDto > getSiggAreasClusters (double south , double north , double west , double east ) {
47- return siggAreasRepository .findSiggAreaClusters (south , north , west , east );
130+ private List <DiaryMarkerResponseDto > loadDiariesAndCache (List <Long > missedIds ) {
131+ if (missedIds .isEmpty ())
132+ return Collections .emptyList ();
133+
134+ List <Diary > diaries = diaryService .getDiaries (missedIds );
135+ List <DiaryMarkerResponseDto > result = new ArrayList <>();
136+
137+ for (Diary diary : diaries ) {
138+ DiaryMarkerResponseDto dto = DiaryMarkerResponseDto .of (diary );
139+ diaryCacheDao .cacheDiary (diary .getDiaryId (), dto );
140+ result .add (dto );
141+ }
142+
143+ return result ;
48144 }
49145
146+ private void validateGeohashLength (String geohash , int expectedLength ) {
147+ if (geohash == null || geohash .length () != expectedLength ) {
148+ throw new InvalidGeohashException ();
149+ }
150+ }
50151
51- @ Transactional
152+ @ Transactional
52153 public void increaseRegionDiaryCount (Double lat , Double lon ) {
53154 sidoAreasRepository .findRegionByLatLon (lat , lon )
54155 .flatMap (sido -> sidoAreasDiaryCountRepository .findById (sido .getId ()))
@@ -81,10 +182,4 @@ public void decreaseRegionDiaryCount(Double lat, Double lon) {
81182 siggAreasDiaryCountRepository .save (count );
82183 });
83184 }
84-
85- @ Transactional (readOnly = true )
86- public List <DiaryMarkerResponseDto > getDiariesInBounds (double south , double north , double west , double east ) {
87- return diaryRepository .findDiariesInBounds (south , north , west , east );
88- }
89-
90185}
0 commit comments