Skip to content

feat: 배치 작업 안정성 개선#30

Merged
23tae merged 10 commits intodevelopfrom
fix/DND1302002-64
Aug 28, 2025
Merged

feat: 배치 작업 안정성 개선#30
23tae merged 10 commits intodevelopfrom
fix/DND1302002-64

Conversation

@23tae
Copy link
Copy Markdown
Member

@23tae 23tae commented Aug 28, 2025

📝 작업 내용

외부 API(KAMIS)를 사용하는 일일 가격 동기화 배치연간 가격 동기화 배치의 안정성을 개선했습니다.
간헐적으로 발생하던 아래의 오류를 해결하고, 불필요한 API 호출 및 DB 업데이트 로직을 최적화했습니다.

WebClientRequestException: Connection prematurely closed BEFORE response

⚡ 주요 변경사항

  1. API 요청 속도 제어 (Thread.sleep 추가)

    • 원인: 위 오류의 간헐적인 발생을 토대로, 단시간에 집중된 API 요청이 외부 서버의 트래픽 제어 정책에 의해 차단된 것이 원인이라고 추정했습니다.
    • 조치: 이러한 잠재적 문제를 예방하고 안정적인 호출 패턴을 확보하기 위해, 각 API 호출 사이에 200ms의 지연 시간을 추가하여 트래픽을 분산시켰습니다. (KamisDailyPriceApiReader, KamisAnnualPriceProcessor)
  2. 재시도(Retry) 로직 도입

    • 문제: 외부 서버의 일시적인 장애나 네트워크 불안정에 대응할 수 없었습니다.
    • 해결: Spring Batch의 faultTolerant 기능을 활용하여, 네트워크 예외(WebClientRequestException) 발생 시 최대 3회까지 작업을 자동으로 재시도하도록 설정했습니다.
  3. 연간 가격 배치 로직 최적화

    • API 호출 최적화: 연간 가격 정보를 제공하지 않는 상품(축산물, categoryCode: 500)에 대해 불필요한 API를 호출하지 않도록 Reader의 JPQL 쿼리를 수정했습니다.
    • DB 업데이트 최적화: API 응답(6년치) 중 현재 연도의 데이터만 필터링하여 DB에 업데이트하도록 Processor 로직을 개선했습니다. 이를 통해 불필요한 DB UPDATE 쿼리를 제거했습니다.

📌 리뷰 포인트

📋 체크리스트

  • ✍ PR 제목 규칙을 맞췄나요? (예: feat: 기능1 추가)
  • 📝 관련 이슈를 등록했나요?
  • ✅ 모든 테스트가 통과했나요?
  • 🏗 빌드가 정상적으로 완료되었나요?

Summary by CodeRabbit

  • 신기능

    • 네트워크 오류 발생 시 일일/연간 가격 동기화 자동 재시도(최대 3회) 적용으로 수집 안정성 향상
    • 일일 동기화가 저장된 카테고리·지역 목록을 기반해 동적으로 동작하도록 개선
  • 리팩터

    • 연간 가격 처리 흐름을 모듈화하고 API 호출 간 소폭 대기 및 가격 유효성 검증 추가
    • 일일 수집에 로깅·예외 처리 강화로 문제 진단 용이성 향상
    • 연간 수집에서 특정 카테고리 제외 및 순위 판정 로직 정교화
  • 변경

    • 일일 동기화 시 기준 날짜 계산을 서울(Asia/Seoul) 시간대로 명확히 지정

@23tae 23tae self-assigned this Aug 28, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 28, 2025

Walkthrough

배치 스텝에 WebClientRequestException 재시도(faultTolerant, retryLimit(3))를 추가하고, 일/연간 처리기에 로깅·지연(200ms)·예외 처리를 강화했습니다. 연간 조회에서 categoryCode != '500' 필터를 추가하고 리포지토리에 카테고리·지역 코드 조회 메서드를 도입했습니다.

Changes

Cohort / File(s) Summary of Changes
배치 스텝 재시도 구성
src/main/kotlin/com/eodigo/batch/KamisAnnualPriceBatchConfiguration.kt, src/main/kotlin/com/eodigo/batch/KamisDailyPriceBatchConfiguration.kt
Step에 faultTolerant(), retryLimit(3), retry(WebClientRequestException::class.java) 추가 및 관련 import 추가. Annual reader 쿼리에 p.categoryCode != '500' 필터 적용.
연간 가격 프로세서 리팩터링
src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt
process를 fetchAnnualPriceDataparseAndFilterPriceItemsconvertToEntities로 분리. KamisYearlyPriceItemDto 사용, 상수화(API_CALL_DELAY_MS, RETAIL_CODE, RANK_STANDARD), 200ms 지연, 숫자 검증·연도 필터, InterruptedException 처리 및 일반 예외 재던짐. 퍼블릭 시그니처 불변.
일일 가격 리더 안정화
src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt
SLF4J 로깅 추가, API 호출 루프에 try/catch/finally 도입, InterruptedException 재인터럽트 및 일반 예외 로깅, finally에서 200ms 지연, 초기화 println→log.info, 상수화(ERROR_CODE_SUCCESS, API_CALL_DELAY_MS).
일일 가격 프로세서 체크 변경
src/main/kotlin/com/eodigo/batch/processor/KamisDailyPriceProcessor.kt
랭크 필터를 item.rank != "상품"에서 item.rankCode != RANK_CODE_STANDARD ("04")로 변경(상수화).
스케줄러 타임존 명시
src/main/kotlin/com/eodigo/batch/scheduler/KamisJobScheduler.kt
surveyDate 계산을 LocalDate.now(ZoneId.of("Asia/Seoul"))로 명시적 타임존 적용(기존 시스템 기본 → 명시적 Asia/Seoul).
리포지토리 쿼리 추가
src/main/kotlin/com/eodigo/domain/product/repository/ProductRepository.kt, src/main/kotlin/com/eodigo/domain/product/repository/RegionRepository.kt
ProductRepository.findDistinctCategoryCodes(): List<String> 추가(@Query 사용). RegionRepository.findAllCodes(): List<String> 추가(@Query("SELECT r.code FROM Region r")). 관련 import 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Job as Spring Batch Job
  participant Step as Kamis*PriceSyncStep
  participant Reader as ItemReader
  participant Processor as ItemProcessor
  participant Writer as ItemWriter
  participant API as KamisApiClient

  Note over Step: faultTolerant + retryLimit(3)<br/>retry on WebClientRequestException
  Job->>Step: execute()
  loop for each item
    Step->>Reader: read()
    Reader-->>Step: item or null
    alt item != null
      Step->>Processor: process(item)
      rect rgba(230,240,255,0.6)
        Processor->>API: call(...)
        Note right of Processor: 200ms throttle
        API-->>Processor: response
      end
      alt WebClientRequestException
        Step-->>Processor: retry up to 3
      else success
        Processor-->>Step: result
        Step->>Writer: write(result)
      end
    else
      Step-->>Job: complete
    end
  end
