diff --git a/src/main/java/com/team4/giftidea/controller/ProductController.java b/src/main/java/com/team4/giftidea/controller/ProductController.java index 81d0d5f..07f7768 100644 --- a/src/main/java/com/team4/giftidea/controller/ProductController.java +++ b/src/main/java/com/team4/giftidea/controller/ProductController.java @@ -13,7 +13,7 @@ @Slf4j @RestController -@RequestMapping("/api/ products") +@RequestMapping("/api/products") public class ProductController { private final CoupangApiService coupangApiService; private final ProductService productService; @@ -82,11 +82,4 @@ public void crawlAndStoreData() { log.info("🎯 크둀링 및 μ €μž₯ μž‘μ—… μ™„λ£Œ!"); } - - @Scheduled(cron = "0 5 1 * * *") - public void autoCrawlAndStoreData() { - log.info("πŸ• μžλ™ 크둀링 μ‹œμž‘ (μƒˆλ²½ 1μ‹œ)..."); - crawlAndStoreData(); - log.info("βœ… μžλ™ 크둀링 μ™„λ£Œ!"); - } -} \ No newline at end of file +} diff --git a/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java b/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java index 2496b9d..5b274b3 100644 --- a/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java +++ b/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java @@ -16,6 +16,10 @@ import java.text.SimpleDateFormat; import java.util.*; +/** + * 쿠팑 νŒŒνŠΈλ„ˆμŠ€ API 호좜 μ‹œ "1λΆ„λ‹Ή 100회" μ œν•œμ„ μ΄ˆκ³Όν•˜μ§€ μ•Šλ„λ‘ + * 둀링 μœˆλ„μš° λ°©μ‹μ˜ Rate Limiterλ₯Ό μ μš©ν•œ μ„œλΉ„μŠ€ + */ @Slf4j @Service public class CoupangPartnersService { @@ -35,12 +39,24 @@ public class CoupangPartnersService { @Value("${coupang.api.partner-id}") private String partnerId; + /** + * 졜근 API 호좜 μ‹œμ μ„ μ €μž₯ν•˜λŠ” 큐 (λ°€λ¦¬μ΄ˆ λ‹¨μœ„) + * - 맨 μ•ž: κ°€μž₯ 였래된 호좜 μ‹œμ  + * - 맨 λ’€: κ°€μž₯ μ΅œμ‹  호좜 μ‹œμ  + */ + private final Deque callTimestamps = new LinkedList<>(); + + /** + * λ™μ‹œμ„± μ œμ–΄λ₯Ό μœ„ν•œ lock 객체 + */ + private final Object lock = new Object(); + public CoupangPartnersService(ProductRepository productRepository) { this.productRepository = productRepository; } /** - * 배치 & μƒν’ˆ λ‹¨μœ„λ‘œ λŒ€κΈ° μ‹œκ°„μ„ 두어 과도 ν˜ΈμΆœμ„ ν”Όν•˜λŠ” 방식 + * DBμ—μ„œ λͺ¨λ“  쿠팑 μƒν’ˆμ„ μ‘°νšŒν•˜κ³ , νŒŒνŠΈλ„ˆμŠ€ 링크둜 μ—…λ°μ΄νŠΈ */ @Transactional public int updateAllCoupangProductLinks() { @@ -49,54 +65,20 @@ public int updateAllCoupangProductLinks() { List coupangProducts = productRepository.findByMallName("Coupang"); log.info("πŸ“¦ 총 {}개의 쿠팑 μƒν’ˆμ„ 찾음", coupangProducts.size()); - // ν•œ λ²ˆμ— μ²˜λ¦¬ν•  μƒν’ˆ 수 - final int BATCH_SIZE = 50; - // 각 μƒν’ˆ 처리 ν›„ λŒ€κΈ° (ms) - 1초 - final long ITEM_SLEEP_MS = 1000L; - // 배치 μ™„λ£Œ ν›„ λŒ€κΈ° (ms) - 10초 (상황에 따라 λŠ˜λ¦¬κ±°λ‚˜ 쀄일 수 있음) - final long BATCH_SLEEP_MS = 10000L; - int updatedCount = 0; - - for (int i = 0; i < coupangProducts.size(); i += BATCH_SIZE) { - List batch = coupangProducts.subList(i, Math.min(i + BATCH_SIZE, coupangProducts.size())); - log.info("πŸ”Έ Batch 처리: index {} ~ {} (총 {}개)", i, i + batch.size() - 1, batch.size()); - - for (int j = 0; j < batch.size(); j++) { - Product product = batch.get(j); - String originalUrl = product.getLink(); - log.info("πŸ”— μƒν’ˆ ID {}의 κΈ°μ‘΄ URL: {}", product.getProductId(), originalUrl); - - String partnerLink = generatePartnerLink(originalUrl); - if (partnerLink != null) { - log.info("βœ… μƒν’ˆ ID {}의 λ³€ν™˜λœ νŒŒνŠΈλ„ˆμŠ€ 링크: {}", product.getProductId(), partnerLink); - product.setLink(partnerLink); - productRepository.save(product); - updatedCount++; - } else { - log.warn("⚠️ νŒŒνŠΈλ„ˆμŠ€ 링크 생성 μ‹€νŒ¨ (μƒν’ˆ ID: {})", product.getProductId()); - } - - // [μ€‘μš”] μƒν’ˆ 1건 처리 ν›„ 1초 λŒ€κΈ° -> 1λΆ„ μ΅œλŒ€ 60건 - if (j < batch.size() - 1) { - try { - Thread.sleep(ITEM_SLEEP_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("μƒν’ˆ λ‹¨μœ„ λŒ€κΈ° 쀑 μΈν„°λŸ½νŠΈ λ°œμƒ: {}", e.getMessage()); - } - } - } - - // λ°°μΉ˜κ°€ 끝났닀면 μΆ”κ°€λ‘œ 10초 λŒ€κΈ° - if (i + BATCH_SIZE < coupangProducts.size()) { - log.info("πŸ”Έ Batch 처리 μ™„λ£Œ: {}개 μƒν’ˆ μ—…λ°μ΄νŠΈ, λ‹€μŒ 배치 μ „ {}ms λŒ€κΈ°", batch.size(), BATCH_SLEEP_MS); - try { - Thread.sleep(BATCH_SLEEP_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("배치 λ‹¨μœ„ λŒ€κΈ° 쀑 μΈν„°λŸ½νŠΈ λ°œμƒ: {}", e.getMessage()); - } + for (Product product : coupangProducts) { + String originalUrl = product.getLink(); + log.info("πŸ”— μƒν’ˆ ID {}의 κΈ°μ‘΄ URL: {}", product.getProductId(), originalUrl); + + // νŒŒνŠΈλ„ˆμŠ€ 링크 λ³€ν™˜ + String partnerLink = generatePartnerLink(originalUrl); + if (partnerLink != null) { + log.info("βœ… μƒν’ˆ ID {}의 λ³€ν™˜λœ νŒŒνŠΈλ„ˆμŠ€ 링크: {}", product.getProductId(), partnerLink); + product.setLink(partnerLink); + productRepository.save(product); + updatedCount++; + } else { + log.warn("⚠️ νŒŒνŠΈλ„ˆμŠ€ 링크 생성 μ‹€νŒ¨ (μƒν’ˆ ID: {})", product.getProductId()); } } @@ -105,9 +87,13 @@ public int updateAllCoupangProductLinks() { } /** - * κΈ°μ‘΄ 쿠팑 μƒν’ˆ URL을 νŒŒνŠΈλ„ˆμŠ€ νŠΈλž˜ν‚Ή URL둜 λ³€ν™˜ν•˜κΈ° μœ„ν•œ API 호좜 + * κΈ°μ‘΄ 쿠팑 μƒν’ˆ URL을 νŒŒνŠΈλ„ˆμŠ€ νŠΈλž˜ν‚Ή URL둜 λ³€ν™˜ν•˜λŠ” API 호좜 + * β†’ "1λΆ„λ‹Ή 100회" μ œν•œμ„ λ„˜μ§€ μ•Šλ„λ‘ Rate Limiter 적용 */ private String generatePartnerLink(String originalUrl) { + // API 호좜 전에 Rate Limiter 체크 + waitIfLimitReached(); + try { String endpoint = "/v2/providers/affiliate_open_api/apis/openapi/v1/deeplink"; String apiUrl = baseUrl + endpoint; @@ -130,12 +116,14 @@ private String generatePartnerLink(String originalUrl) { log.info("πŸ” μš”μ²­ λ°”λ””: {}", requestBody); log.debug("πŸ” μš”μ²­ 헀더: {}", headers); + // API 호좜 long startTime = System.currentTimeMillis(); HttpEntity> entity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, Map.class); long duration = System.currentTimeMillis() - startTime; log.info("⏱️ API 호좜 μ†Œμš” μ‹œκ°„: {}ms", duration); + // API 응닡 처리 log.info("πŸ” API 응닡 μƒνƒœ μ½”λ“œ: {}", response.getStatusCode()); log.debug("πŸ” API 응닡 헀더: {}", response.getHeaders()); log.info("πŸ“¦ API 응닡 λ°”λ””: {}", response.getBody()); @@ -161,6 +149,45 @@ private String generatePartnerLink(String originalUrl) { return null; } + /** + * "1λΆ„(60초) λ‚΄ 100회" μ œν•œμ„ μ§€ν‚€κΈ° μœ„ν•œ Rate Limiter + */ + private void waitIfLimitReached() { + synchronized (lock) { + long now = System.currentTimeMillis(); + + // 60μ΄ˆκ°€ μ§€λ‚œ 기둝은 νμ—μ„œ 제거 + while (!callTimestamps.isEmpty() && now - callTimestamps.peekFirst() >= 60000) { + callTimestamps.removeFirst(); + } + + // 이미 100νšŒκ°€ μ°¨ μžˆλ‹€λ©΄, κ°€μž₯ 였래된 호좜과의 μ‹œκ°„ 차만큼 λŒ€κΈ° + if (callTimestamps.size() >= 100) { + long earliestCall = callTimestamps.peekFirst(); // κ°€μž₯ 였래된 호좜 μ‹œμ  + long waitTime = 60000 - (now - earliestCall); // 60초 - (ν˜„μž¬μ‹œκ°„ - κ°€μž₯ 였래된 ν˜ΈμΆœμ‹œκ°) + if (waitTime > 0) { + log.info("🚦 1λΆ„ 100회 초과 감지 β†’ {}ms λŒ€κΈ° μ‹œμž‘", waitTime); + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Rate Limiter λŒ€κΈ° 쀑 μΈν„°λŸ½νŠΈ λ°œμƒ: {}", e.getMessage()); + } + log.info("βœ… λŒ€κΈ° μ’…λ£Œ, λ‹€μ‹œ 호좜 μ§„ν–‰"); + } + + // λŒ€κΈ° ν›„ λ‹€μ‹œ now κ°±μ‹ , 60초 μ§€λ‚œ 기둝 제거 + now = System.currentTimeMillis(); + while (!callTimestamps.isEmpty() && now - callTimestamps.peekFirst() >= 60000) { + callTimestamps.removeFirst(); + } + } + + // ν˜„μž¬ 호좜 μ‹œμ μ„ 큐에 μΆ”κ°€ + callTimestamps.addLast(System.currentTimeMillis()); + } + } + /** * HMAC μ„œλͺ… 기반의 Authorization 헀더 생성 */ @@ -189,6 +216,6 @@ private String generateAuthorizationHeader(String method, String uri) { } return String.format("CEA algorithm=%s, access-key=%s, signed-date=%s, signature=%s", - "HmacSHA256", accessKey, signedDate, signature); + "HmacSHA256", accessKey, signedDate, signature); } -} \ No newline at end of file +}