Skip to content

Commit 75d4c6c

Browse files
authored
Merge pull request #68 from prgrms-web-devcourse-final-project/refactor/map-api
지도 API 성능 개선을 위한 Geohash 기반 구조 리팩토링 및 Redis 캐싱 적용
2 parents 1c714c7 + b9f1ad5 commit 75d4c6c

29 files changed

+760
-239
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ dependencies {
3737

3838
implementation ("org.antlr:antlr4-runtime:4.10.1")
3939

40+
implementation("ch.hsr:geohash:1.4.0")
41+
4042

4143
// PostgreSQL + PostGIS
4244
implementation("org.postgresql:postgresql:42.7.3") // 최신 버전 확인

src/main/java/com/example/log4u/common/config/RedisConfig.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
package com.example.log4u.common.config;
22

3+
import java.util.List;
4+
35
import org.springframework.context.annotation.Bean;
46
import org.springframework.context.annotation.Configuration;
57
import org.springframework.data.redis.connection.RedisConnectionFactory;
68
import org.springframework.data.redis.core.RedisTemplate;
79
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
10+
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
811
import org.springframework.data.redis.serializer.StringRedisSerializer;
912

13+
import com.example.log4u.domain.map.dto.response.DiaryClusterResponseDto;
14+
import com.fasterxml.jackson.databind.JavaType;
15+
import com.fasterxml.jackson.databind.ObjectMapper;
16+
17+
import software.amazon.awssdk.thirdparty.jackson.core.type.TypeReference;
18+
1019
@Configuration
1120
public class RedisConfig {
1221

@@ -20,4 +29,19 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connec
2029

2130
return template;
2231
}
32+
33+
@Bean
34+
public RedisTemplate<String, List<DiaryClusterResponseDto>> diaryClusterRedisTemplate(
35+
RedisConnectionFactory connectionFactory,
36+
ObjectMapper objectMapper
37+
) {
38+
RedisTemplate<String, List<DiaryClusterResponseDto>> template = new RedisTemplate<>();
39+
template.setConnectionFactory(connectionFactory);
40+
template.setKeySerializer(new StringRedisSerializer());
41+
42+
JavaType javaType = objectMapper.getTypeFactory().constructCollectionType(List.class, DiaryClusterResponseDto.class);
43+
Jackson2JsonRedisSerializer<List<DiaryClusterResponseDto>> serializer = new Jackson2JsonRedisSerializer<>(objectMapper, javaType);
44+
template.setValueSerializer(serializer);
45+
return template;
46+
}
2347
}

src/main/java/com/example/log4u/common/redis/ClusterCacheInitializer.java

Lines changed: 0 additions & 41 deletions
This file was deleted.

src/main/java/com/example/log4u/common/redis/RedisDao.java

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.example.log4u.domain.diary.entity;
2+
3+
import jakarta.persistence.Entity;
4+
import jakarta.persistence.GeneratedValue;
5+
import jakarta.persistence.GenerationType;
6+
import jakarta.persistence.Id;
7+
import jakarta.persistence.Table;
8+
import lombok.AccessLevel;
9+
import lombok.AllArgsConstructor;
10+
import lombok.Builder;
11+
import lombok.Getter;
12+
import lombok.NoArgsConstructor;
13+
14+
@Entity
15+
@Getter
16+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
17+
@AllArgsConstructor
18+
@Builder
19+
@Table(name = "DiaryGeoHash")
20+
public class DiaryGeoHash {
21+
22+
@Id
23+
@GeneratedValue(strategy = GenerationType.IDENTITY)
24+
private Long id;
25+
26+
private Long diaryId;
27+
28+
private String geohash;
29+
}

