Skip to content

Commit bc2c847

Browse files
authored
Merge d027700 into 9a3563f
2 parents 9a3563f + d027700 commit bc2c847

39 files changed

+1681
-518
lines changed

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ dependencies {
4040
testAnnotationProcessor 'org.projectlombok:lombok'
4141
testImplementation 'org.springframework.boot:spring-boot-starter-test'
4242
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
43-
43+
// 인메모리 캐시
44+
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.6'
4445
// jwt
4546
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
4647
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
@@ -78,6 +79,7 @@ dependencies {
7879
// db migration
7980
implementation 'org.flywaydb:flyway-core'
8081
implementation 'org.flywaydb:flyway-mysql'
82+
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
8183

8284
}
8385

src/main/java/sevenstar/marineleisure/MarineLeisureApplication.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.boot.context.properties.EnableConfigurationProperties;
6-
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6+
import org.springframework.cache.annotation.EnableCaching;
7+
import org.springframework.scheduling.annotation.EnableAsync;
78

89
import sevenstar.marineleisure.global.api.config.properties.KhoaProperties;
910
import sevenstar.marineleisure.global.api.config.properties.OpenMeteoProperties;
10-
import sevenstar.marineleisure.global.api.config.properties.OpenMeteoProperties;
1111

1212
@SpringBootApplication
1313
@EnableConfigurationProperties({KhoaProperties.class, OpenMeteoProperties.class})
14+
@EnableAsync
15+
@EnableCaching
1416
public class MarineLeisureApplication {
1517

1618
public static void main(String[] args) {

src/main/java/sevenstar/marineleisure/global/api/kakao/service/PresetSchedulerService.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import java.time.LocalDate;
44

5+
import org.springframework.cache.annotation.CacheEvict;
56
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Transactional;
68

79
import lombok.RequiredArgsConstructor;
810
import sevenstar.marineleisure.global.enums.Region;
@@ -18,10 +20,12 @@ public class PresetSchedulerService {
1820
private final OutdoorSpotRepository outdoorSpotRepository;
1921
private final SpotPresetRepository spotPresetRepository;
2022

23+
@Transactional
2124
public void updateRegionApi() {
2225
LocalDate now = LocalDate.now();
2326
BestSpot emptySpot = new BestSpot(-1L, "없는 지역입니다", TotalIndex.NONE);
2427
for (Region region : Region.getAllKoreaRegion()) {
28+
evictRegionCache(region);
2529
BestSpot bestSpotInFishing = outdoorSpotRepository.findBestSpotInFishing(region.getLatitude(),
2630
region.getLongitude(), now, PRESET_RADIUS).map(BestSpot::new).orElse(emptySpot);
2731
BestSpot bestSpotInMudflat = outdoorSpotRepository.findBestSpotInMudflat(region.getLatitude(),
@@ -38,4 +42,9 @@ public void updateRegionApi() {
3842
bestSpotInSurfing.getTotalIndex().name());
3943
}
4044
}
45+
46+
@CacheEvict(value = "spotPresetPreviews", key = "#region.name()")
47+
public void evictRegionCache(Region region) {
48+
// 아무 동작 없음
49+
}
4150
}
Lines changed: 5 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,24 @@
11
package sevenstar.marineleisure.global.api.openmeteo.dto.service;
22

33
import java.time.LocalDate;
4-
import java.time.LocalDateTime;
4+
import java.util.List;
55

6-
import org.springframework.core.ParameterizedTypeReference;
7-
import org.springframework.http.ResponseEntity;
86
import org.springframework.stereotype.Service;
97
import org.springframework.transaction.annotation.Transactional;
108

119
import lombok.RequiredArgsConstructor;
12-
import sevenstar.marineleisure.forecast.repository.FishingRepository;
13-
import sevenstar.marineleisure.forecast.repository.MudflatRepository;
14-
import sevenstar.marineleisure.forecast.repository.ScubaRepository;
15-
import sevenstar.marineleisure.forecast.repository.SurfingRepository;
16-
import sevenstar.marineleisure.global.api.openmeteo.OpenMeteoApiClient;
17-
import sevenstar.marineleisure.global.api.openmeteo.dto.common.OpenMeteoReadResponse;
18-
import sevenstar.marineleisure.global.api.openmeteo.dto.item.SunTimeItem;
19-
import sevenstar.marineleisure.global.api.openmeteo.dto.item.UvIndexItem;
20-
import sevenstar.marineleisure.spot.domain.OutdoorSpot;
21-
import sevenstar.marineleisure.spot.repository.OutdoorSpotRepository;
10+
import sevenstar.marineleisure.spot.dto.detail.provider.ActivityProvider;
2211

2312
@Service
2413
@RequiredArgsConstructor
25-
@Transactional(readOnly = true)
2614
public class OpenMeteoService {
27-
private final OpenMeteoApiClient openMeteoApiClient;
28-
private final OutdoorSpotRepository outdoorSpotRepository;
29-
private final FishingRepository fishingRepository;
30-
private final MudflatRepository mudflatRepository;
31-
private final ScubaRepository scubaRepository;
32-
private final SurfingRepository surfingRepository;
15+
private final List<ActivityProvider> providers;
3316

34-
// TODO : exception , refactoring
3517
@Transactional
3618
public void updateApi(LocalDate startDate, LocalDate endDate) {
37-
// update fishing uvIndex
38-
for (Long spotId : fishingRepository.findByForecastDateBetween(startDate, endDate)) {
39-
OutdoorSpot outdoorSpot = outdoorSpotRepository.findById(spotId).orElseThrow();
40-
UvIndexItem uvIndex = getUvIndex(startDate, endDate, outdoorSpot.getLatitude().doubleValue(),
41-
outdoorSpot.getLongitude().doubleValue());
42-
for (int i = 0; i < uvIndex.getTime().size(); i++) {
43-
Float uvIndexValue = uvIndex.getUvIndexMax().get(i);
44-
LocalDate date = uvIndex.getTime().get(i);
45-
fishingRepository.updateUvIndex(uvIndexValue, spotId, date);
46-
}
19+
for (ActivityProvider provider : providers) {
20+
provider.update(startDate, endDate);
4721
}
48-
49-
// update mudflat uvIndex
50-
for (Long spotId : mudflatRepository.findByForecastDateBetween(startDate, endDate)) {
51-
OutdoorSpot outdoorSpot = outdoorSpotRepository.findById(spotId).orElseThrow();
52-
UvIndexItem uvIndex = getUvIndex(startDate, endDate, outdoorSpot.getLatitude().doubleValue(),
53-
outdoorSpot.getLongitude().doubleValue());
54-
for (int i = 0; i < uvIndex.getTime().size(); i++) {
55-
Float uvIndexValue = uvIndex.getUvIndexMax().get(i);
56-
LocalDate date = uvIndex.getTime().get(i);
57-
mudflatRepository.updateUvIndex(uvIndexValue, spotId, date);
58-
}
59-
}
60-
61-
// update scuba sunrise and sunset
62-
for (Long spotId : scubaRepository.findByForecastDateBetween(startDate, endDate)) {
63-
OutdoorSpot outdoorSpot = outdoorSpotRepository.findById(spotId).orElseThrow();
64-
SunTimeItem sunTimeItem = getSunTimes(startDate, endDate, outdoorSpot.getLatitude().doubleValue(),
65-
outdoorSpot.getLongitude().doubleValue());
66-
for (int i = 0; i < sunTimeItem.getTime().size(); i++) {
67-
LocalDateTime sunrise = sunTimeItem.getSunrise().get(i);
68-
LocalDateTime sunset = sunTimeItem.getSunset().get(i);
69-
LocalDate date = sunTimeItem.getTime().get(i);
70-
scubaRepository.updateSunriseAndSunset(sunrise.toLocalTime(), sunset.toLocalTime(), spotId, date);
71-
}
72-
}
73-
74-
// update surfing uvIndex
75-
for (Long spotId : surfingRepository.findByForecastDateBetween(startDate, endDate)) {
76-
OutdoorSpot outdoorSpot = outdoorSpotRepository.findById(spotId).orElseThrow();
77-
UvIndexItem uvIndex = getUvIndex(startDate, endDate, outdoorSpot.getLatitude().doubleValue(),
78-
outdoorSpot.getLongitude().doubleValue());
79-
for (int i = 0; i < uvIndex.getTime().size(); i++) {
80-
Float uvIndexValue = uvIndex.getUvIndexMax().get(i);
81-
LocalDate date = uvIndex.getTime().get(i);
82-
surfingRepository.updateUvIndex(uvIndexValue, spotId, date);
83-
}
84-
}
85-
8622
}
8723

88-
private SunTimeItem getSunTimes(LocalDate startDate, LocalDate endDate, double latitude, double longitude) {
89-
ResponseEntity<OpenMeteoReadResponse<SunTimeItem>> response = openMeteoApiClient.getSunTimes(
90-
new ParameterizedTypeReference<>() {
91-
}, startDate, endDate, latitude, longitude);
92-
return response.getBody().getDaily();
93-
}
94-
95-
private UvIndexItem getUvIndex(LocalDate startDate, LocalDate endDate, double latitude, double longitude) {
96-
ResponseEntity<OpenMeteoReadResponse<UvIndexItem>> response = openMeteoApiClient.getUvIndex(
97-
new ParameterizedTypeReference<>() {
98-
}, startDate, endDate, latitude, longitude);
99-
return response.getBody().getDaily();
100-
}
10124
}
Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package sevenstar.marineleisure.global.api.scheduler;
22

33
import java.time.LocalDate;
4+
import java.util.concurrent.CompletableFuture;
5+
import java.util.concurrent.Executor;
46

57
import org.springframework.scheduling.annotation.Scheduled;
68
import org.springframework.stereotype.Service;
7-
import org.springframework.transaction.annotation.Transactional;
89

910
import lombok.RequiredArgsConstructor;
1011
import lombok.extern.slf4j.Slf4j;
@@ -14,29 +15,59 @@
1415
import sevenstar.marineleisure.spot.repository.SpotViewQuartileRepository;
1516

1617
@Service
17-
@RequiredArgsConstructor
18-
@Transactional(readOnly = true)
1918
@Slf4j
19+
@RequiredArgsConstructor
2020
public class SchedulerService {
2121
public static final int MAX_UPDATE_DAY = 3;
2222
private final KhoaApiService khoaApiService;
2323
private final OpenMeteoService openMeteoService;
2424
private final PresetSchedulerService presetSchedulerService;
2525
private final SpotViewQuartileRepository spotViewQuartileRepository;
2626

27+
28+
private final Executor taskExecutor;
29+
30+
// public SchedulerService(
31+
// KhoaApiService khoaApiService,
32+
// OpenMeteoService openMeteoService,
33+
// PresetSchedulerService presetSchedulerService,
34+
// SpotViewQuartileRepository spotViewQuartileRepository,
35+
// @Qualifier("applicationTaskExecutor") Executor taskExecutor // ★ 여기
36+
// ) {
37+
// this.khoaApiService = khoaApiService;
38+
// this.openMeteoService = openMeteoService;
39+
// this.presetSchedulerService = presetSchedulerService;
40+
// this.spotViewQuartileRepository = spotViewQuartileRepository;
41+
// this.taskExecutor = taskExecutor;
42+
// }
2743
/**
2844
* 앞으로의 스케줄링 전략에 의해 수정될 부분입니다.
2945
* @author guwnoong
3046
*/
3147
@Scheduled(initialDelay = 0, fixedDelay = 86400000)
32-
@Transactional
3348
public void scheduler() {
3449
LocalDate today = LocalDate.now();
3550
LocalDate endDate = today.plusDays(MAX_UPDATE_DAY);
51+
52+
// 1. khoaApiService 먼저 실행 (순차적)
3653
khoaApiService.updateApi(today, endDate);
37-
openMeteoService.updateApi(today, endDate);
38-
presetSchedulerService.updateRegionApi();
39-
spotViewQuartileRepository.upsertQuartile();
54+
55+
// 2. 나머지 작업들을 병렬로 실행
56+
CompletableFuture<Void> openMeteoFuture = CompletableFuture.runAsync(() -> {
57+
openMeteoService.updateApi(today, endDate);
58+
}, taskExecutor);
59+
60+
CompletableFuture<Void> presetSchedulerFuture = CompletableFuture.runAsync(() -> {
61+
presetSchedulerService.updateRegionApi();
62+
}, taskExecutor);
63+
64+
CompletableFuture<Void> spotViewQuartileFuture = CompletableFuture.runAsync(() -> {
65+
spotViewQuartileRepository.upsertQuartile();
66+
}, taskExecutor);
67+
68+
// 모든 병렬 작업이 완료될 때까지 기다림
69+
CompletableFuture.allOf(openMeteoFuture, presetSchedulerFuture, spotViewQuartileFuture).join();
70+
4071
log.info("=== update data ===");
4172
}
4273
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package sevenstar.marineleisure.global.config;
2+
3+
import java.util.concurrent.Executor;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.scheduling.annotation.EnableAsync;
7+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
8+
9+
@Configuration
10+
@EnableAsync
11+
public class AsyncConfig {
12+
13+
@Bean(name = "taskExecutor")
14+
public Executor taskExecutor() {
15+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
16+
executor.setCorePoolSize(2); // 기본 스레드 수
17+
executor.setMaxPoolSize(4); // 최대 스레드 수
18+
executor.setQueueCapacity(100); // 큐 용량
19+
executor.setThreadNamePrefix("Async-Task-With-Sevenball-");
20+
executor.initialize();
21+
return executor;
22+
}
23+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package sevenstar.marineleisure.global.util;
2+
3+
import java.security.MessageDigest;
4+
import java.security.SecureRandom;
5+
import java.util.Base64;
6+
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
public class PkceUtil {
11+
public String generateCodeVerifier() {
12+
SecureRandom random = new SecureRandom();
13+
byte[] bytes = new byte[64];
14+
random.nextBytes(bytes);
15+
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
16+
}
17+
18+
public String generateCodeChallenge(String codeVerifier) {
19+
try {
20+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
21+
byte[] bytes = digest.digest(codeVerifier.getBytes());
22+
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
23+
} catch (Exception e) {
24+
throw new RuntimeException("Failed to generate code challenge", e);
25+
}
26+
}
27+
28+
public boolean verifyCodeChallenge(String codeChallenge, String codeVerifier) {
29+
return false;
30+
}
31+
}

src/main/java/sevenstar/marineleisure/global/util/StateEncryptionUtil.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public String encryptState(String state) {
3939
}
4040
}
4141

42+
4243
/**
4344
* 암호화된 상태 값을 복호화.
4445
*
@@ -67,8 +68,7 @@ public String decryptState(String encryptedState) {
6768
*/
6869
public boolean validateState(String state, String encryptedState) {
6970
try {
70-
String decryptedState = decryptState(encryptedState);
71-
return decryptedState.equals(state);
71+
return decryptState(encryptedState).equals(state);
7272
} catch (Exception e) {
7373
return false;
7474
}
@@ -87,4 +87,4 @@ private SecretKeySpec generateKey(String key) throws NoSuchAlgorithmException {
8787
keyBytes = Arrays.copyOf(keyBytes, 16); // AES-128 키 길이
8888
return new SecretKeySpec(keyBytes, "AES");
8989
}
90-
}
90+
}

0 commit comments

Comments
 (0)