Skip to content

Commit 5474ab7

Browse files
Merge pull request #162 from prgrms-web-devcourse-final-project/feat/#127
[Concert] 공연 목록 , 상세 캐싱 추가
2 parents 9330819 + ff02e20 commit 5474ab7

8 files changed

Lines changed: 291 additions & 173 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.back.web7_9_codecrete_be.domain.concerts.controller;
22

33
import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.result.SetResultResponse;
4-
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertDetailResponse;
5-
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem;
6-
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertTicketTimeSetRequest;
7-
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertUpdateRequest;
4+
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.*;
85
import com.back.web7_9_codecrete_be.domain.concerts.service.ConcertNotifyService;
96
import com.back.web7_9_codecrete_be.domain.concerts.service.ConcertService;
107
import com.back.web7_9_codecrete_be.domain.concerts.service.KopisApiService;
@@ -125,6 +122,13 @@ public RsData<SetResultResponse> updateConcert() throws InterruptedException {
125122
return RsData.success(kopisApiService.updateConcertData());
126123
}
127124

125+
@Operation(summary = "공연 조회수 갱신", description = "캐시에 저장되어 있는 조회수를 DB에 반영하고 캐싱된 공연 목록을 초기화합니다.")
126+
@PostMapping("updateConcertViewCount")
127+
public RsData<Void> updateConcertViewCount(){
128+
concertService.viewCountUpdate();
129+
return RsData.success(null);
130+
}
131+
128132
@Operation(summary = "알림 이메일 전송", description = "예매일이 오늘인 공연에 대해 알림 이메일을 전송합니다.")
129133
@PostMapping("sendTicketingEmail")
130134
public RsData<String> sendTicketingEmail(){

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concert.ConcertListResponse;
44
import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concertPlace.ConcertPlaceDetailResponse;
55
import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concertPlace.ConcertPlaceListResponse;
6-
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertDetailResponse;
7-
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem;
8-
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertLikeResponse;
9-
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ListSort;
6+
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.*;
107
import com.back.web7_9_codecrete_be.domain.concerts.dto.concertPlace.PlaceDetailResponse;
118
import com.back.web7_9_codecrete_be.domain.concerts.dto.ticketOffice.TicketOfficeElement;
129
import com.back.web7_9_codecrete_be.domain.concerts.entity.TicketOffice;
@@ -80,7 +77,7 @@ public RsData<List<ConcertItem>> getLikedConcertList(
8077

8178
@Operation(summary = "공연 상세 조회", description = "공연에 대한 상세 목록을 조회합니다.")
8279
@GetMapping("concertDetail")
83-
public ConcertDetailResponse getConcertDetail(
80+
public RsData<ConcertDetailResponse> getConcertDetail(
8481
@RequestParam
8582
@Schema(description = """
8683
<h3>조회 기준이 되는 concertId입니다.</h3>
@@ -90,7 +87,7 @@ public ConcertDetailResponse getConcertDetail(
9087
""")
9188
long concertId
9289
) {
93-
return concertService.getConcertDetail(concertId);
90+
return RsData.success(concertService.getConcertDetail(concertId));
9491
}
9592

9693
@Operation(summary = "공연 예매처 조회", description = "공연에 대한 예매처들을 조회합니다.")

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertDetailResponse.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package com.back.web7_9_codecrete_be.domain.concerts.dto.concert;
22

3+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
4+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
5+
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
6+
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
7+
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
8+
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
39
import io.swagger.v3.oas.annotations.media.Schema;
410
import lombok.AllArgsConstructor;
511
import lombok.Getter;
@@ -29,12 +35,18 @@ public class ConcertDetailResponse {
2935
@Schema(description = "콘서트 장 주소입니다.")
3036
private String placeAddress;
3137

38+
@JsonSerialize(using = LocalDateTimeSerializer.class)
39+
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
3240
@Schema(description = "콘서트 예매 시작 날짜입니다.")
3341
private LocalDateTime ticketTime;
3442

43+
@JsonSerialize(using = LocalDateSerializer.class)
44+
@JsonDeserialize(using = LocalDateDeserializer.class)
3545
@Schema(description = "콘서트 시작 날짜입니다.",format = "yyyy-MM-dd")
3646
private LocalDate startDate;
3747

48+
@JsonSerialize(using = LocalDateSerializer.class)
49+
@JsonDeserialize(using = LocalDateDeserializer.class)
3850
@Schema(description = "콘서트 종료 날짜입니다.",format = "yyyy-MM-dd")
3951
private LocalDate endDate;
4052

@@ -55,4 +67,6 @@ public class ConcertDetailResponse {
5567

5668
@Schema(description = "콘서트 이미지 목록입니다.")
5769
private List<String> concertImageUrls;
70+
71+
5872
}

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertItem.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
package com.back.web7_9_codecrete_be.domain.concerts.dto.concert;
22

33
import com.back.web7_9_codecrete_be.domain.concerts.entity.Concert;
4+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
5+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
6+
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
7+
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
8+
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
9+
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
410
import io.swagger.v3.oas.annotations.media.Schema;
511
import lombok.Getter;
12+
import lombok.RequiredArgsConstructor;
613
import lombok.Setter;
714

815
import java.time.LocalDate;
916
import java.time.LocalDateTime;
1017

1118
@Getter
1219
@Setter
20+
@RequiredArgsConstructor
1321
public class ConcertItem {
1422

1523
@Schema(description = "콘서트 Id입니다.")
@@ -21,12 +29,18 @@ public class ConcertItem {
2129
@Schema(description = "콘서트 장소 이름입니다.")
2230
private String placeName;
2331

32+
@JsonSerialize(using = LocalDateTimeSerializer.class)
33+
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
2434
@Schema(description = "콘서트 예매 시작 날짜입니다.",format = "yyyy-MM-ddThh:mm:ss")
2535
private LocalDateTime ticketTime;
2636

37+
@JsonSerialize(using = LocalDateSerializer.class)
38+
@JsonDeserialize(using = LocalDateDeserializer.class)
2739
@Schema(description = "콘서트 시작 날짜입니다.",format = "yyyy-MM-dd")
2840
private LocalDate startDate ;
2941

42+
@JsonSerialize(using = LocalDateSerializer.class)
43+
@JsonDeserialize(using = LocalDateDeserializer.class)
3044
@Schema(description = "콘서트 종료 날짜입니다.",format = "yyyy-MM-dd")
3145
private LocalDate endDate ;
3246

@@ -59,7 +73,7 @@ public ConcertItem(Concert concert) {
5973
this.likeCount = concert.getLikeCount();
6074
}
6175

62-
public ConcertItem(long id, String name, String placeName,LocalDateTime ticketTime, LocalDate startDate, LocalDate endDate, String posterUrl, int maxPrice, int minPrice, int viewCount, int likeCount) {
76+
public ConcertItem(long id, String name, String placeName, LocalDateTime ticketTime, LocalDate startDate, LocalDate endDate, String posterUrl, int maxPrice, int minPrice, int viewCount, int likeCount) {
6377
this.id = id;
6478
this.name = name;
6579
this.placeName = placeName;

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
package com.back.web7_9_codecrete_be.domain.concerts.repository;
22

3+
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertDetailResponse;
4+
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem;
5+
import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ListSort;
36
import lombok.RequiredArgsConstructor;
4-
import org.springframework.data.redis.core.RedisTemplate;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.data.domain.Pageable;
9+
import org.springframework.data.redis.core.*;
10+
import org.springframework.security.core.parameters.P;
511
import org.springframework.stereotype.Repository;
612

13+
import java.util.*;
714
import java.util.concurrent.TimeUnit;
815

16+
@Slf4j
917
@Repository
1018
@RequiredArgsConstructor
1119
public class ConcertRedisRepository {
12-
private final RedisTemplate<String,String> redisTemplate;
20+
private final RedisTemplate<String, String> redisTemplate;
21+
private final RedisTemplate<String, Object> objectRedisTemplate;
1322

1423
private static final String LOCK_FLAG_PREFIX = "initLoad: ";
1524

25+
private static final String CONCERT_DETAIL_PREFIX = "concertDetail: ";
26+
27+
private static final String CONCERT_LIST_PREFIX = "concertList: ";
28+
29+
private static final String VIEW_COUNT_MAP = "viewCountMap";
30+
31+
private static final int HOUR = 3600;
32+
33+
// 최초 공연 로드 락
1634
public void lockSave(String key, String value) {
1735
redisTemplate.opsForValue().set(
1836
LOCK_FLAG_PREFIX + key,
@@ -29,4 +47,118 @@ public void unlockSave(String key) {
2947
redisTemplate.delete(LOCK_FLAG_PREFIX + key);
3048
}
3149

50+
// 공연 목록 캐싱
51+
public void listSave(ListSort sort, Pageable pageable, List<ConcertItem> list) {
52+
String key = CONCERT_LIST_PREFIX + sort.name() + pageable.getPageNumber();
53+
objectRedisTemplate.opsForValue().set(key, list, HOUR, TimeUnit.SECONDS);
54+
}
55+
56+
// 공연 목록 가져오기
57+
public List<ConcertItem> getConcertsList(Pageable pageable, ListSort sort) {
58+
String key = CONCERT_LIST_PREFIX + sort.name() + pageable.getPageNumber();
59+
Object object = objectRedisTemplate.opsForValue().get(key);
60+
List<ConcertItem> list = (List<ConcertItem>) object;
61+
if (list == null || list.isEmpty()) return List.of(); // null 이 아닌 empty 값 반환
62+
return list;
63+
}
64+
65+
// 캐싱된 모든 공연 목록 삭제
66+
public void deleteAllConcertsList() {
67+
deleteAllItemsByPREFIX(CONCERT_LIST_PREFIX);
68+
}
69+
70+
// 공연 상세 캐싱
71+
public void detailSave(long concertId, ConcertDetailResponse concertDetailResponse) {
72+
objectRedisTemplate.opsForValue().set(
73+
CONCERT_DETAIL_PREFIX + concertId,
74+
concertDetailResponse,
75+
HOUR,
76+
TimeUnit.SECONDS
77+
);
78+
}
79+
80+
// todo : 객체 일부의 값만 바뀌는거니 해당 값만 바꿔서 저장하거나 Redis 내부의 값만 갱신할 수 있는 방법 찾기
81+
public ConcertDetailResponse getDetail(long concertId) {
82+
ConcertDetailResponse concertDetailResponse = (ConcertDetailResponse) objectRedisTemplate.opsForValue().get(CONCERT_DETAIL_PREFIX + concertId);
83+
if (concertDetailResponse == null) return null;
84+
int viewCount = concertDetailResponse.getViewCount();
85+
viewCountSet(concertId, viewCount + 1);
86+
concertDetailResponse.setViewCount(viewCount + 1);
87+
detailSave(concertId, concertDetailResponse);
88+
return concertDetailResponse;
89+
}
90+
91+
// 공연 상세 삭제
92+
public void deleteDetail(String concertId) {
93+
redisTemplate.delete(CONCERT_DETAIL_PREFIX + concertId);
94+
}
95+
96+
// 모든 공연 상세 삭제
97+
public void deleteAllConcertDetail() {
98+
deleteAllItemsByPREFIX(CONCERT_DETAIL_PREFIX);
99+
}
100+
101+
// 조회수 처리 -> 좀 지저분한데 개선 여지 찾아보기
102+
public int viewCountSet(long concertId, int viewCount) {
103+
Map<String, Integer> rawMap = (Map<String, Integer>) objectRedisTemplate.opsForValue().get(VIEW_COUNT_MAP);
104+
105+
if (rawMap == null) {
106+
Map<Long, Integer> viewCountMap = new HashMap<>();
107+
viewCountMap.put(concertId, viewCount);
108+
objectRedisTemplate.opsForValue().set(VIEW_COUNT_MAP, viewCountMap);
109+
} else {
110+
Map<Long, Integer> viewCountMap = convertViewCountMap(rawMap);
111+
viewCountMap.put(concertId, viewCount);
112+
objectRedisTemplate.opsForValue().set(VIEW_COUNT_MAP, viewCountMap);
113+
log.info(viewCountMap.size() + "view count size.");
114+
}
115+
return viewCount;
116+
}
117+
118+
// 조회수 맵 조회
119+
public Map<Long, Integer> getViewCountMap() {
120+
Map<String, Integer> rawMap = (Map<String, Integer>) objectRedisTemplate.opsForValue().get(VIEW_COUNT_MAP);
121+
if (rawMap == null) return null;
122+
objectRedisTemplate.delete(VIEW_COUNT_MAP);
123+
return convertViewCountMap(rawMap);
124+
}
125+
126+
// 조회수 맵 삭제
127+
public void deleteViewCountMap() {
128+
objectRedisTemplate.delete(VIEW_COUNT_MAP);
129+
}
130+
131+
// String Integer 타입 맵을 Long Integer로 변환
132+
private Map<Long, Integer> convertViewCountMap(Map<String, Integer> rawMap) {
133+
Map<Long, Integer> viewCountMap = new HashMap<>();
134+
for (Map.Entry<String, Integer> stringIntegerEntry : rawMap.entrySet()) {
135+
viewCountMap.put(Long.parseLong(stringIntegerEntry.getKey()), stringIntegerEntry.getValue());
136+
}
137+
return viewCountMap;
138+
}
139+
140+
// 해당 접두어의 모든 항목 삭제
141+
private void deleteAllItemsByPREFIX(String prefix) {
142+
String pattern = CONCERT_LIST_PREFIX + "*";
143+
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build();
144+
Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
145+
Set<String> keySet = new HashSet<>();
146+
try (Cursor<byte[]> cursor = connection.scan(options)) {
147+
while (cursor.hasNext()) {
148+
keySet.add(new String(cursor.next()));
149+
}
150+
}
151+
return keySet;
152+
}
153+
);
154+
155+
if (keys != null && !keys.isEmpty()) {
156+
redisTemplate.delete(keys);
157+
log.info("Successfully deleted %s items with prefix: %s".formatted(keys.size() + "", prefix));
158+
} else {
159+
log.info("no items with prefix: %s".formatted(prefix));
160+
}
161+
}
162+
163+
32164
}

0 commit comments

Comments
 (0)