src/main/java/com/example/log4u/domain/diary/facade/DiaryFacade.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.example.log4u.domain.diary.dto.PopularDiaryDto;
1414
import com.example.log4u.domain.diary.entity.Diary;
1515
import com.example.log4u.domain.diary.service.DiaryService;
16+
import com.example.log4u.domain.diary.service.DiaryGeohashService;
1617
import com.example.log4u.domain.hashtag.service.HashtagService;
1718
import com.example.log4u.domain.like.service.LikeService;
1819
import com.example.log4u.domain.map.service.MapService;
@@ -33,6 +34,7 @@ public class DiaryFacade {
3334
private final LikeService likeService;
3435
private final HashtagService hashtagService;
3536
private final UserService userService;
37+
private final DiaryGeohashService diaryGeohashService;
3638

3739
/**
3840
* 다이어리 생성 use case
@@ -41,23 +43,30 @@ public class DiaryFacade {
4143
* 2. diaryService: 다이어리 생성<br>
4244
* 2. mediaService: 해당 다이어리의 이미지 저장<br>
4345
* 3. mapService: 해당 구역 카운트 증가
46+
* 4. diaryGeohashService: 해당 다이어리 위치가 포함되어있는 geoHash 문자열 저장
4447
* */
4548
@Transactional
4649
public void createDiary(Long userId, DiaryRequestDto request) {
4750
String thumbnailUrl = mediaService.extractThumbnailUrl(request.mediaList());
4851
Diary diary = diaryService.saveDiary(userId, request, thumbnailUrl);
4952
mediaService.saveMedia(diary.getDiaryId(), request.mediaList());
5053
hashtagService.saveOrUpdateHashtag(diary.getDiaryId(), request.hashtagList());
51-
mapService.increaseRegionDiaryCount(request.location().latitude(), request.location().longitude());
54+
55+
double lat = request.location().latitude();
56+
double lon = request.location().longitude();
57+
mapService.increaseRegionDiaryCount(lat, lon);
58+
diaryGeohashService.saveGeohash(diary.getDiaryId(), lat, lon);
5259
}
5360

5461
/**
5562
* 다이어리 삭제 use case
5663
* <ul><li>호출 과정</li></ul>
5764
* 1. diaryService: 다이어리 검증
5865
* 2. mediaService: 해당 다이어리 이미지 삭제<br>
59-
* 3. diaryService: 다이어리 삭제<br>
60-
* 4. mapService: 해당 구역 카운트 감소
66+
* 3. mapService: 해당 구역 카운트 감소
67+
* 4. diaryGeohashService: 캐싱 되어있는 id, 데이터 삭제
68+
* 5. diaryService: 다이어리 삭제<br>
69+
*
6170
* */
6271
@Transactional
6372
public void deleteDiary(Long userId, Long diaryId) {
@@ -66,6 +75,7 @@ public void deleteDiary(Long userId, Long diaryId) {
6675
hashtagService.deleteHashtagsByDiaryId(diaryId);
6776
mapService.decreaseRegionDiaryCount(diary.getLocation().getLatitude(), diary.getLocation().getLongitude());
6877

78+
diaryGeohashService.deleteGeohashAndCache(diaryId);
6979
diaryService.deleteDiary(diary);
7080
}
7181

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.example.log4u.domain.diary.repository;
2+
3+
import java.util.List;
4+
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
8+
import com.example.log4u.domain.diary.entity.DiaryGeoHash;
9+
10+
import io.lettuce.core.dynamic.annotation.Param;
11+
12+
public interface DiaryGeoHashRepository extends JpaRepository<DiaryGeoHash, Long> {
13+
14+
@Query("SELECT d.diaryId FROM DiaryGeoHash d WHERE d.geohash = :geohash")
15+
List<Long> findDiaryIdByGeohash(@Param("geohash") String geohash);
16+
17+
DiaryGeoHash findByDiaryId(Long diaryId);
18+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.example.log4u.domain.diary.service;
2+
3+
import java.util.List;
4+
5+
import org.springframework.stereotype.Service;
6+
import org.springframework.transaction.annotation.Transactional;
7+
8+
import com.example.log4u.domain.diary.entity.DiaryGeoHash;
9+
import com.example.log4u.domain.diary.repository.DiaryGeoHashRepository;
10+
import com.example.log4u.domain.map.cache.dao.DiaryCacheDao;
11+
12+
import ch.hsr.geohash.GeoHash;
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
@Slf4j
19+
public class DiaryGeohashService {
20+
21+
private final DiaryGeoHashRepository diaryGeoHashRepository;
22+
private final DiaryCacheDao diaryCacheDao;
23+
24+
@Transactional
25+
public void saveGeohash(Long diaryId, double lat, double lon) {
26+
String hash = GeoHash.withCharacterPrecision(lat, lon, 5).toBase32();
27+
DiaryGeoHash diaryGeoHash = DiaryGeoHash.builder()
28+
.diaryId(diaryId)
29+
.geohash(hash)
30+
.build();
31+
diaryGeoHashRepository.save(diaryGeoHash);
32+
}
33+
34+
@Transactional
35+
public void deleteGeohashAndCache(Long diaryId) {
36+
DiaryGeoHash geoHash = diaryGeoHashRepository.findByDiaryId(diaryId);
37+
diaryCacheDao.evictDiaryIdFromCache(geoHash.getGeohash(), diaryId);
38+
diaryCacheDao.evictDiaryFromCache(diaryId);
39+
40+
diaryGeoHashRepository.deleteById(diaryId);
41+
}
42+
43+
public List<Long> getDiaryIdsByGeohash(String geohash) {
44+
return diaryGeoHashRepository.findDiaryIdByGeohash(geohash);
45+
}
46+
}

src/main/java/com/example/log4u/domain/diary/service/DiaryService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,4 +278,8 @@ public void checkDiaryExists(Long diaryId) {
278278
public List<Diary> getTop10Diaries() {
279279
return diaryRepository.findTop10ByVisibilityOrderByLikeCountDesc(VisibilityType.PUBLIC);
280280
}
281+
282+
public List<Diary> getDiaries(List<Long> ids) {
283+
return diaryRepository.findAllById(ids);
284+
}
281285
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.example.log4u.domain.map.cache;
2+
3+
public class CacheKeyGenerator {
4+
5+
private static final String CLUSTER_CACHE_KEY_FORMAT = "cluster:geohash:%s:level:%d";
6+
private static final String DIARY_KEY_PREFIX = "marker:diary:";
7+
private static final String GEOHASH_ID_SET_PREFIX = "marker:ids:geohash:";
8+
9+
public static String clusterCacheKey(String geohash, int level) {
10+
return String.format(CLUSTER_CACHE_KEY_FORMAT, geohash, level);
11+
}
12+
13+
public static String diaryKey(Long diaryId) {
14+
return DIARY_KEY_PREFIX + diaryId;
15+
}
16+
17+
public static String diaryIdSetKey(String geohash) {
18+
return GEOHASH_ID_SET_PREFIX + geohash;
19+
}
20+
}
21+

0 commit comments

Comments
 (0)