Skip to content

Commit 429e449

Browse files
Merge pull request #299 from prgrms-web-devcourse-final-project/feat/#295
[Concert] 공연 캐싱 처리 수정, 자동 완성 메모리 사용량 최적화
2 parents d1666bb + f3574ce commit 429e449

4 files changed

Lines changed: 108 additions & 81 deletions

File tree

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,12 @@ public Map<Long, Integer> getCachedViewCountMap() {
122122

123123
// 공연의 조회수 조회
124124
public Long getCachedViewCount(Long concertId) {
125-
Long viewCount = Long.valueOf(redisTemplate.opsForHash()
126-
.get(
127-
CONCERTS_VIEW_COUNTS,
128-
concertId.toString()
129-
)
125+
return Long.valueOf(Objects.requireNonNull(redisTemplate.opsForHash()
126+
.get(
127+
CONCERTS_VIEW_COUNTS,
128+
concertId.toString()
129+
))
130130
.toString());
131-
132-
return viewCount;
133131
}
134132

135133
// 캐시된 조회수 삭제

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ public class ConcertSearchRedisTemplate {
2121
private final RedisTemplate<String,String> redisTemplate;
2222

2323
private static final String INDEX_KEY = "index:";
24-
private static final String DATE_KEY = "data:";
25-
private static final String CONCERT_ID_KEY = "concertName:";
24+
private static final String CONCERT_ID_KEY = "concertId:";
2625

2726
public void addAllWordsWithWeight(List<WeightedString> weightedStrings) {
2827
// PipeLine 사용해서 한번에 처리 -> IO 시간 감소
@@ -32,18 +31,18 @@ public void addAllWordsWithWeight(List<WeightedString> weightedStrings) {
3231
String word = weightedString.getWord();
3332
double score = weightedString.getScore();
3433

35-
// 개별 문자들에 대한 키-값 설정
36-
connection.commands().set((CONCERT_ID_KEY + word).getBytes(StandardCharsets.UTF_8), id.getBytes(StandardCharsets.UTF_8));
34+
// 검색 결과를 ID - 제목 쌍으로 저장
35+
connection.commands().set((CONCERT_ID_KEY + id).getBytes(StandardCharsets.UTF_8),word.getBytes(StandardCharsets.UTF_8));
3736

37+
// 역 인덱싱
3838
for(int i = 0 ;i<word.length();i++){
3939
for(int j = i+1;j<= word.length();j++ ){
4040
String subWord = word.substring(i,j);
41-
4241
// 공백은 검색어 인덱스에서 제외
4342
if(subWord.isBlank()) continue;
44-
43+
// 서브 문자열을 인덱스 키, 값은 ID 값으로 해서 저장
4544
byte[] indexKey = (INDEX_KEY + subWord).getBytes(StandardCharsets.UTF_8);
46-
connection.zAdd(indexKey,score,word.getBytes(StandardCharsets.UTF_8));
45+
connection.zAdd(indexKey,score,id.getBytes(StandardCharsets.UTF_8));
4746
}
4847
}
4948
}
@@ -54,17 +53,18 @@ public void addAllWordsWithWeight(List<WeightedString> weightedStrings) {
5453
public List<AutoCompleteItem> getAutoCompleteWord(String keyword, int start, int end) {
5554
Set<String> results = redisTemplate.opsForZSet().reverseRange(INDEX_KEY + keyword, start, end);
5655
List<String> resultList = new ArrayList<>(results);
57-
return resultList.stream().map(name ->{
58-
Long id = Long.valueOf(redisTemplate.opsForValue().get(CONCERT_ID_KEY + name));
59-
return new AutoCompleteItem(name,id);
56+
return resultList.stream().map(id ->{
57+
String name = redisTemplate.opsForValue().get(CONCERT_ID_KEY + id);
58+
return new AutoCompleteItem(name,Long.valueOf(id));
6059
}).toList();
6160
}
6261

6362
public void deleteAutoCompleteWords() {
6463
Set<String> keys = redisTemplate.keys("index:*");
65-
Set<String> datas = redisTemplate.keys("data:*");
66-
if (keys != null || !keys.isEmpty()) redisTemplate.delete(keys);
67-
if (datas != null || !keys.isEmpty()) redisTemplate.delete(datas);
64+
Set<String> concertIdKeys = redisTemplate.keys("concertId:*");
65+
redisTemplate.delete(keys);
66+
redisTemplate.delete(concertIdKeys);
67+
log.info("자동 검색 키워드 삭제: " + keys.size() + "개의 키워드, " + concertIdKeys.size() + "개의 제목이 삭제되었습니다.");
6868
}
6969

7070
public Long getConcertIdByName(String concertName) {

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java

Lines changed: 89 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public class KopisApiService {
5353

5454
private final ConcertRedisRepository concertRedisRepository;
5555

56+
private final ConcertService concertService;
57+
5658
private final ArtistRepository artistRepository;
5759

5860
private final ConcertArtistRepository concertArtistRepository;
@@ -73,7 +75,8 @@ public KopisApiService(
7375
ConcertRedisRepository concertRedisRepository,
7476
ConcertKopisApiLogService kopisApiLogService,
7577
ArtistRepository artistRepository,
76-
ConcertArtistRepository concertArtistRepository
78+
ConcertArtistRepository concertArtistRepository,
79+
ConcertService concertService
7780
) {
7881
this.concertRepository = concertRepository;
7982
this.placeRepository = placeRepository;
@@ -83,6 +86,7 @@ public KopisApiService(
8386
this.concertKopisApiLogService = kopisApiLogService;
8487
this.artistRepository = artistRepository;
8588
this.concertArtistRepository = concertArtistRepository;
89+
this.concertService = concertService;
8690
this.restClient = RestClient.builder()
8791
.baseUrl("https://kopis.or.kr/openApi/restful")
8892
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
@@ -136,6 +140,7 @@ public void setConcertsList() throws InterruptedException {
136140
// 한국어 실명 기만 artistMap 가져오기
137141
Map<String, Artist> artistMap = getKoNameArtistMap();
138142

143+
// 개별 공연 항목에 대한 상세 데이터 저장
139144
for(int i = index.intValue(); i < totalConcertsList.size(); i++) {
140145
ConcertListElement concertListElement = totalConcertsList.get(i);
141146
// API에서 공연 상세 가져오기
@@ -167,7 +172,10 @@ public void setConcertsList() throws InterruptedException {
167172
long endNs = System.currentTimeMillis();
168173
long durationSec = ((endNs - startNs) / 1000);
169174
log.info(durationSec/60 + "분, " + durationSec % 60 + "초 소요되었습니다." );
175+
// 캐시 초기화
170176
cacheClear();
177+
// 자동 완성 검색어 재설정
178+
resetSearchKeyword();
171179
} catch (Exception e) {
172180
log.error("개별 공연 세부 내용 저장 도중 오류 발생");
173181
log.error("오류 내용 : " + e.getMessage());
@@ -195,73 +203,74 @@ public SetResultResponse updateConcertData() throws InterruptedException {
195203
LocalDate edate = LocalDate.now().plusYears(1);
196204

197205
SetResultResponse setResultResponse = new SetResultResponse();
198-
199-
ConcertListResponse plr;
200-
List<ConcertListElement> totalConcertsList = new ArrayList<>();
201206
Map<String, ConcertPlace> concertPlaceMap = new HashMap<>();
202207

203-
int page = 1;
204-
while (true) {
205-
plr = getConcertListResponse(serviceKey, sdate, edate, page, lastUpdatedDate);
206-
page++;
207-
if (plr.getConcertList() == null) break;
208-
for (ConcertListElement p : plr.getConcertList()) {
209-
totalConcertsList.add(p);
210-
}
211-
Thread.sleep(200);
212-
}
213-
log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장");
208+
try {
209+
// API에서 공연 목록 가져오기
210+
List<ConcertListElement> totalConcertsList = getConcertListElements(sdate, edate, lastUpdatedDate);
211+
log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장");
214212

215-
// artist map 가져오기
216-
Map<String ,Artist> artistMap = getKoNameArtistMap();
217-
for (ConcertListElement performanceListElement : totalConcertsList) {
218-
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId());
219-
ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail();
220-
log.info("concert detail: " + concertDetailResponse.getConcertDetail());
213+
// artist map 가져오기
214+
Map<String ,Artist> artistMap = getKoNameArtistMap();
221215

222-
// 콘서트 위치 탐색 또는 추가
223-
String concertPlaceAPiKey = concertDetailResponse.getConcertDetail().getMt10id();
224-
ConcertPlace concertPlace = getConcertPlaceOrSaveNewConcertPlace(concertPlaceMap, concertPlaceAPiKey);
216+
// 개별 공연 항목에 대한 상세 데이터 저장
217+
for (ConcertListElement performanceListElement : totalConcertsList) {
218+
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId());
219+
ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail();
220+
log.info("concert detail: " + concertDetailResponse.getConcertDetail());
225221

226-
// 콘서트 저장
227-
Concert concert = concertRepository.getConcertByApiConcertId(concertDetail.getApiConcertId());
222+
// 콘서트 위치 탐색 또는 추가
223+
String concertPlaceAPiKey = concertDetailResponse.getConcertDetail().getMt10id();
224+
ConcertPlace concertPlace = getConcertPlaceOrSaveNewConcertPlace(concertPlaceMap, concertPlaceAPiKey);
228225

229-
if (concert == null) {
230-
// 새 공연 저장
231-
Concert savedConcert = saveConcert(concertPlace,concertDetail);
232-
// 공연 예매처 저장
233-
setResultResponse.addedTicketOfficeAccumulator(saveConcertTicketOffice(concertDetail, savedConcert));
234-
// 공연 이미지 저장
235-
setResultResponse.addedConcertImagesAccumulator(saveConcertImages(concertDetail, savedConcert));
236-
// 공연 아티스트 저장
237-
setConcertArtist(artistMap,concertDetail,savedConcert);
238-
setResultResponse.addConcerts();
239-
} else {
240-
// 공연 데이터 갱신 후 저장
241-
Concert savedConcert = updateConcert(concert, concertPlace, concertDetail);
242-
// 기존에 저장되어 있던 연관 테이블 데이터 삭제
243-
ticketOfficeRepository.deleteByConcertId(savedConcert.getConcertId());
244-
imageRepository.deleteByConcertId(savedConcert.getConcertId());
245-
// 갱신된 데이터로 연관 테이블 저장
246-
setResultResponse.updatedTicketOfficesAccumulator(saveConcertTicketOffice(concertDetail, savedConcert));
247-
setResultResponse.updatedConcertImagesAccumulator(saveConcertImages(concertDetail, savedConcert));
248-
setResultResponse.addUpdatedConcerts();
226+
// 콘서트 저장
227+
Concert concert = concertRepository.getConcertByApiConcertId(concertDetail.getApiConcertId());
228+
229+
if (concert == null) {
230+
// 새 공연 저장
231+
Concert savedConcert = saveConcert(concertPlace,concertDetail);
232+
// 공연 예매처 저장
233+
setResultResponse.addedTicketOfficeAccumulator(saveConcertTicketOffice(concertDetail, savedConcert));
234+
// 공연 이미지 저장
235+
setResultResponse.addedConcertImagesAccumulator(saveConcertImages(concertDetail, savedConcert));
236+
// 공연 아티스트 저장
237+
setConcertArtist(artistMap,concertDetail,savedConcert);
238+
setResultResponse.addConcerts();
239+
} else {
240+
// 공연 데이터 갱신 후 저장
241+
Concert savedConcert = updateConcert(concert, concertPlace, concertDetail);
242+
// 기존에 저장되어 있던 연관 테이블 데이터 삭제
243+
ticketOfficeRepository.deleteByConcertId(savedConcert.getConcertId());
244+
imageRepository.deleteByConcertId(savedConcert.getConcertId());
245+
// 갱신된 데이터로 연관 테이블 저장
246+
setResultResponse.updatedTicketOfficesAccumulator(saveConcertTicketOffice(concertDetail, savedConcert));
247+
setResultResponse.updatedConcertImagesAccumulator(saveConcertImages(concertDetail, savedConcert));
248+
setResultResponse.addUpdatedConcerts();
249+
}
250+
251+
Thread.sleep(300);
249252
}
250253

251-
Thread.sleep(300);
254+
// 갱신 후 업데이트 시간 저장
255+
concertKopisApiLogService.saveSuccessLog("save","공연 데이터 업데이트 완료",0L);
256+
// 락 해제
257+
concertRedisRepository.unlockSave(key);
258+
// 이전 캐시 데이터 삭제
259+
cacheClear();
260+
// 자동 완성 검색어 재설정
261+
resetSearchKeyword();
262+
return setResultResponse;
263+
} catch (Exception e) {
264+
log.error("공연 정보 갱신 도중 오류 발생 : " +e.getMessage());
265+
concertKopisApiLogService.saveErrorLog("save",e,0L);
266+
} finally {
267+
// 락 해제
268+
concertRedisRepository.unlockSave(key);
252269
}
253-
254-
// 갱신 후 업데이트 시간 저장
255-
concertKopisApiLogService.saveSuccessLog("save","공연 데이터 업데이트 완료",0L);
256-
// 락 해제
257-
concertRedisRepository.unlockSave(key);
258-
// 이전 캐시 데이터 삭제
259-
cacheClear();
260270
return setResultResponse;
261271
}
262272

263-
264-
273+
// concertId의 공연만 API에서 갱신
265274
@Transactional
266275
public void concertUpdateByKopisApi(Long concertId){
267276
// 해당 콘서트 ID로 콘서트 객체 찾기
@@ -290,11 +299,10 @@ public void concertUpdateByKopisApi(Long concertId){
290299
// 새로 받아온 예매처, 이미지 데이터 저장
291300
saveConcertTicketOffice(concertDetail, savedConcert);
292301
saveConcertImages(concertDetail, savedConcert);
293-
294-
295302
}
296303

297304

305+
// API에서 모든 공연 목록 가져오기
298306
private List<ConcertListElement> getAllConcertsListFromKopisAPI(int page) throws InterruptedException {
299307
List<ConcertListElement> totalConcertsList = new ArrayList<>();
300308
while (true) {
@@ -311,6 +319,23 @@ private List<ConcertListElement> getAllConcertsListFromKopisAPI(int page) throws
311319
return totalConcertsList;
312320
}
313321

322+
// API에서 이전 업데이트 날짜 이후로 변경된 공연 목록 가져오기
323+
private List<ConcertListElement> getConcertListElements(LocalDate sdate, LocalDate edate, LocalDate lastUpdatedDate) throws InterruptedException {
324+
List<ConcertListElement> totalConcertsList = new ArrayList<>();
325+
int page = 1;
326+
while (true) {
327+
ConcertListResponse plr = getConcertListResponse(serviceKey, sdate, edate, page, lastUpdatedDate);
328+
page++;
329+
if (plr.getConcertList() == null) break;
330+
for (ConcertListElement p : plr.getConcertList()) {
331+
totalConcertsList.add(p);
332+
}
333+
Thread.sleep(120);
334+
}
335+
return totalConcertsList;
336+
}
337+
338+
314339
// 공연 데이터 저장
315340
private Concert saveConcert(ConcertPlace concertPlace, ConcertDetailElement concertDetail) {
316341
TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice());
@@ -427,6 +452,12 @@ private void cacheClear() {
427452
concertRedisRepository.deleteTotalConcertsCount(ListSort.VIEW);
428453
}
429454

455+
// 캐시에 저장된 자동완성 단어들을 삭제하고 다시 설정합니다.
456+
private void resetSearchKeyword() {
457+
concertService.resetAutoComplete();
458+
concertService.setAutoComplete();
459+
}
460+
430461
public ConcertListResponse getConcertsList() {
431462
return getConcertListResponse(serviceKey, sdate, edate, 1);
432463
}

src/main/java/com/back/web7_9_codecrete_be/global/scheduler/ConcertScheduler.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@ public void concertUpdateSchedule() throws InterruptedException {
2424
}
2525

2626
// 공연 관련 정보를 갱신합니다.
27-
@Scheduled(cron = "0 0 3 * * *")
27+
@Scheduled(cron = "0 10 3 * * *")
2828
public void concertDataUpdateSchedule() {
2929
concertService.viewCountUpdate();
30-
concertService.resetAutoComplete();
31-
concertService.setAutoComplete();
3230
}
3331

3432
// 이메일 알림을 전송합니다.

0 commit comments

Comments
 (0)