33import static org .assertj .core .api .AssertionsForInterfaceTypes .*;
44import static org .mockito .BDDMockito .*;
55
6+ import java .util .Collections ;
67import java .util .List ;
8+ import java .util .Optional ;
9+ import java .util .Set ;
710
811import org .junit .jupiter .api .DisplayName ;
912import org .junit .jupiter .api .Test ;
1215import org .mockito .Mock ;
1316import org .mockito .junit .jupiter .MockitoExtension ;
1417
18+ import com .example .log4u .domain .diary .entity .Diary ;
1519import com .example .log4u .domain .diary .repository .DiaryRepository ;
16- import com .example .log4u .domain .map .dto .response .DiaryMarkerResponseDto ;
17- import com .example .log4u .fixture .AreaClusterFixture ;
20+ import com .example .log4u .domain .diary .service .DiaryGeohashService ;
21+ import com .example .log4u .domain .diary .service .DiaryService ;
22+ import com .example .log4u .domain .map .cache .dao .ClusterCacheDao ;
23+ import com .example .log4u .domain .map .cache .dao .DiaryCacheDao ;
1824import com .example .log4u .domain .map .dto .response .DiaryClusterResponseDto ;
25+ import com .example .log4u .domain .map .dto .response .DiaryMarkerResponseDto ;
26+ import com .example .log4u .domain .map .exception .InvalidGeohashException ;
27+ import com .example .log4u .domain .map .exception .InvalidMapLevelException ;
1928import com .example .log4u .domain .map .repository .sido .SidoAreasRepository ;
2029import com .example .log4u .domain .map .repository .sigg .SiggAreasRepository ;
2130import com .example .log4u .domain .map .service .MapService ;
31+ import com .example .log4u .fixture .DiaryFixture ;
2232import com .example .log4u .fixture .DiaryMarkerFixture ;
2333
2434@ DisplayName ("지도 API 단위 테스트" )
@@ -37,64 +47,259 @@ class MapServiceTest {
3747 @ Mock
3848 private DiaryRepository diaryRepository ;
3949
40- // @DisplayName("성공 테스트: 줌레벨이 10 이하이면 시/도 클러스터 조회")
41- // @Test
42- // void getDiaryClusters_sidoAreas_success() {
43- // // given
44- // double south = 37.0, north = 38.0, west = 126.0, east = 127.0;
45- // int zoom = 9;
46- //
47- // given(sidoAreasRepository.findSidoAreaClusters(south, north, west, east))
48- // .willReturn(AreaClusterFixture.sidoAreaList());
49- //
50- // // when
51- // List<DiaryClusterResponseDto> result = mapService.getDiaryClusters(south, north, west, east, zoom);
52- //
53- // // then
54- // assertThat(result).hasSize(2);
55- // assertThat(result.getFirst().areaName()).isEqualTo("서울특별시");
56- // assertThat(result.getFirst().diaryCount()).isEqualTo(100L);
57- // verify(sidoAreasRepository).findSidoAreaClusters(south, north, west, east);
58- // }
59- //
60- // @DisplayName("성공 테스트: 줌레벨이 11 이상이면 시군구 클러스터 조회")
61- // @Test
62- // void getDiaryClusters_siggAreas_success() {
63- // // given
64- // double south = 37.0, north = 38.0, west = 126.0, east = 127.0;
65- // int zoom = 12;
66- //
67- // given(siggAreasRepository.findSiggAreaClusters(south, north, west, east))
68- // .willReturn(AreaClusterFixture.siggAreaList());
69- //
70- // // when
71- // List<DiaryClusterResponseDto> result = mapService.getDiaryClusters(south, north, west, east, zoom);
72- //
73- // // then
74- // assertThat(result).hasSize(2);
75- // assertThat(result.get(1).areaName()).isEqualTo("송파구");
76- // assertThat(result.get(1).diaryCount()).isEqualTo(30L);
77- // verify(siggAreasRepository).findSiggAreaClusters(south, north, west, east);
78- // }
79-
80- @ DisplayName ("성공 테스트: 마커 조회 (줌 14 이상)" )
50+ @ Mock
51+ private DiaryCacheDao diaryCacheDao ;
52+
53+ @ Mock
54+ private DiaryService diaryService ;
55+
56+ @ Mock
57+ private DiaryGeohashService diaryGeohashService ;
58+
59+ @ Mock
60+ private ClusterCacheDao clusterCacheDao ;
61+
62+ private static final String GEOHASH = "abc" ;
63+ private static final int VALID_LEVEL_1 = 1 ;
64+ private static final int VALID_LEVEL_2 = 2 ;
65+
66+ private final List <DiaryClusterResponseDto > mockResult = List .of (
67+ new DiaryClusterResponseDto ("서울" , 1L , 37.5665 , 126.9780 , 10L )
68+ );
69+
70+ @ DisplayName ("성공: 클러스터 캐시 HIT" )
71+ @ Test
72+ void getDiaryClusters_success_cacheHit () {
73+ // given
74+ given (clusterCacheDao .getDiaryCluster (GEOHASH , VALID_LEVEL_1 )).willReturn (Optional .of (mockResult ));
75+
76+ // when
77+ List <DiaryClusterResponseDto > result = mapService .getDiaryClusters (GEOHASH , VALID_LEVEL_1 );
78+
79+ // then
80+ assertThat (result ).isEqualTo (mockResult );
81+ verify (sidoAreasRepository , never ()).findByGeohashPrefix (any ());
82+ verify (clusterCacheDao , never ()).setDiaryCluster (any (), anyInt (), any (), any ());
83+ }
84+
85+ @ DisplayName ("성공: 캐시 MISS → DB 조회 → 캐시 저장" )
86+ @ Test
87+ void getDiaryClusters_success_cacheMiss_then_dbHit_and_cacheStore () {
88+ // given
89+ given (clusterCacheDao .getDiaryCluster (GEOHASH , VALID_LEVEL_1 )).willReturn (Optional .empty ());
90+ given (sidoAreasRepository .findByGeohashPrefix (GEOHASH )).willReturn (mockResult );
91+
92+ // when
93+ List <DiaryClusterResponseDto > result = mapService .getDiaryClusters (GEOHASH , VALID_LEVEL_1 );
94+
95+ // then
96+ assertThat (result ).isEqualTo (mockResult );
97+ verify (clusterCacheDao ).setDiaryCluster (eq (GEOHASH ), eq (VALID_LEVEL_1 ), eq (mockResult ), any ());
98+ }
99+
100+ @ DisplayName ("실패: 유효하지 않은 level" )
101+ @ Test
102+ void getDiaryClusters_invalidLevel () {
103+ // given
104+ int invalidLevel = 99 ;
105+
106+ // expect
107+ assertThatThrownBy (() -> mapService .getDiaryClusters (GEOHASH , invalidLevel ))
108+ .isInstanceOf (InvalidMapLevelException .class );
109+ }
110+
111+ @ DisplayName ("실패: geohash 길이 불일치" )
112+ @ Test
113+ void getDiaryClusters_invalidGeohashLength () {
114+ // given
115+ String invalidGeohash = "abcd" ;
116+
117+ // expect
118+ assertThatThrownBy (() -> mapService .getDiaryClusters (invalidGeohash , VALID_LEVEL_1 ))
119+ .isInstanceOf (InvalidGeohashException .class )
120+ .hasMessageContaining ("geohash 길이가 유효하지 않습니다" );
121+ }
122+
123+ @ DisplayName ("성공 : geohash 캐시 HIT + 모든 diary 캐시 HIT" )
124+ @ Test
125+ void getDiariesByGeohash_success_allCacheHit () {
126+ // given
127+ String geohash = "wydmt" ;
128+ Set <Long > cachedIds = Set .of (1L , 2L );
129+ DiaryMarkerResponseDto dto1 = DiaryMarkerFixture .createDiaryMarker (1L );
130+ DiaryMarkerResponseDto dto2 = DiaryMarkerFixture .createDiaryMarker (2L );
131+
132+ given (diaryCacheDao .getDiaryIdSetFromCache (geohash )).willReturn (cachedIds );
133+ given (diaryCacheDao .getDiaryFromCache (1L )).willReturn (dto1 );
134+ given (diaryCacheDao .getDiaryFromCache (2L )).willReturn (dto2 );
135+
136+ // when
137+ List <DiaryMarkerResponseDto > result = mapService .getDiariesByGeohash (geohash );
138+
139+ // then
140+ assertThat (result ).containsExactlyInAnyOrder (dto1 , dto2 );
141+ verify (diaryRepository , never ()).findAllById (any ());
142+ verify (diaryGeohashService , never ()).getDiaryIdsByGeohash (any ());
143+ }
144+
145+ @ DisplayName ("성공 : geohash 캐시 HIT + 모든 diary 캐시 MISS" )
146+ @ Test
147+ void getDiariesByGeohash_success_allDiaryCacheMiss () {
148+ // given
149+ String geohash = "wydmt" ;
150+ Set <Long > cachedIds = Set .of (1L , 2L );
151+ Diary diary1 = DiaryFixture .createDiaryFixture (1L );
152+ Diary diary2 = DiaryFixture .createDiaryFixture (2L );
153+ DiaryMarkerResponseDto dto1 = DiaryMarkerResponseDto .of (diary1 );
154+ DiaryMarkerResponseDto dto2 = DiaryMarkerResponseDto .of (diary2 );
155+
156+ given (diaryCacheDao .getDiaryIdSetFromCache (geohash )).willReturn (cachedIds );
157+ given (diaryCacheDao .getDiaryFromCache (1L )).willReturn (null );
158+ given (diaryCacheDao .getDiaryFromCache (2L )).willReturn (null );
159+ given (diaryService .getDiaries (List .of (1L , 2L ))).willReturn (List .of (diary1 , diary2 ));
160+
161+ // when
162+ List <DiaryMarkerResponseDto > result = mapService .getDiariesByGeohash (geohash );
163+
164+ // then
165+ assertThat (result ).containsExactlyInAnyOrder (dto1 , dto2 );
166+ verify (diaryCacheDao ).cacheDiary (1L , dto1 );
167+ verify (diaryCacheDao ).cacheDiary (2L , dto2 );
168+ }
169+
170+ @ DisplayName ("성공 : geohash 캐시 HIT + 일부 diary 캐시 MISS" )
171+ @ Test
172+ void getDiariesByGeohash_success_partialDiaryCacheMiss () {
173+ // given
174+ String geohash = "wydmt" ;
175+ Set <Long > cachedIds = Set .of (1L , 2L );
176+ DiaryMarkerResponseDto dto1 = DiaryMarkerFixture .createDiaryMarker (1L );
177+ Diary diary2 = DiaryFixture .createDiaryFixture (2L );
178+ DiaryMarkerResponseDto dto2 = DiaryMarkerResponseDto .of (diary2 );
179+
180+ given (diaryCacheDao .getDiaryIdSetFromCache (geohash )).willReturn (cachedIds );
181+ given (diaryCacheDao .getDiaryFromCache (1L )).willReturn (dto1 );
182+ given (diaryCacheDao .getDiaryFromCache (2L )).willReturn (null );
183+ given (diaryService .getDiaries (List .of (2L ))).willReturn (List .of (diary2 ));
184+
185+ // when
186+ List <DiaryMarkerResponseDto > result = mapService .getDiariesByGeohash (geohash );
187+
188+ // then
189+ assertThat (result ).containsExactlyInAnyOrder (dto1 , dto2 );
190+ verify (diaryService ).getDiaries (List .of (2L ));
191+ }
192+
193+ @ DisplayName ("성공 : geohash 캐시 MISS → DB 조회 → 모든 diary 캐시 HIT" )
194+ @ Test
195+ void getDiariesByGeohash_success_geohashMiss_allDiaryCacheHit () {
196+ // given
197+ String geohash = "wydmt" ;
198+ List <Long > diaryIdsFromDb = List .of (1L , 2L );
199+ DiaryMarkerResponseDto dto1 = DiaryMarkerFixture .createDiaryMarker (1L );
200+ DiaryMarkerResponseDto dto2 = DiaryMarkerFixture .createDiaryMarker (2L );
201+
202+ given (diaryCacheDao .getDiaryIdSetFromCache (geohash )).willReturn (Collections .emptySet ());
203+ given (diaryGeohashService .getDiaryIdsByGeohash (geohash )).willReturn (diaryIdsFromDb );
204+ given (diaryCacheDao .getDiaryFromCache (1L )).willReturn (dto1 );
205+ given (diaryCacheDao .getDiaryFromCache (2L )).willReturn (dto2 );
206+
207+ // when
208+ List <DiaryMarkerResponseDto > result = mapService .getDiariesByGeohash (geohash );
209+
210+ // then
211+ assertThat (result ).containsExactlyInAnyOrder (dto1 , dto2 );
212+ verify (diaryCacheDao ).cacheDiaryIdSetByGeohash (geohash , diaryIdsFromDb );
213+ verify (diaryService , never ()).getDiaries (any ());
214+ }
215+
216+ @ DisplayName ("성공 : geohash 캐시 MISS → DB 조회 → 모든 diary 캐시 MISS" )
217+ @ Test
218+ void getDiariesByGeohash_success_geohashMiss_allDiaryCacheMiss () {
219+ // given
220+ String geohash = "wydmt" ;
221+ List <Long > diaryIdsFromDb = List .of (1L , 2L );
222+ Diary diary1 = DiaryFixture .createDiaryFixture (1L );
223+ Diary diary2 = DiaryFixture .createDiaryFixture (2L );
224+ DiaryMarkerResponseDto dto1 = DiaryMarkerResponseDto .of (diary1 );
225+ DiaryMarkerResponseDto dto2 = DiaryMarkerResponseDto .of (diary2 );
226+
227+ given (diaryCacheDao .getDiaryIdSetFromCache (geohash )).willReturn (Collections .emptySet ());
228+ given (diaryGeohashService .getDiaryIdsByGeohash (geohash )).willReturn (diaryIdsFromDb );
229+ given (diaryCacheDao .getDiaryFromCache (1L )).willReturn (null );
230+ given (diaryCacheDao .getDiaryFromCache (2L )).willReturn (null );
231+ given (diaryService .getDiaries (List .of (1L , 2L ))).willReturn (List .of (diary1 , diary2 ));
232+
233+ // when
234+ List <DiaryMarkerResponseDto > result = mapService .getDiariesByGeohash (geohash );
235+
236+ // then
237+ assertThat (result ).containsExactlyInAnyOrder (dto1 , dto2 );
238+ verify (diaryCacheDao ).cacheDiaryIdSetByGeohash (geohash , diaryIdsFromDb );
239+ verify (diaryCacheDao ).cacheDiary (1L , dto1 );
240+ verify (diaryCacheDao ).cacheDiary (2L , dto2 );
241+ }
242+
243+ @ DisplayName ("성공 : geohash 캐시 MISS → DB 조회 → diary 일부 캐시 MISS" )
81244 @ Test
82- void getDiariesInBounds_success () {
245+ void getDiariesByGeohash_success_geohashCacheMissAndDiaryCacheMiss () {
83246 // given
84- double south = 37.0 , north = 38.0 , west = 126.0 , east = 127.0 ;
85- List <DiaryMarkerResponseDto > markers = DiaryMarkerFixture .createDiaryMarkers ();
247+ String geohash = "abcde" ;
248+ List <Long > dbIds = List .of (1L , 2L );
249+ DiaryMarkerResponseDto dto1 = DiaryMarkerFixture .createDiaryMarker (1L );
250+ Diary diary2 = DiaryFixture .createDiaryFixture (2L );
251+ DiaryMarkerResponseDto dto2 = DiaryMarkerResponseDto .of (diary2 );
252+
253+ given (diaryCacheDao .getDiaryIdSetFromCache (geohash )).willReturn (Collections .emptySet ());
254+ given (diaryGeohashService .getDiaryIdsByGeohash (geohash )).willReturn (dbIds );
255+ given (diaryCacheDao .getDiaryFromCache (1L )).willReturn (dto1 );
256+ given (diaryCacheDao .getDiaryFromCache (2L )).willReturn (null );
257+ given (diaryService .getDiaries (List .of (2L ))).willReturn (List .of (diary2 ));
258+
259+ // when
260+ List <DiaryMarkerResponseDto > result = mapService .getDiariesByGeohash (geohash );
86261
87- given (diaryRepository .findDiariesInBounds (south , north , west , east ))
88- .willReturn (markers );
262+ // then
263+ assertThat (result ).containsExactlyInAnyOrder (dto1 , dto2 );
264+ verify (diaryCacheDao ).cacheDiaryIdSetByGeohash (geohash , dbIds );
265+ }
266+
267+ @ DisplayName ("성공 : Redis 예외 발생 시 fallback 작동: DB에서 조회됨" )
268+ @ Test
269+ void getDiariesByGeohash_redisFailureHandledInternally () {
270+ // given
271+ String geohash = "abcde" ;
272+ given (diaryCacheDao .getDiaryIdSetFromCache (geohash )).willReturn (Collections .emptySet ());
273+
274+ List <Long > diaryIds = List .of (1L );
275+ Diary diary = DiaryFixture .createDiaryFixture (1L );
276+ given (diaryGeohashService .getDiaryIdsByGeohash (geohash )).willReturn (diaryIds );
277+ given (diaryService .getDiaries (diaryIds )).willReturn (List .of (diary ));
278+
279+ // when
280+ List <DiaryMarkerResponseDto > result = mapService .getDiariesByGeohash (geohash );
281+
282+ // then
283+ assertThat (result ).hasSize (1 );
284+ assertThat (result .getFirst ().diaryId ()).isEqualTo (1L );
285+ verify (diaryService ).getDiaries (diaryIds );
286+ }
287+
288+ @ DisplayName ("예외 : diaryId 존재하나 DB에 diary 없음 " )
289+ @ Test
290+ void diaryIdExistsButDiaryMissingInDb () {
291+ // given
292+ String geohash = "wydmt" ;
293+ Set <Long > cachedIds = Set .of (100L );
294+ given (diaryCacheDao .getDiaryIdSetFromCache (geohash )).willReturn (cachedIds );
295+ given (diaryCacheDao .getDiaryFromCache (100L )).willReturn (null ); // cache miss
296+ given (diaryService .getDiaries (List .of (100L ))).willReturn (Collections .emptyList ());
89297
90298 // when
91- List <DiaryMarkerResponseDto > result = mapService .getDiariesInBounds ( south , north , west , east );
299+ List <DiaryMarkerResponseDto > result = mapService .getDiariesByGeohash ( geohash );
92300
93301 // then
94- assertThat (result ).hasSize (2 );
95- assertThat (result .get (0 ).title ()).isEqualTo ("첫번째 다이어리" );
96- assertThat (result .get (1 ).likeCount ()).isEqualTo (7L );
97- verify (diaryRepository ).findDiariesInBounds (south , north , west , east );
302+ assertThat (result ).isEmpty ();
98303 }
99304
100305}
0 commit comments