Loading
sequenceDiagram
  autonumber
  participant Proc as KamisAnnualPriceProcessor
  participant API as KamisApiClient
  participant Parser as JSON Parser
  participant Ent as Entity Builder

  Proc->>API: getYearlyPrices(itemCode, kindCode, year)
  Note right of Proc: sleep 200ms
  API-->>Proc: response string or invalid
  alt invalid/absent
    Proc-->>Proc: return null
  else valid
    Proc->>Parser: parseAndFilterPriceItems()
    Parser-->>Proc: retail priceItems or null
    alt null
      Proc-->>Proc: return null
    else
      Proc->>Ent: convertToEntities(priceItems, year)
      Ent-->>Proc: List<AnnualNationalPrice>
      Proc-->>Proc: return list
    end
  end
  opt InterruptedException
    Proc-->>Proc: interrupt thread, return null
  end
Loading
sequenceDiagram
  autonumber
  participant Reader as KamisDailyPriceApiReader
  participant API as KamisApiClient
  participant Log as Logger

  loop categoryCodes × regionCodes
    Reader->>API: getDailyPrices(...)
    Note right of Reader: sleep 200ms
    alt success && errorCode == "000"
      API-->>Reader: items
      Reader-->>Reader: collect KamisDailyPriceApiData
    else InterruptedException
      Reader->>Log: warn + re-interrupt
    else other Exception
      Reader->>Log: error(category, region, message)
    end
  end
  Reader->>Log: info(initialized with N items)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/DND1302002-64

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (9)
src/main/kotlin/com/eodigo/batch/KamisAnnualPriceBatchConfiguration.kt (1)

66-66: 카테고리 코드 non-null 속성 확인됨—NULL 처리 불필요, 매직 문자열만 파라미터화 권장
Product.entity의 categoryCode@Column(nullable = false) val categoryCode: String로 non-null이 보장되므로 NULL 체크는 생략해도 됩니다. 대신 쿼리의 매직 리터럴 제거를 위해 파라미터화하세요.

- .queryString("SELECT p FROM Product p WHERE p.categoryCode != '500' ORDER BY p.id ASC")
+ .queryString("SELECT p FROM Product p WHERE p.categoryCode <> :excluded ORDER BY p.id ASC")
+ .parameterValues(mapOf("excluded" to "500"))
src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (4)

28-29: 현재 연도(LocalDate.now) 고정 의존 — 재실행 재현성 저하. jobParameter 주입 권장

연도 기준을 파라미터화하면 백필/재처리 시 안정적입니다. Processor에 대상 연도를 주입(선택적)하고 미지정 시 now()를 사용하세요.

예시:

-class KamisAnnualPriceProcessor(
-    private val kamisApiClient: KamisApiClient,
-    private val apiKey: String,
-    private val certId: String,
-) : ItemProcessor<Product, List<AnnualNationalPrice>> {
+class KamisAnnualPriceProcessor(
+    private val kamisApiClient: KamisApiClient,
+    private val apiKey: String,
+    private val certId: String,
+    private val targetYear: Int? = null,
+) : ItemProcessor<Product, List<AnnualNationalPrice>> {
@@
-        val currentYear = LocalDate.now().year.toString()
+        val currentYear = (targetYear ?: LocalDate.now().year).toString()

구성 클래스에서 StepScope로 연도 파라미터 주입을 연결해 드릴 수 있습니다.


50-54: 예외 로깅에서 stack trace 누락

원인 파악을 위해 예외 객체를 함께 로깅하세요.

-            log.error(
-                "Failed to process annual price for product id: ${item.id}. Error: ${e.message}"
-            )
+            log.error("Failed to process annual price for product id: {}", item.id, e)

72-72: Thread.sleep(200) 매직 넘버 — 상수/설정으로 추출

지연값을 상수 또는 설정(@value)로 노출해 운영 중 튜닝 가능하게 하세요.

-        Thread.sleep(200)
+        Thread.sleep(API_CALL_DELAY_MS)

파일 상단(또는 companion object)에 다음을 추가:

private const val API_CALL_DELAY_MS = 200L

혹은 생성자 파라미터로 주입받도록 변경 가능합니다.


87-91: caption 문자열(“상품”) 의존은 취약 — 코드값 기반 필터만 사용 권장

표기 변경/다국어에 취약합니다. productClsCode만으로 판별하세요.

-        val retailPriceSection =
-            priceSections.find { it.productClsCode == "01" && it.caption?.contains("상품") == true }
+        val retailPriceSection =
+            priceSections.find { it.productClsCode == "01" }

KAMIS 스키마에서 productClsCode “01”=소매가 맞는지 사양 링크/문서 기준 확인 부탁드립니다.

src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt (4)

75-80: 예외 로깅에서 stack trace 누락

원인 분석을 위해 예외 객체를 함께 전달하세요.

-                    log.error(
-                        "Failed to fetch data for category: {}, region: {}. Error: {}",
-                        categoryCode,
-                        regionCode,
-                        e.message,
-                    )
+                    log.error(
+                        "Failed to fetch data for category: {}, region: {}",
+                        categoryCode,
+                        regionCode,
+                        e
+                    )

61-69: API 비정상 응답(errorCode != "000")에 대한 관찰성 부족 — 경고 로그 추가 권장

성공 외 케이스를 로그로 남기면 장애 분석이 수월합니다.

-                    if (response?.data?.errorCode == "000") {
+                    if (response?.data?.errorCode == "000") {
                         response.data.items?.let { items ->
                             val wrappedItems =
                                 items.map { item ->
                                     KamisDailyPriceApiData(regionCode = regionCode, item = item)
                                 }
                             allItems?.addAll(wrappedItems)
                         }
+                    } else {
+                        val code = response?.data?.errorCode
+                        val msg = response?.data?.errorMsg
+                        log.warn(
+                            "KAMIS daily API returned errorCode={}, errorMsg={} for category={}, region={}",
+                            code, msg, categoryCode, regionCode
+                        )
                     }

70-74: Thread.sleep(200) 매직 넘버 — 상수/설정으로 추출

운영 튜닝을 위해 지연값을 상수 또는 설정으로 노출하세요.

-                    Thread.sleep(200)
+                    Thread.sleep(API_CALL_DELAY_MS)

이 파일에 상수 추가:

private const val API_CALL_DELAY_MS = 200L

또는 Reader 생성 시 파라미터로 주입하는 방식을 선호합니다.


84-84: 문자열 보간 로그 → placeholder 스타일로 통일

SLF4J 권장 스타일을 사용해 불필요한 문자열 생성 방지.

-        log.info("KamisDailyPriceApiReader: Total ${allItems?.size} items initialized.")
+        log.info("KamisDailyPriceApiReader: Total {} items initialized.", allItems?.size)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 138bad2 and 2008981.

📒 Files selected for processing (4)
  • src/main/kotlin/com/eodigo/batch/KamisAnnualPriceBatchConfiguration.kt (3 hunks)
  • src/main/kotlin/com/eodigo/batch/KamisDailyPriceBatchConfiguration.kt (2 hunks)
  • src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (2 hunks)
  • src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.kt

⚙️ CodeRabbit configuration file

src/**/*.kt: ## 1. Kotlin 코딩 컨벤션

  • Kotlin 공식 코딩 컨벤션을 준수해야 합니다.
  • 클래스는 PascalCase, 함수/변수는 camelCase, 상수는 UPPER_SNAKE_CASE로 작성하세요.
  • 변경 불가능한 데이터는 val을, 변경이 필요한 경우에만 var를 사용하세요.
  • Null Pointer Exception을 유발하는 !! 연산자 사용을 금지합니다.
  • 의미를 알 수 없는 숫자나 문자열(매직 넘버) 대신 명명된 상수를 사용하세요.

2. Spring Boot

  • Controller-Service-Repository 계층형 아키텍처의 책임을 분리하세요.
  • Controller의 요청/응답에는 반드시 DTO를 사용하고, 엔티티를 직접 노출하지 마세요.
  • 민감한 설정 정보(DB 계정, API 키 등)는 코드에 하드코딩하지 말고 외부에서 주입하세요.
  • @Autowired 필드 주입 대신 생성자 주입을 사용하세요.

3. JPA 및 데이터베이스

  • @entity 클래스에 Setter 사용을 지양하고, 비즈니스 메서드를 통해 상태를 변경하세요.
  • 데이터를 변경하지 않는 조회용 Service 메서드에는 @transactional(readOnly = true)를 적용하세요.
  • N+1 문제가 발생하는 쿼리가 있는지 확인하고, Fetch Join으로 최적화하세요.
  • 엔티티의 양방향 연관관계에서 toString()을 사용할 경우 순환 참조가 발생하지 않도록 주의하세요.

4. 예외 처리

  • @RestControllerAdvice를 사용하여 예외를 전역에서 일관되게 처리하세요.
  • RuntimeException 대신, 의미가 명확한 커스텀 예외(예: MemberNotFoundException)를 정의하여 사용하세요.

Files:

  • src/main/kotlin/com/eodigo/batch/KamisAnnualPriceBatchConfiguration.kt
  • src/main/kotlin/com/eodigo/batch/KamisDailyPriceBatchConfiguration.kt
  • src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt
  • src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (4)
src/main/kotlin/com/eodigo/batch/KamisAnnualPriceBatchConfiguration.kt (1)

23-23: spring-retry 의존성 확인 및 backOffPolicy 적용
org.springframework.retry:spring-retry 라이브러리가 Gradle 설정에 포함되어 있는지 수동으로 검증하고(검색 결과 없으므로), 누락됐다면 의존성을 추가한 뒤에

.faultTolerant()
  .retryLimit(3)
  .retry(WebClientRequestException::class.java)
  .backOffPolicy(FixedBackOffPolicy().apply { backOffPeriod = 500L })
.build()

와 같이 backoff 정책을 설정해 주세요.

src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (2)

99-117: AnnualNationalPrice.price가 Int 타입으로 정의되어 있어, 제안된 BigDecimal → Int 반올림 변환 적용이 적절합니다. 엔티티 타입 변경 없이 코드 적용 가능합니다.


31-45: Writer의 중복 키 처리 로직 안전 확인
Writer는 findByProductIdAndSurveyYearAndMarketType로 기존 레코드를 조회해 updatePrice 후 merge하고, id가 null인 신규 엔티티는 persist하여 unique 제약을 위반하지 않습니다. parse 단계에서 동일 키 중복 생성이 없으므로 별도 대응 불필요합니다.

src/main/kotlin/com/eodigo/batch/KamisDailyPriceBatchConfiguration.kt (1)

27-27: spring-retry 의존성 확인 및 backoffPolicy 적용

FixedBackOffPolicy 적용 전 org.springframework.retry 모듈이 빌드에 포함됐는지 확인하세요. 없으면 build.gradle(.kts)에 의존성을 추가한 뒤 아래처럼 backoffPolicy를 설정해주세요.

+import org.springframework.retry.backoff.FixedBackOffPolicy
@@
             .writer(kamisDailyPriceJpaWriter())
             .faultTolerant()
             .retryLimit(3)
             .retry(WebClientRequestException::class.java)
+            .backOffPolicy(FixedBackOffPolicy().apply { backOffPeriod = 500L })
             .build()

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/kotlin/com/eodigo/batch/processor/KamisDailyPriceProcessor.kt (1)

45-47: 가격 파싱을 보다 견고하게: 숫자 변환 실패 방지

, 제거만으로는 공백/비숫자 문자가 남아 NFE가 날 수 있습니다. toIntOrNull() 기반으로 안전 파싱하고, 변환된 값을 재사용하세요.

-        val priceStr = item.price?.replace(",", "")
-        if (priceStr.isNullOrBlank() || priceStr == "-") {
-            return null
-        }
+        val priceStrNormalized = item.price?.replace(",", "")?.trim()
+        val price = priceStrNormalized?.toIntOrNull() ?: return null
...
-            price = priceStr.toInt(),
+            price = price,

Also applies to: 62-68

🧹 Nitpick comments (9)
src/main/kotlin/com/eodigo/batch/scheduler/KamisJobScheduler.kt (1)

30-33: 서울 타임존 기준 산출, 좋습니다. run.time도 동일 타임존으로 정렬 권장

스케줄러 zone과 동일하게 run.time 파라미터도 서울 기준으로 맞추면 추후 로그/리플레이 시 혼선을 줄일 수 있습니다.

다음과 같이 변경해 주세요:

-                    .addLocalDateTime("run.time", LocalDateTime.now())
+                    .addLocalDateTime("run.time", LocalDateTime.now(ZoneId.of("Asia/Seoul")))
src/main/kotlin/com/eodigo/batch/processor/KamisDailyPriceProcessor.kt (1)

51-58: 널 키 조합 방지: 유효하지 않은 키 조합은 조기 필터링

createProductKey(itemCode, kindCode)null-... 형태를 만들 수 있습니다. 키 생성 전에 item.itemCode/item.kindCode/wrapper.regionCode 널·빈값을 선제 차단하면 불필요한 로그와 캐시 미스가 줄어듭니다.

src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (2)

34-35: 연도 계산도 서울 타임존으로 일관화

스케줄이 Asia/Seoul 기준이므로 현 연도 계산도 동일 타임존으로 맞추는 것이 안전합니다(해외 서버 시간대 영향 방지).

-        val currentYear = LocalDate.now().year.toString()
+        val currentYear = LocalDate.now(java.time.ZoneId.of("Asia/Seoul")).year.toString()

91-97: 리테일 섹션 선택 로직 보강(캡션 의존 완화)

일부 응답에서 caption이 누락/불일치할 수 있습니다. RETAIL_CODE 일치 섹션을 1순위, 실패 시 코드만으로 fallback 하도록 완화하면 누락을 줄일 수 있습니다.

-        val retailPriceSection =
-            priceSections.find {
-                it.productClsCode == RETAIL_CODE && it.caption?.contains(RANK_STANDARD) == true
-            }
+        val retailPriceSection =
+            priceSections.find {
+                it.productClsCode == RETAIL_CODE && it.caption?.contains(RANK_STANDARD) == true
+            } ?: priceSections.find { it.productClsCode == RETAIL_CODE }
src/main/kotlin/com/eodigo/batch/KamisDailyPriceBatchConfiguration.kt (2)

27-27: 예외 타깃은 적절하나, 응답 단계 오류도 재시도 고려

네트워크 레벨(WebClientRequestException)만이 아니라 5xx 등 응답 레벨(WebClientResponseException)도 재시도 대상으로 포함하는 것을 권장합니다.

-import org.springframework.web.reactive.function.client.WebClientRequestException
+import org.springframework.web.reactive.function.client.WebClientRequestException
+import org.springframework.web.reactive.function.client.WebClientResponseException
-            .retry(WebClientRequestException::class.java)
+            .retry(WebClientRequestException::class.java)
+            .retry(WebClientResponseException::class.java)

74-75: 빈 코드 목록에 대한 빠른 실패 또는 경고 추가 제안

DB에서 카테고리/지역 코드가 비어오면 조용히 “성공처럼” 종료될 수 있습니다. 빠른 실패(throw) 또는 명시적 경고 로그를 권장합니다.

-        val categoryCodes = productRepository.findDistinctCategoryCodes()
-        val regionCodes = regionRepository.findAllCodes()
+        val categoryCodes = productRepository.findDistinctCategoryCodes()
+        val regionCodes = regionRepository.findAllCodes()
+        require(categoryCodes.isNotEmpty()) { "No category codes found for daily price sync." }
+        require(regionCodes.isNotEmpty()) { "No region codes found for daily price sync." }
src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt (3)

19-25: 상수·로거 도입은 👍, 지연값은 프로퍼티로 외부화 고려

API_CALL_DELAY_MS를 애플리케이션 프로퍼티로 외부화하면 운영 환경별 조정이 수월합니다(예: kamis.api.call-delay-ms).


7-7: 필수 import 누락 가능성

위 수정 반영 시 WebClientRequestException import가 필요합니다.

 import org.slf4j.LoggerFactory
+import org.springframework.web.reactive.function.client.WebClientRequestException

89-89: 초기화 로그: 널 안전·가독성 개선

allItems는 이 시점에 널이 아니므로 명시적으로 사이즈를 계산하세요.

-        log.info("KamisDailyPriceApiReader: Total ${allItems?.size} items initialized.")
+        val total = allItems?.size ?: 0
+        log.info("KamisDailyPriceApiReader: Total {} items initialized.", total)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2008981 and c138d3c.

📒 Files selected for processing (7)
  • src/main/kotlin/com/eodigo/batch/KamisDailyPriceBatchConfiguration.kt (3 hunks)
  • src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (2 hunks)
  • src/main/kotlin/com/eodigo/batch/processor/KamisDailyPriceProcessor.kt (2 hunks)
  • src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt (3 hunks)
  • src/main/kotlin/com/eodigo/batch/scheduler/KamisJobScheduler.kt (2 hunks)
  • src/main/kotlin/com/eodigo/domain/product/repository/ProductRepository.kt (2 hunks)
  • src/main/kotlin/com/eodigo/domain/product/repository/RegionRepository.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.kt

⚙️ CodeRabbit configuration file

src/**/*.kt: ## 1. Kotlin 코딩 컨벤션

  • Kotlin 공식 코딩 컨벤션을 준수해야 합니다.
  • 클래스는 PascalCase, 함수/변수는 camelCase, 상수는 UPPER_SNAKE_CASE로 작성하세요.
  • 변경 불가능한 데이터는 val을, 변경이 필요한 경우에만 var를 사용하세요.
  • Null Pointer Exception을 유발하는 !! 연산자 사용을 금지합니다.
  • 의미를 알 수 없는 숫자나 문자열(매직 넘버) 대신 명명된 상수를 사용하세요.

2. Spring Boot

  • Controller-Service-Repository 계층형 아키텍처의 책임을 분리하세요.
  • Controller의 요청/응답에는 반드시 DTO를 사용하고, 엔티티를 직접 노출하지 마세요.
  • 민감한 설정 정보(DB 계정, API 키 등)는 코드에 하드코딩하지 말고 외부에서 주입하세요.
  • @Autowired 필드 주입 대신 생성자 주입을 사용하세요.

3. JPA 및 데이터베이스

  • @entity 클래스에 Setter 사용을 지양하고, 비즈니스 메서드를 통해 상태를 변경하세요.
  • 데이터를 변경하지 않는 조회용 Service 메서드에는 @transactional(readOnly = true)를 적용하세요.
  • N+1 문제가 발생하는 쿼리가 있는지 확인하고, Fetch Join으로 최적화하세요.
  • 엔티티의 양방향 연관관계에서 toString()을 사용할 경우 순환 참조가 발생하지 않도록 주의하세요.

4. 예외 처리

  • @RestControllerAdvice를 사용하여 예외를 전역에서 일관되게 처리하세요.
  • RuntimeException 대신, 의미가 명확한 커스텀 예외(예: MemberNotFoundException)를 정의하여 사용하세요.

Files:

  • src/main/kotlin/com/eodigo/domain/product/repository/ProductRepository.kt
  • src/main/kotlin/com/eodigo/batch/scheduler/KamisJobScheduler.kt
  • src/main/kotlin/com/eodigo/domain/product/repository/RegionRepository.kt
  • src/main/kotlin/com/eodigo/batch/KamisDailyPriceBatchConfiguration.kt
  • src/main/kotlin/com/eodigo/batch/processor/KamisDailyPriceProcessor.kt
  • src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt
  • src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt
🔇 Additional comments (3)
src/main/kotlin/com/eodigo/batch/processor/KamisDailyPriceProcessor.kt (2)

21-23: 상수 도입으로 매직 넘버 제거, 👍

RANK_CODE_STANDARD 상수화로 가독성과 유지보수성이 좋아졌습니다.


41-43: 필터 기준 변경(문자열 → 코드)으로 결과 집합 달라질 수 있음 — 도메인 매핑 재확인 필요

item.rank == "상품"에서 item.rankCode == "04"로 바뀌었습니다. KAMIS 스키마에서 rankCode="04"가 항상 "상품"을 의미하는지, 연/일별 API 간 불일치가 없는지 확인해 주세요. 기준이 다르면 누락/과다 필터링 위험이 있습니다.

검증이 필요하면 알려주세요. 단위 테스트/샘플 페이로드 기반의 검증 케이스를 만들어 드리겠습니다.

src/main/kotlin/com/eodigo/domain/product/repository/ProductRepository.kt (1)

17-18: findDistinctCategoryCodesWHERE p.categoryCode IS NOT NULL 조건 불필요 categoryCode@Column(nullable=false) 및 Kotlin non-null 타입이므로 null이 반환되지 않으며, 기존 쿼리를 그대로 유지해도 됩니다.

Likely an incorrect or invalid review comment.

Comment thread src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt (2)

66-74: 비성공 응답 로깅 누락 — 관측성 보강 필요

KAMIS가 errorCode != "000"인 경우 경고 로그가 없어 트러블슈팅이 어렵습니다. 아래처럼 코드/메시지를 함께 남겨주세요.

-                    if (response?.data?.errorCode == ERROR_CODE_SUCCESS) {
+                    if (response?.data?.errorCode == ERROR_CODE_SUCCESS) {
                         response.data.items?.let { items ->
                             val wrappedItems =
                                 items.map { item ->
                                     KamisDailyPriceApiData(regionCode = regionCode, item = item)
                                 }
                             allItems?.addAll(wrappedItems)
                         }
+                    } else {
+                        val code = response?.data?.errorCode
+                        val msg = response?.data?.errorMessage
+                        log.warn(
+                            "KAMIS daily API non-success. category={}, region={}, code={}, message={}",
+                            categoryCode, regionCode, code, msg
+                        )
                     }

75-87: 재시도 무력화·중복 위험 — WebClientRequestException 재던지기 전 상태 리셋

예외를 포괄적으로 던지지만, 부분적으로 채워진 allItems가 남아 재시도 시 중복/불완전 읽기가 발생할 수 있습니다. 네트워크 계열 예외에서 상태를 초기화하고 재던지세요. 또한 현재 try 블록에서 InterruptedException이 발생할 곳이 없어 해당 catch는 도달하지 않습니다(불필요).

-                } catch (e: InterruptedException) {
-                    log.warn("API call delay was interrupted", e)
-                    Thread.currentThread().interrupt()
-                    break
-                } catch (e: Exception) {
-                    log.error(
-                        "Failed to fetch data for category: {}, region: {}. Error: {}",
-                        categoryCode,
-                        regionCode,
-                        e.message,
-                    )
-                    throw e
+                } catch (e: org.springframework.web.reactive.function.client.WebClientRequestException) {
+                    // Clean retry: reset state and rethrow to let Step-level retry handle it
+                    allItems = null
+                    currentItemIndex = 0
+                    log.warn(
+                        "Transient client/network error — triggering step retry. category={}, region={}",
+                        categoryCode, regionCode, e
+                    )
+                    throw e
+                } catch (e: Exception) {
+                    log.error("Failed to fetch daily data. category={}, region={}", categoryCode, regionCode, e)
+                    throw e
                 }
src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (1)

36-59: 재시도 범위 축소: 네트워크 예외만 재던지고 나머지는 스킵

모든 예외를 재던지면 파싱/데이터 이슈까지 스텝 실패로 이어집니다. WebClientRequestException만 재던지고, 그 외는 로깅 후 null 반환으로 스킵하세요.

         try {
             // 1. API 호출
             val responseString = fetchAnnualPriceData(item.itemCode, kindCode, currentYear)
             if (responseString == null) {
                 return null
             }
             // 2. 응답 파싱 및 필터링
             val priceItems = parseAndFilterPriceItems(responseString)
             if (priceItems == null) {
                 return null
             }
             // 3. 엔티티로 변환
             return convertToEntities(priceItems, item, currentYear)
         } catch (e: InterruptedException) {
             log.warn("Annual price processing was interrupted for product id: ${item.id}", e)
             Thread.currentThread().interrupt()
             return null
-        } catch (e: Exception) {
-            log.error("Failed to process annual price for product id: {}", item.id, e)
-            throw e
+        } catch (e: org.springframework.web.reactive.function.client.WebClientRequestException) {
+            log.warn("Transient client/network error — triggering step retry. productId={}", item.id, e)
+            throw e
+        } catch (e: Exception) {
+            log.error("Failed to process annual price (non-retryable). productId={}", item.id, e)
+            return null
         }

검증 스크립트(재시도 대상 확인):

#!/bin/bash
# Annual Batch에서 retry 대상을 점검 (WebClientRequestException)
rg -n -C2 -g '!**/build/**' -P 'KamisAnnualPriceBatchConfiguration|faultTolerant\(|retry\(|retryLimit\('
🧹 Nitpick comments (4)
src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt (1)

97-97: 로그 값 명시화(경미)

널 가능성은 낮지만 가독성을 위해 0 기본값을 사용하세요.

-        log.info("KamisDailyPriceApiReader: Total ${allItems?.size} items initialized.")
+        val count = allItems?.size ?: 0
+        log.info("KamisDailyPriceApiReader: Total {} items initialized.", count)
src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (3)

22-26: 상수화 👍 + 누락 상수 제안

API 지연·리테일 코드·캡션 기준 상수화는 적절합니다. “결과가 존재하지 않습니다.”도 상수화해 중복·오타를 방지하세요.

     companion object {
         private const val API_CALL_DELAY_MS = 200L
         private const val RETAIL_CODE = "01"
         private const val RANK_STANDARD = "상품"
+        private const val NO_RESULT_MESSAGE = "결과가 존재하지 않습니다."
     }

34-35: 현재 연도 계산의 시간대·테스트 가능성

Asia/Seoul 스케줄과의 시간대 일치 및 테스트 용이성을 위해 Clock 주입을 고려하세요.

-        val currentYear = LocalDate.now().year.toString()
+        val currentYear = LocalDate.now(clock).year.toString()

추가: 생성자에 private val clock: Clock 주입 및 기본값 Clock.system(ZoneId.of("Asia/Seoul")).


61-82: 지연·재시도 책임의 모듈화 제안

지연/재시도 로직을 KamisApiClient(또는 공통 헬퍼)로 흡수하면 Reader/Processor 중복이 줄고 정책 변경 시 한 곳에서 관리할 수 있습니다. resilience4j RateLimiter/Retry 도입을 고려하세요.

Also applies to: 36-59

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c138d3c and cde2ec9.

📒 Files selected for processing (2)
  • src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (2 hunks)
  • src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.kt

⚙️ CodeRabbit configuration file

src/**/*.kt: ## 1. Kotlin 코딩 컨벤션

  • Kotlin 공식 코딩 컨벤션을 준수해야 합니다.
  • 클래스는 PascalCase, 함수/변수는 camelCase, 상수는 UPPER_SNAKE_CASE로 작성하세요.
  • 변경 불가능한 데이터는 val을, 변경이 필요한 경우에만 var를 사용하세요.
  • Null Pointer Exception을 유발하는 !! 연산자 사용을 금지합니다.
  • 의미를 알 수 없는 숫자나 문자열(매직 넘버) 대신 명명된 상수를 사용하세요.

2. Spring Boot

  • Controller-Service-Repository 계층형 아키텍처의 책임을 분리하세요.
  • Controller의 요청/응답에는 반드시 DTO를 사용하고, 엔티티를 직접 노출하지 마세요.
  • 민감한 설정 정보(DB 계정, API 키 등)는 코드에 하드코딩하지 말고 외부에서 주입하세요.
  • @Autowired 필드 주입 대신 생성자 주입을 사용하세요.

3. JPA 및 데이터베이스

  • @entity 클래스에 Setter 사용을 지양하고, 비즈니스 메서드를 통해 상태를 변경하세요.
  • 데이터를 변경하지 않는 조회용 Service 메서드에는 @transactional(readOnly = true)를 적용하세요.
  • N+1 문제가 발생하는 쿼리가 있는지 확인하고, Fetch Join으로 최적화하세요.
  • 엔티티의 양방향 연관관계에서 toString()을 사용할 경우 순환 참조가 발생하지 않도록 주의하세요.

4. 예외 처리

  • @RestControllerAdvice를 사용하여 예외를 전역에서 일관되게 처리하세요.
  • RuntimeException 대신, 의미가 명확한 커스텀 예외(예: MemberNotFoundException)를 정의하여 사용하세요.

Files:

  • src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt
  • src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt
🔇 Additional comments (4)
src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt (1)

19-25: 상수·로거 도입 적절

지연(ms)·성공코드 상수화와 로거 주입 모두 👍

src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt (3)

7-7: DTO 타입 명시 import 적절

연간 응답 파싱의 타입 안정성 확보에 도움이 됩니다.


84-97: 소매 섹션 선택 로직 적절

productClsCode·캡션 기준 필터링 타당합니다.


130-158: 유연한 응답(normalize) 처리 👍

price가 List/Map 양자 처리 및 예외 로깅이 적절합니다.

Comment on lines +61 to +82
/** API를 호출하고 응답 문자열을 반환합니다. 유효하지 않은 응답은 null을 반환합니다. */
private fun fetchAnnualPriceData(
itemCode: String,
kindCode: String,
currentYear: String,
): String? {
val responseString =
kamisApiClient.getYearlyPrices(
certKey = apiKey,
certId = certId,
year = currentYear,
itemCode = item.itemCode,
itemCode = itemCode,
kindCode = kindCode,
)

Thread.sleep(API_CALL_DELAY_MS)

if (responseString.isNullOrBlank() || responseString.contains("결과가 존재하지 않습니다.")) {
return null
}
return responseString
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

실패 시에도 지연 보장 — 지연을 finally로 이동

현재는 API 호출이 예외를 던지면 지연이 생략됩니다. 실패 시에도 레이트 리밋을 보장하세요.

-    private fun fetchAnnualPriceData(
+    private fun fetchAnnualPriceData(
         itemCode: String,
         kindCode: String,
         currentYear: String,
     ): String? {
-        val responseString =
-            kamisApiClient.getYearlyPrices(
-                certKey = apiKey,
-                certId = certId,
-                year = currentYear,
-                itemCode = itemCode,
-                kindCode = kindCode,
-            )
-
-        Thread.sleep(API_CALL_DELAY_MS)
+        val responseString =
+            try {
+                kamisApiClient.getYearlyPrices(
+                    certKey = apiKey,
+                    certId = certId,
+                    year = currentYear,
+                    itemCode = itemCode,
+                    kindCode = kindCode,
+                )
+            } finally {
+                try {
+                    Thread.sleep(API_CALL_DELAY_MS)
+                } catch (ie: InterruptedException) {
+                    log.warn("Annual API call delay was interrupted", ie)
+                    Thread.currentThread().interrupt()
+                }
+            }
 
-        if (responseString.isNullOrBlank() || responseString.contains("결과가 존재하지 않습니다.")) {
+        if (responseString.isNullOrBlank() || responseString.contains(NO_RESULT_MESSAGE)) {
             return null
         }
         return responseString
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** API를 호출하고 응답 문자열을 반환합니다. 유효하지 않은 응답은 null을 반환합니다. */
private fun fetchAnnualPriceData(
itemCode: String,
kindCode: String,
currentYear: String,
): String? {
val responseString =
kamisApiClient.getYearlyPrices(
certKey = apiKey,
certId = certId,
year = currentYear,
itemCode = item.itemCode,
itemCode = itemCode,
kindCode = kindCode,
)
Thread.sleep(API_CALL_DELAY_MS)
if (responseString.isNullOrBlank() || responseString.contains("결과가 존재하지 않습니다.")) {
return null
}
return responseString
}
/** API를 호출하고 응답 문자열을 반환합니다. 유효하지 않은 응답은 null을 반환합니다. */
private fun fetchAnnualPriceData(
itemCode: String,
kindCode: String,
currentYear: String,
): String? {
val responseString = try {
kamisApiClient.getYearlyPrices(
certKey = apiKey,
certId = certId,
year = currentYear,
itemCode = itemCode,
kindCode = kindCode,
)
} finally {
try {
Thread.sleep(API_CALL_DELAY_MS)
} catch (ie: InterruptedException) {
log.warn("Annual API call delay was interrupted", ie)
Thread.currentThread().interrupt()
}
}
if (responseString.isNullOrBlank() || responseString.contains(NO_RESULT_MESSAGE)) {
return null
}
return responseString
}
🤖 Prompt for AI Agents
In src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt
around lines 61 to 82, the Thread.sleep currently runs only after a successful
API call so exceptions skip the delay; wrap the kamisApiClient.getYearlyPrices
call (and assignment to responseString) in a try-finally block and move
Thread.sleep(API_CALL_DELAY_MS) into the finally clause so the delay always
executes whether the call succeeds or throws, preserving existing null/blank
checks and allowing exceptions to propagate as before.

Comment on lines +100 to 128
private fun convertToEntities(
priceItems: List<KamisYearlyPriceItemDto>,
product: Product,
currentYear: String,
): List<AnnualNationalPrice>? {
val entities =
priceItems
.filter { it.div == currentYear }
.mapNotNull { priceItem ->
val yearStr = priceItem.div
val priceStr = priceItem.avgData?.replace(",", "")

if (
yearStr != null &&
priceStr?.all(Char::isDigit) == true &&
priceStr.isNotEmpty()
) {
AnnualNationalPrice(
product = product,
price = priceStr.toInt(),
surveyYear = yearStr.toInt(),
marketType = MarketType.RETAIL,
)
} else {
null
}
}
return entities.ifEmpty { null }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

가격 파싱 견고성 강화(콤마 외 문자를 제거하고 toIntOrNull 사용)

숫자 외 문자(공백/원 단위 등)로 실패할 수 있습니다. 안전 파싱으로 NPE/NumberFormatException 방지하세요.

-                .mapNotNull { priceItem ->
-                    val yearStr = priceItem.div
-                    val priceStr = priceItem.avgData?.replace(",", "")
-
-                    if (
-                        yearStr != null &&
-                            priceStr?.all(Char::isDigit) == true &&
-                            priceStr.isNotEmpty()
-                    ) {
-                        AnnualNationalPrice(
-                            product = product,
-                            price = priceStr.toInt(),
-                            surveyYear = yearStr.toInt(),
-                            marketType = MarketType.RETAIL,
-                        )
-                    } else {
-                        null
-                    }
-                }
+                .mapNotNull { priceItem ->
+                    val yearStr = priceItem.div
+                    val priceStr = priceItem.avgData
+                        ?.replace(Regex("[^0-9]"), "")
+                        ?.trim()
+
+                    val price = priceStr?.toIntOrNull()
+                    val surveyYear = yearStr?.toIntOrNull()
+                    if (price != null && surveyYear != null) {
+                        AnnualNationalPrice(
+                            product = product,
+                            price = price,
+                            surveyYear = surveyYear,
+                            marketType = MarketType.RETAIL,
+                        )
+                    } else null
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun convertToEntities(
priceItems: List<KamisYearlyPriceItemDto>,
product: Product,
currentYear: String,
): List<AnnualNationalPrice>? {
val entities =
priceItems
.filter { it.div == currentYear }
.mapNotNull { priceItem ->
val yearStr = priceItem.div
val priceStr = priceItem.avgData?.replace(",", "")
if (
yearStr != null &&
priceStr?.all(Char::isDigit) == true &&
priceStr.isNotEmpty()
) {
AnnualNationalPrice(
product = product,
price = priceStr.toInt(),
surveyYear = yearStr.toInt(),
marketType = MarketType.RETAIL,
)
} else {
null
}
}
return entities.ifEmpty { null }
}
private fun convertToEntities(
priceItems: List<KamisYearlyPriceItemDto>,
product: Product,
currentYear: String,
): List<AnnualNationalPrice>? {
val entities =
priceItems
.filter { it.div == currentYear }
.mapNotNull { priceItem ->
val yearStr = priceItem.div
val priceStr = priceItem.avgData
?.replace(Regex("[^0-9]"), "")
?.trim()
val price = priceStr?.toIntOrNull()
val surveyYear = yearStr?.toIntOrNull()
if (price != null && surveyYear != null) {
AnnualNationalPrice(
product = product,
price = price,
surveyYear = surveyYear,
marketType = MarketType.RETAIL,
)
} else null
}
return entities.ifEmpty { null }
}
🤖 Prompt for AI Agents
In src/main/kotlin/com/eodigo/batch/processor/KamisAnnualPriceProcessor.kt
around lines 100 to 128, the current parsing assumes priceStr contains only
digits after removing commas and uses toInt(), which can throw
NumberFormatException for other characters (spaces, currency symbols) and
doesn't safely parse year; update parsing to first normalize the string by
stripping all non-digit characters (e.g., replace(Regex("\\D+"), "")), then use
toIntOrNull() for both price and surveyYear, only construct AnnualNationalPrice
when both toIntOrNull results are non-null, otherwise return null for that item;
keep returning null for the whole list when empty.

Comment on lines +87 to 93
} finally {
try {
Thread.sleep(API_CALL_DELAY_MS)
} catch (ie: InterruptedException) {
log.warn("API call delay was interrupted", ie)
Thread.currentThread().interrupt()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

인터럽트 시 초기화 즉시 중단하여 부분 상태 방지

sleep 인터럽트 후에도 루프가 계속되어 부분 수집 상태가 남을 수 있습니다. 인터럽트 재설정 후 안전 종료하세요.

                 } finally {
                     try {
                         Thread.sleep(API_CALL_DELAY_MS)
                     } catch (ie: InterruptedException) {
                         log.warn("API call delay was interrupted", ie)
                         Thread.currentThread().interrupt()
-                    }
+                        // Abort initialization to avoid partial state on interrupt
+                        allItems = null
+                        currentItemIndex = 0
+                        return
+                    }
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} finally {
try {
Thread.sleep(API_CALL_DELAY_MS)
} catch (ie: InterruptedException) {
log.warn("API call delay was interrupted", ie)
Thread.currentThread().interrupt()
}
} finally {
try {
Thread.sleep(API_CALL_DELAY_MS)
} catch (ie: InterruptedException) {
log.warn("API call delay was interrupted", ie)
Thread.currentThread().interrupt()
// Abort initialization to avoid partial state on interrupt
allItems = null
currentItemIndex = 0
return
}
}
🤖 Prompt for AI Agents
In src/main/kotlin/com/eodigo/batch/reader/KamisDailyPriceApiReader.kt around
lines 87 to 93, the catch for InterruptedException currently logs and resets the
interrupt but allows the loop to continue, risking partial/half-collected state;
after restoring the interrupt (Thread.currentThread().interrupt()) immediately
stop the reader's execution (e.g., break out of the surrounding loop or return
from the method, or rethrow the InterruptedException) so the job exits safely
and no further processing occurs.

@23tae 23tae merged commit 545d018 into develop Aug 28, 2025
2 checks passed
@23tae 23tae deleted the fix/DND1302002-64 branch August 28, 2025 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant