Skip to content

Commit 8183a69

Browse files
authored
Merge pull request #146 from prgrms-web-devcourse-final-project/feat/#142
[Concert] 최초 데이터 로드 비동기 처리 + redis 락 통해서 중복 실행 방지
2 parents a8b4317 + 1633099 commit 8183a69

3 files changed

Lines changed: 127 additions & 59 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.swagger.v3.oas.annotations.tags.Tag;
1515
import lombok.RequiredArgsConstructor;
1616
import org.springframework.data.domain.Pageable;
17+
import org.springframework.http.HttpStatus;
1718
import org.springframework.web.bind.annotation.*;
1819

1920
import java.util.List;
@@ -30,8 +31,9 @@ public class ConcertAdminController { // todo : 인증 권한 추가하기
3031

3132
@Operation(summary = "초기 공연 정보 저장", description = "25년 12월부터 앞으로 6개월 이후까지의 전체 공연의 정보를 가져와서 저장합니다. 대략 10~12분 정도 시간이 소요됩니다.")
3233
@PostMapping("setConcertData")
33-
public RsData<SetResultResponse> setConcert() throws InterruptedException {
34-
return RsData.success(kopisApiService.setConcertsList());
34+
public RsData<Void> setConcert() throws InterruptedException {
35+
kopisApiService.setConcertsList();
36+
return RsData.success(HttpStatus.ACCEPTED,"저장 요청을 보냈습니다",null);
3537
}
3638

3739
@Operation(summary = "공연 정보 갱신", description = "공연 정보를 직접 갱신합니다.")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.back.web7_9_codecrete_be.domain.concerts.repository;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.data.redis.core.RedisTemplate;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.concurrent.TimeUnit;
8+
9+
@Repository
10+
@RequiredArgsConstructor
11+
public class ConcertRedisRepository {
12+
private final RedisTemplate<String,String> redisTemplate;
13+
14+
private static final String LOCK_FLAG_PREFIX = "initLoad: ";
15+
16+
public void lockSave(String key, String value) {
17+
redisTemplate.opsForValue().set(
18+
LOCK_FLAG_PREFIX + key,
19+
value,
20+
900,
21+
TimeUnit.SECONDS);
22+
}
23+
24+
public String lockGet(String key) {
25+
return redisTemplate.opsForValue().get(LOCK_FLAG_PREFIX + key);
26+
}
27+
28+
public void unlockSave(String key) {
29+
redisTemplate.delete(LOCK_FLAG_PREFIX + key);
30+
}
31+
32+
}

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

Lines changed: 91 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import org.springframework.beans.factory.annotation.Value;
1313
import org.springframework.http.HttpHeaders;
1414
import org.springframework.http.MediaType;
15+
import org.springframework.scheduling.annotation.Async;
16+
import org.springframework.scheduling.annotation.EnableAsync;
1517
import org.springframework.scheduling.annotation.EnableScheduling;
1618
import org.springframework.scheduling.annotation.Scheduled;
1719
import org.springframework.stereotype.Service;
@@ -28,6 +30,7 @@
2830

2931
@Slf4j
3032
@Service
33+
@EnableAsync
3134
@EnableScheduling
3235
public class KopisApiService {
3336
// 공연예술통합 전산망 조회를 위한 서비스 클래스입니다.
@@ -41,28 +44,46 @@ public class KopisApiService {
4144

4245
private final ConcertUpdateTimeRepository concertUpdateTimeRepository;
4346

47+
private final ConcertRedisRepository concertRedisRepository;
48+
4449
@Value("${kopis.api-key}")
4550
private String serviceKey;
4651
private LocalDate sdate = LocalDate.of(2025, 12, 1);
47-
private LocalDate edate = LocalDate.now().plusMonths(6);
52+
private LocalDate edate = LocalDate.now().plusYears(1);
4853

4954
private final RestClient restClient;
5055

51-
public KopisApiService(ConcertRepository concertRepository, ConcertPlaceRepository placeRepository, TicketOfficeRepository ticketOfficeRepository, ConcertImageRepository imageRepository, ConcertUpdateTimeRepository concertUpdateTimeRepository) {
56+
public KopisApiService(ConcertRepository concertRepository, ConcertPlaceRepository placeRepository, TicketOfficeRepository ticketOfficeRepository, ConcertImageRepository imageRepository, ConcertUpdateTimeRepository concertUpdateTimeRepository,ConcertRedisRepository concertRedisRepository) {
5257
this.concertRepository = concertRepository;
5358
this.placeRepository = placeRepository;
5459
this.ticketOfficeRepository = ticketOfficeRepository;
5560
this.imageRepository = imageRepository;
5661
this.concertUpdateTimeRepository = concertUpdateTimeRepository;
62+
this.concertRedisRepository = concertRedisRepository;
5763
this.restClient = RestClient.builder()
5864
.baseUrl("https://kopis.or.kr/openApi/restful")
5965
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
6066
.build();
6167
}
6268

69+
@Async
6370
@Transactional
64-
public SetResultResponse setConcertsList() throws InterruptedException {
71+
public void setConcertsList() throws InterruptedException {
6572
// 최초 시작 시간 저장
73+
if(concertUpdateTimeRepository.count() != 0) {
74+
log.error("이미 최초 저장이 되었습니다!. UpdateConcert를 통해 데이터를 갱신해주십시오!");
75+
return;
76+
}
77+
String key = "init";
78+
79+
String value = concertRedisRepository.lockGet(key);
80+
if(value != null) {
81+
log.error("이미 실행중인 스레드입니다.");
82+
return;
83+
} else {
84+
concertRedisRepository.lockSave(key,"running...");
85+
}
86+
6687
LocalDateTime now = LocalDateTime.now();
6788
Long startNs = System.currentTimeMillis();
6889

@@ -79,74 +100,87 @@ public SetResultResponse setConcertsList() throws InterruptedException {
79100
int addedConcertImages = 0;
80101

81102
int page = 1;
82-
while (true) {
83-
// 콘서트 목록 받아오기
84-
plr = getConcertListResponse(serviceKey, sdate, edate, page);
85-
page++;
86-
// 더 이상 받아올 콘서트 목록이 없으면 멈춤
87-
if (plr.getConcertList() == null) break;
88-
// 콘서트 요소를 콘서트 목록에서 꺼내서 더하기
89-
for (ConcertListElement p : plr.getConcertList()) {
90-
totalConcertsList.add(p);
103+
try{
104+
while (true) {
105+
// 콘서트 목록 받아오기
106+
plr = getConcertListResponse(serviceKey, sdate, edate, page);
107+
page++;
108+
// 더 이상 받아올 콘서트 목록이 없으면 멈춤
109+
if (plr.getConcertList() == null) break;
110+
// 콘서트 요소를 콘서트 목록에서 꺼내서 더하기
111+
for (ConcertListElement p : plr.getConcertList()) {
112+
totalConcertsList.add(p);
113+
}
114+
log.info("Total Concert List: {}", totalConcertsList.size() + "개의 데이터 가져오는중...");
115+
Thread.sleep(200);
91116
}
92-
log.info("Total Concert List: {}", totalConcertsList.size() + "개의 데이터 가져오는중...");
93-
Thread.sleep(200);
117+
118+
}catch (Exception e){
119+
log.error("공연 목록 저장 도중 오류 발생");
120+
log.error("오류 내용 : " + e.getMessage());
121+
return;
94122
}
95123

96-
log.info("Total concert list size: {}", totalConcertsList.size());
97-
log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장");
98-
for (ConcertListElement performanceListElement : totalConcertsList) {
99-
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId());
100-
ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail();
101124

102-
// 콘서트 위치 저장
103-
// 콘서트 상세에서 저장할 콘서트 위치의 API ID 값 가져오기
104-
String concertPlaceAPiKey = concertDetailResponse.getConcertDetail().getMt10id();
105-
// 캐시로 사용하는 맵이나 DB에서 콘서트 위치가 있는지 확인하기
106-
ConcertPlace concertPlace = concertPlaceMap.getOrDefault(concertPlaceAPiKey, placeRepository.getConcertPlaceByApiConcertPlaceId(concertPlaceAPiKey));
107-
if (concertPlace == null) {
108-
// 맵이나 DB에 없다면 API에서 해당 콘서트 위치를 가져와서 DB에 저장 후 캐시에 저장
109-
ConcertPlaceDetailResponse concertPlaceDetailElement = getConcertPlaceDetailResponse(serviceKey, concertPlaceAPiKey);
110-
ConcertPlaceDetailElement concertPlaceDetail = concertPlaceDetailElement.getConcertPlaceDetail();
111-
concertPlace = concertPlaceDetail.getConcertPlace();
112-
ConcertPlace savedConcertPlace = placeRepository.save(concertPlace);
113-
concertPlaceMap.put(concertPlaceAPiKey, savedConcertPlace);
114-
addedConcertPlaces++;
115-
}
125+
log.info("저장할 총 공연의 수: {}", totalConcertsList.size());
126+
log.info("공연 목록 로드 완료, 공연 세부 내용 로드 및 저장");
127+
try {
128+
for (ConcertListElement performanceListElement : totalConcertsList) {
129+
ConcertDetailResponse concertDetailResponse = getConcertDetailResponse(serviceKey, performanceListElement.getApiConcertId());
130+
ConcertDetailElement concertDetail = concertDetailResponse.getConcertDetail();
131+
132+
// 콘서트 위치 저장
133+
// 콘서트 상세에서 저장할 콘서트 위치의 API ID 값 가져오기
134+
String concertPlaceAPiKey = concertDetailResponse.getConcertDetail().getMt10id();
135+
// 캐시로 사용하는 맵이나 DB에서 콘서트 위치가 있는지 확인하기
136+
ConcertPlace concertPlace = concertPlaceMap.getOrDefault(concertPlaceAPiKey, placeRepository.getConcertPlaceByApiConcertPlaceId(concertPlaceAPiKey));
137+
if (concertPlace == null) {
138+
// 맵이나 DB에 없다면 API에서 해당 콘서트 위치를 가져와서 DB에 저장 후 캐시에 저장
139+
ConcertPlaceDetailResponse concertPlaceDetailElement = getConcertPlaceDetailResponse(serviceKey, concertPlaceAPiKey);
140+
ConcertPlaceDetailElement concertPlaceDetail = concertPlaceDetailElement.getConcertPlaceDetail();
141+
concertPlace = concertPlaceDetail.getConcertPlace();
142+
ConcertPlace savedConcertPlace = placeRepository.save(concertPlace);
143+
concertPlaceMap.put(concertPlaceAPiKey, savedConcertPlace);
144+
addedConcertPlaces++;
145+
}
116146

117-
//콘서트 최고 금액, 최저 금액 처리.
118-
TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice());
147+
//콘서트 최고 금액, 최저 금액 처리.
148+
TicketPrice ticketPrice = new TicketPrice(concertDetail.getConcertPrice());
119149

120-
// 콘서트 저장
121-
Concert concert = new Concert(
122-
concertPlace,
123-
concertDetail.getConcertName(),
124-
concertDetail.getConcertDescription(),
125-
dateStringToDateTime(concertDetail.getStartDate()),
126-
dateStringToDateTime(concertDetail.getEndDate()),
127-
null,
128-
ticketPrice.maxPrice,
129-
ticketPrice.minPrice,
130-
concertDetail.getPosterUrl(),
131-
concertDetail.getApiConcertId()
132-
);
150+
// 콘서트 저장
151+
Concert concert = new Concert(
152+
concertPlace,
153+
concertDetail.getConcertName(),
154+
concertDetail.getConcertDescription(),
155+
dateStringToDateTime(concertDetail.getStartDate()),
156+
dateStringToDateTime(concertDetail.getEndDate()),
157+
null,
158+
ticketPrice.maxPrice,
159+
ticketPrice.minPrice,
160+
concertDetail.getPosterUrl(),
161+
concertDetail.getApiConcertId()
162+
);
133163

134-
Concert savedConcert = concertRepository.save(concert);
135-
addedConcerts++;
164+
Concert savedConcert = concertRepository.save(concert);
165+
addedConcerts++;
136166

137-
addedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert);
138-
addedConcertImages += saveConcertImages(concertDetail, savedConcert);
167+
addedTicketOffices += saveConcertTicketOffice(concertDetail, savedConcert);
168+
addedConcertImages += saveConcertImages(concertDetail, savedConcert);
139169

140-
Thread.sleep(300);
170+
Thread.sleep(300);
171+
}
172+
} catch (Exception e) {
173+
log.error("개별 공연 세부 내용 저장 도중 오류 발생");
174+
log.error("오류 내용 : " + e.getMessage());
175+
return ;
141176
}
142-
143177
ConcertUpdateTime concertUpdateTime = new ConcertUpdateTime(now);
144178
concertUpdateTimeRepository.save(concertUpdateTime);
145179
log.info(now + "시 기준 " + totalConcertsList.size() + "개의 공연 데이터 저장 완료!");
146180
long endNs = System.currentTimeMillis();
147181
long durationSec = ((endNs - startNs) / 1000);
148182
log.info(durationSec/60 + "분, " + durationSec % 60 + "초 소요되었습니다." );
149-
return new SetResultResponse(addedConcerts,0,addedConcertPlaces,0,addedConcertImages,0,addedTicketOffices,0);
183+
concertRedisRepository.unlockSave(key);
150184
}
151185

152186

@@ -157,8 +191,8 @@ public SetResultResponse updateConcertData() throws InterruptedException {
157191
ConcertUpdateTime concertUpdateTime = concertUpdateTimeRepository.getReferenceById(1L);
158192
LocalDate lastUpdatedDate = concertUpdateTime.getUpdateTime().toLocalDate();
159193
ConcertUpdateTime updatedTime = concertUpdateTime.setUpdateTime(LocalDateTime.now());
160-
LocalDate sdate = lastUpdatedDate.plusDays(1);
161-
LocalDate edate = LocalDate.now().withDayOfMonth(1).plusMonths(6);
194+
LocalDate sdate = lastUpdatedDate;
195+
LocalDate edate = LocalDate.now().plusYears(1);
162196

163197
int addedConcerts = 0;
164198
int updatedConcerts = 0;

0 commit comments

Comments
 (0)