-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor/jira kan 72 환율 및 환전 리팩토링 #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "Refactor/JIRA-kan-72-\uD658\uC728-\uBC0F-\uD658\uC804-\uB9AC\uD329\uD1A0\uB9C1"
Changes from 10 commits
df5f2ca
4650f0b
5af3abd
a470c31
cde12a8
0bb9309
84b8b44
dd9154b
8091827
bd7f28e
4d302fa
30058a3
31e63e5
80f84aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,20 +6,28 @@ | |
| import bumblebee.xchangepass.domain.exchangeRate.repository.ExchangeRateTempRepository; | ||
| import bumblebee.xchangepass.domain.exchangeRate.repository.ExchangeRepository; | ||
| import bumblebee.xchangepass.domain.exchangeRate.util.Country; | ||
| import bumblebee.xchangepass.domain.exchangeRate.util.ExchangeRateLockManager; | ||
| import bumblebee.xchangepass.global.error.ErrorCode; | ||
| import bumblebee.xchangepass.global.exception.CommonException; | ||
| import jakarta.persistence.EntityManager; | ||
| import jakarta.persistence.PersistenceContext; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.beans.factory.annotation.Qualifier; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.cache.Cache; | ||
| import org.springframework.cache.CacheManager; | ||
| import org.springframework.cache.annotation.CacheEvict; | ||
| import org.springframework.cache.annotation.Cacheable; | ||
| import org.springframework.context.ApplicationContext; | ||
| import org.springframework.data.redis.RedisConnectionFailureException; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.transaction.support.TransactionSynchronization; | ||
| import org.springframework.transaction.support.TransactionSynchronizationManager; | ||
| import org.springframework.web.client.HttpClientErrorException; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
||
|
|
@@ -32,6 +40,7 @@ | |
| import java.util.concurrent.Executor; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class ExchangeService { | ||
|
|
||
| @Value("${api.key}") | ||
|
|
@@ -40,27 +49,33 @@ public class ExchangeService { | |
| private final ExchangeRepository exchangeRepository; | ||
| private final ExchangeRateTransactionService exchangeTransactionService; | ||
| private final ApplicationContext applicationContext; | ||
| private final RestTemplate restTemplate = new RestTemplate(); | ||
| private final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); | ||
| private final RestTemplate restTemplate; | ||
| private final Executor executor; | ||
| private final ExchangeRateTempRepository exchangeRateTempRepository; | ||
| private final String url = "https://v6.exchangerate-api.com/v6/" + authkey + "/latest/"; | ||
| private final ExchangeRateLockManager lockManager; | ||
| private final CacheManager cacheManager; | ||
|
|
||
| @Autowired | ||
| public ExchangeService(@Qualifier("asyncExecutor") Executor executor, | ||
| ExchangeRepository exchangeRepository, | ||
| ExchangeRateTempRepository exchangeRateTempRepository, | ||
| ExchangeRateTransactionService exchangeTransactionService, | ||
| ApplicationContext applicationContext) { | ||
| ApplicationContext applicationContext, | ||
| ExchangeRateLockManager lockManager, | ||
| CacheManager cacheManager, | ||
| RestTemplate restTemplate) { | ||
| this.exchangeRepository = exchangeRepository; | ||
| this.exchangeRateTempRepository = exchangeRateTempRepository; | ||
| this.exchangeTransactionService = exchangeTransactionService; | ||
| this.applicationContext = applicationContext; | ||
| this.executor = executor; | ||
|
|
||
| this.restTemplate = restTemplate; | ||
| this.lockManager = lockManager; | ||
| this.cacheManager = cacheManager; | ||
| } | ||
|
|
||
| public ExchangeRateResponse fetchExchangeRates(String baseCurrency) { | ||
| String API_URL = url + baseCurrency; | ||
| String API_URL = "https://v6.exchangerate-api.com/v6/" + authkey + "/latest/" + baseCurrency; | ||
|
||
| try { | ||
| return restTemplate.getForObject(API_URL, ExchangeRateResponse.class); | ||
| } catch (HttpClientErrorException e) { | ||
|
|
@@ -98,37 +113,88 @@ public CompletableFuture<Void> fetchAndSaveAllExchangeRates() { | |
| public void fetchAndSaveExchangeRate(String baseCurrency) { | ||
| ExchangeRateResponse response = fetchExchangeRates(baseCurrency); | ||
| saveRatesToTempDB(baseCurrency, response); | ||
|
|
||
| ExchangeService self = applicationContext.getBean(ExchangeService.class); | ||
| self.evictExchangeRateCache(baseCurrency); | ||
| evictExchangeRateCache(baseCurrency); | ||
| } | ||
|
|
||
| public void evictExchangeRateCache(String baseCurrency) { | ||
| redisTemplate.delete("all::" + baseCurrency); | ||
| try { | ||
| Cache cache = cacheManager.getCache("exchangeRates"); | ||
| if (cache != null) { | ||
| cache.evict("all::" + baseCurrency); | ||
|
|
||
| List<CompletableFuture<Void>> futures = new ArrayList<>(); | ||
|
|
||
| Set<String> rateKeys = redisTemplate.keys("rate::" + baseCurrency + "::*"); | ||
| if (rateKeys != null && !rateKeys.isEmpty()) { | ||
| redisTemplate.delete(rateKeys); | ||
| for (String targetCurrency : Country.create()) { | ||
| CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { | ||
| cache.evict("rate::" + baseCurrency + "::" + targetCurrency); | ||
| }, executor); | ||
|
|
||
| futures.add(future); | ||
| } | ||
|
|
||
| // 모든 작업 완료까지 대기 | ||
| CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); | ||
| } | ||
| } catch (RedisConnectionFailureException e) { | ||
| throw ErrorCode.REDIS_EVICT_ERROR.commonException(); | ||
| } catch (Exception e) { | ||
| throw ErrorCode.CACHE_EVICT_ERROR.commonException(); | ||
| } | ||
| } | ||
|
|
||
| @Transactional | ||
|
|
||
| @Cacheable(value = "exchangeRates", key = "'all::' + #baseCurrency", sync = true) | ||
| public ExchangeRateResponse getExchangeRateAll(String baseCurrency) { | ||
|
|
||
| List<ExchangeRate> exchangeRates = exchangeRepository.findByBaseCurrency(baseCurrency); | ||
| if (!exchangeRates.isEmpty()) { | ||
| Map<String, Double> conversionRates = exchangeRates.get(0).getExchangeRates(); | ||
| return ExchangeRateResponse.builder() | ||
| .baseCurrency(baseCurrency) | ||
| .conversionRates(conversionRates) | ||
| .build(); | ||
| return toResponse(baseCurrency, exchangeRates); | ||
| } | ||
|
|
||
| boolean lockAcquired = lockManager.tryAcquireLock(); | ||
|
|
||
| if (lockAcquired) { | ||
| try { | ||
| exchangeRates = exchangeRepository.findByBaseCurrency(baseCurrency); | ||
| if (!exchangeRates.isEmpty()) { | ||
| return toResponse(baseCurrency, exchangeRates); | ||
| } | ||
|
|
||
| fetchAndSaveAllExchangeRates().join(); | ||
|
|
||
| exchangeRates = exchangeRepository.findByBaseCurrency(baseCurrency); | ||
| if (!exchangeRates.isEmpty()) { | ||
| return toResponse(baseCurrency, exchangeRates); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 똑같은 코드가 두번 반복됩니다
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추후 계속 반복될경우 코드를 빼는 방향으로 가겠습니다. |
||
| } | ||
|
|
||
| } finally { | ||
| lockManager.releaseLock(); | ||
| } | ||
| } else { | ||
| fetchAndSaveAllExchangeRates(); | ||
| int retry = 50; | ||
| for (int i = 0; i < retry; i++) { | ||
| try { | ||
| Thread.sleep(200); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| break; | ||
| } | ||
|
|
||
| return fetchExchangeRates(baseCurrency); | ||
| exchangeRates = exchangeRepository.findByBaseCurrency(baseCurrency); | ||
| if (!exchangeRates.isEmpty()) { | ||
| return toResponse(baseCurrency, exchangeRates); | ||
| } | ||
| } | ||
| } | ||
| throw ErrorCode.EXCHANGE_RATE_NOT_FOUND.commonException(); | ||
| } | ||
|
|
||
| private ExchangeRateResponse toResponse(String baseCurrency, List<ExchangeRate> exchangeRates) { | ||
| Map<String, Double> conversionRates = exchangeRates.get(0).getExchangeRates(); | ||
| return ExchangeRateResponse.builder() | ||
| .baseCurrency(baseCurrency) | ||
| .conversionRates(conversionRates) | ||
| .build(); | ||
| } | ||
|
|
||
| public void saveRatesToTempDB(String baseCurrency, ExchangeRateResponse response) { | ||
| try { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package bumblebee.xchangepass.domain.exchangeRate.util; | ||
|
|
||
| import jakarta.persistence.EntityManager; | ||
| import jakarta.persistence.PersistenceContext; | ||
| import jakarta.persistence.Query; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.hibernate.Session; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.sql.PreparedStatement; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ExchangeRateLockManager { | ||
|
|
||
| @PersistenceContext | ||
| private final EntityManager entityManager; | ||
|
|
||
| private static final long LOCK_KEY = 987654321L; | ||
|
||
|
|
||
| public boolean tryAcquireLock() { | ||
| Query query = entityManager.createNativeQuery("SELECT pg_try_advisory_lock(:lockKey)"); | ||
| query.setParameter("lockKey", LOCK_KEY); | ||
| Boolean result = (Boolean) query.getSingleResult(); | ||
| return result != null && result; | ||
| } | ||
|
|
||
| public void releaseLock() { | ||
| entityManager.unwrap(Session.class).doWork(connection -> { | ||
| try (PreparedStatement stmt = connection.prepareStatement("SELECT pg_advisory_unlock(?)")) { | ||
| stmt.setLong(1, LOCK_KEY); | ||
| stmt.executeQuery(); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package bumblebee.xchangepass.global.config; | ||
|
|
||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
||
| @Configuration | ||
| public class RestTemplateConfig { | ||
|
|
||
| @Bean | ||
| public RestTemplate restTemplate() { | ||
| return new RestTemplate(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RequiredArgsConstructor 사용햇는데 생성자 주입 받고있는데 수정부탁드립니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고쳤습니다.