Skip to content

feat: Spring Batch 인프라 구축 — 자유게시판 스케줄러 분리 샘플 - #1

Merged
cdkkyj123 merged 4 commits into
mainfrom
feat/spring-batch-infra
May 24, 2026
Merged

feat: Spring Batch 인프라 구축 — 자유게시판 스케줄러 분리 샘플#1
cdkkyj123 merged 4 commits into
mainfrom
feat/spring-batch-infra

Conversation

@cdkkyj123

@cdkkyj123 cdkkyj123 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • POCAT 메인 앱의 @scheduled 자유게시판 스케줄러를 Spring Batch Job/Step/Tasklet 구조로 분리
  • 멀티 앱 서버 배포 시 스케줄러 중복 실행 방지를 위한 배치 전용 서버 인프라 구축
  • 팀원 참고용 샘플 코드: freePostRankingJob / viewCountFlushJob 2개 Job 구현

구현 내용

구분 내용
Spring Boot 3.3.5 / Spring Batch 5
Job 1 freePostRankingJob — Redis ZSet 인기 랭킹 60초 갱신
Job 2 viewCountFlushJob — Redis → MySQL viewCount/commentCount 60초 플러시
스케줄러 @scheduled(fixedDelay=60_000) + JobLauncher 트리거
트랜잭션 FreePostFlushService.REQUIRES_NEW — 엔트리 단위 격리
메타테이블 MySQL BATCH_* 자동 생성 (initialize-schema: always)

보안/안정성

  • Redis rename 전 key 존재 가드 + try-finally delete 보장
  • ZSet entry null 체크 (score/value)
  • dotenv OS 환경변수 우선 적용
  • Redis password 지원

운영 배포 전 필수

  • initialize-schema: never 로 변경
  • 메인 앱 스케줄러 비활성화 (중복 실행 방지)
  • Redis requirepass + REDIS_PASSWORD 환경변수 설정

관련 문서

  • docs/ADR-001-spring-batch-separation.md
  • docs/ARCHITECTURE.md
  • docs/RUNBOOK.md

Summary by CodeRabbit

  • New Features

    • Introduced a standalone pocat-batch service that runs scheduled jobs to refresh free-post rankings and flush view/comment counts every 60s.
  • Documentation

    • Added README, architecture docs, ADR, and runbook with design, operation, and deployment guidance.
  • Configuration

    • Added example .env template, .gitignore, Gradle wrapper/build and default app configuration for running the batch server.
  • Tests

    • Added integration tests validating the batch jobs complete successfully.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ab7e000-e958-4fb2-a253-b1465268f801

📥 Commits

Reviewing files that changed from the base of the PR and between 33a7494 and b39c39c.

📒 Files selected for processing (1)
  • src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java

📝 Walkthrough

Walkthrough

The PR adds a new Spring Batch service (pocat-batch) implementing two scheduled jobs (freePostRankingJob and viewCountFlushJob), domain entities/repository/service, Batch job/tasklet implementations with Redis staging and MySQL persistence, Gradle build/wrapper and env/gitignore, and documentation (README, ADR, architecture, runbook).

Changes

pocat-batch Core Implementation

Layer / File(s) Summary
Project Build & Environment Setup
build.gradle, gradle/wrapper/*, gradlew, gradlew.bat, settings.gradle, .env.example, .gitignore
Gradle build configuration with Java 17 toolchain, Spring Boot batch/JPA/Redis starters, Gradle wrapper scripts for Unix/Windows, environment variable template with database/Redis/server-port defaults, and ignore rules for artifacts and secrets.
Spring Application Bootstrap & Core Configuration
src/main/java/com/rocketcrew/pocatbatch/PocatBatchApplication.java, src/main/java/com/rocketcrew/pocatbatch/config/*.java, src/main/resources/application.yaml, src/test/resources/application.yaml
Spring Boot entry point with scheduling enabled, configuration classes for batch and JPA auditing, main and test application properties defining datasource, Redis, batch schema initialization, server port, and custom ranking tuning parameters.
Domain Model & Repository Layer
src/main/java/com/rocketcrew/pocatbatch/domain/freepost/entity/BaseEntity.java, src/main/java/com/rocketcrew/pocatbatch/domain/freepost/entity/FreePost.java, src/main/java/com/rocketcrew/pocatbatch/domain/freepost/repository/FreePostRepository.java
JPA mapped superclass with soft-delete and audit timestamps, FreePost entity with user, title, content, and counter fields, and repository with custom JPQL queries for view/comment increments and popularity-score ranking.
Free Post Service Layer
src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java
Transactional service with REQUIRES_NEW propagation wrapping repository writes, including input validation for view increments and delegation to counter-update methods.
Free Post Ranking Job
src/main/java/com/rocketcrew/pocatbatch/job/ranking/FreePostRankingJobConfig.java, src/main/java/com/rocketcrew/pocatbatch/job/ranking/FreePostRankingTasklet.java, src/test/java/com/rocketcrew/pocatbatch/job/ranking/FreePostRankingJobConfigTest.java
Spring Batch job and step definitions, tasklet that queries top posts by popularity score, stages results in Redis under a temporary key, atomically renames to live key with TTL, and integration test verifying successful completion.
View Count Flush Job
src/main/java/com/rocketcrew/pocatbatch/job/viewcount/ViewCountFlushJobConfig.java, src/main/java/com/rocketcrew/pocatbatch/job/viewcount/ViewCountFlushTasklet.java, src/test/java/com/rocketcrew/pocatbatch/job/viewcount/ViewCountFlushJobConfigTest.java
Spring Batch job and step definitions, tasklet that reconciles Redis ZSET buffers with failed entries, renames buffer to processing key, flushes entries to database via service, and moves failures to error queue with 24-hour TTL, plus integration test.
Batch Job Scheduler & Orchestration
src/main/java/com/rocketcrew/pocatbatch/scheduler/BatchScheduler.java
Spring scheduler component injecting JobLauncher and two qualified job beans, exposing two @Scheduled(fixedDelay=60s) entry points that delegate to a shared launch helper, which builds timestamped JobParameters, executes jobs, and logs status/exceptions.
Project Documentation
README.md, docs/ADR-001-spring-batch-separation.md, docs/ARCHITECTURE.md, docs/RUNBOOK.md
ADR documenting separation rationale and parallel-operation transition plan, architecture guide covering package structure, execution flow diagrams, Redis key schema, transaction boundaries, Spring Batch meta-tables, security decisions, and known limitations, README with project scope and quick-start, and runbook with setup, startup, verification, and production deployment steps.

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant JobLauncher
  participant FreePostTasklet
  participant FreePostRepo
  participant Redis
  participant FreePostService
  Scheduler->>JobLauncher: trigger(Job, JobParameters(ts))
  JobLauncher->>FreePostTasklet: execute()
  FreePostTasklet->>FreePostRepo: findTopByPopularScore(...)
  FreePostRepo-->>FreePostTasklet: posts
  FreePostTasklet->>Redis: zadd(staging_key, scores)
  FreePostTasklet->>Redis: rename(staging_key, live_key)
  FreePostTasklet->>Redis: expire(live_key, ttl)
  alt view/comment flush
    FreePostTasklet->>Redis: zrange(processing_key)
    loop per entry
      FreePostTasklet->>FreePostService: increaseViewCount(postId,count) / updateCommentCount(postId,delta)
      FreePostService->>FreePostRepo: update counters (REQUIRES_NEW)
    end
  end
  FreePostTasklet-->>JobLauncher: COMPLETED
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A batch server hops into place,
Two jobs now race at their own pace,
Ranking posts, flushing views,
Redis buffers, database dues,
Scheduled thumps every sixty ticks! 🎪

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main change: establishing Spring Batch infrastructure by separating free-board schedulers from the main application into a dedicated batch server.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/spring-batch-infra

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
gradlew.bat (1)

1-94: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Windows batch file has Unix line endings.

The batch file uses Unix line endings (LF-only) instead of Windows line endings (CRLF), which can cause GOTO/CALL label parsing failures and script malfunction on Windows due to batch parser 512-byte boundary bugs.

🔧 Fix options

Option 1: Regenerate the Gradle wrapper

./gradlew wrapper --gradle-version 8.8

Option 2: Convert line endings manually

# On Linux/Mac with dos2unix installed
dos2unix -n gradlew.bat gradlew.bat.crlf
mv gradlew.bat.crlf gradlew.bat

# Or configure Git to handle line endings
git config core.autocrlf true
git rm --cached gradlew.bat
git add gradlew.bat

Option 3: Use an editor
Open in an editor that supports line ending conversion (VS Code, Notepad++, etc.) and save with CRLF line endings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gradlew.bat` around lines 1 - 94, The gradlew.bat file uses Unix LF line
endings which can break Windows batch parsing (affecting labels like
:findJavaFromJavaHome, :execute, :fail, :mainEnd); convert the file to CRLF line
endings and commit the change. Fix by either regenerating the wrapper (run
./gradlew wrapper --gradle-version 8.8) or converting gradlew.bat to CRLF (use
dos2unix -n gradlew.bat gradlew.bat.crlf and replace, or set git
core.autocrlf=true then re-add the file, or save with CRLF in an editor like VS
Code/Notepad++), then verify labels (:findJavaFromJavaHome, :execute, :fail,
:mainEnd) parse correctly on Windows before pushing.
🧹 Nitpick comments (8)
.env.example (1)

1-7: ⚡ Quick win

Document production security requirements.

The .env.example provides sensible defaults for local development. Consider adding a comment documenting that production deployments must:

  • Use strong database passwords (not "password")
  • Set REDIS_PASSWORD if Redis requirepass is configured
  • Secure these values using environment variables or secrets management
📝 Suggested documentation addition
+# Environment configuration template for pocat-batch
+# Copy to .env and adjust for your environment
+# PRODUCTION: Use secrets management and strong passwords
+
 DB_URL=jdbc:mysql://localhost:3306/pocat?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
 DB_USERNAME=root
-DB_PASSWORD=password
+DB_PASSWORD=password  # PRODUCTION: Use strong password
 REDIS_HOST=localhost
 REDIS_PORT=6379
-REDIS_PASSWORD=
+REDIS_PASSWORD=  # PRODUCTION: Set if Redis requirepass is enabled
 BATCH_SERVER_PORT=8081
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 1 - 7, Update .env.example to document production
security requirements: add comments near DB_PASSWORD and REDIS_PASSWORD advising
to replace the default "password" with a strong secret, to set REDIS_PASSWORD if
Redis requirepass is enabled, and to never commit credentials in
DB_URL/DB_USERNAME/DB_PASSWORD; also instruct operators to use environment
variable injection or a secrets manager (KMS/Vault/secret store) to inject
DB_URL, DB_USERNAME, DB_PASSWORD, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD and any
BATCH_SERVER_PORT overrides, and include a short note to review network/ACL and
encryption settings for production deployments.
src/main/java/com/rocketcrew/pocatbatch/PocatBatchApplication.java (1)

15-15: 💤 Low value

Consider English comments for international collaboration.

The Korean comment is clear for the current team, but English comments would improve accessibility for international contributors and future maintainers.

💬 Suggested translation
-            // OS 환경변수가 이미 존재하면 덮어쓰지 않음
+            // Don't override if OS environment variable already exists
             if (System.getenv(e.getKey()) == null) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/rocketcrew/pocatbatch/PocatBatchApplication.java` at line
15, Replace the Korean inline comment in PocatBatchApplication (the comment "//
OS 환경변수가 이미 존재하면 덮어쓰지 않음") with an English equivalent such as "// Do not
overwrite existing OS environment variables" so the intent is clear to
international contributors; keep the comment adjacent to the same code block
that checks/sets environment variables in the PocatBatchApplication class.
gradle/wrapper/gradle-wrapper.properties (1)

3-3: Verify Gradle wrapper is on a current Gradle release.

The wrapper is pinned to Gradle 8.8, while the current Gradle release is 9.5.1. Consider upgrading the wrapper to the latest stable version (after checking compatibility) to pick up bug fixes and security patches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gradle/wrapper/gradle-wrapper.properties` at line 3, Update the Gradle
wrapper distributionUrl in gradle-wrapper.properties from 8.8 to the current
stable release (9.5.1) and regenerate the wrapper to ensure all wrapper scripts
and checksums are consistent; specifically, change the distributionUrl value and
run the Gradle wrapper command (e.g., use the wrapper task with --gradle-version
9.5.1 and --distribution-type=bin) so files referenced by distributionUrl and
the wrapper (gradle-wrapper.jar / gradle-wrapper.properties) are updated
together and verified for compatibility.
src/main/java/com/rocketcrew/pocatbatch/config/RedisConfig.java (1)

11-14: ⚡ Quick win

Remove the redundant StringRedisTemplate bean and rely on Spring Boot auto-config

RedisConfig explicitly declares a StringRedisTemplate that simply wraps the provided RedisConnectionFactory (new StringRedisTemplate(connectionFactory)) with no custom serializers or Redis settings.

With Spring Boot 3.3.5 and spring-boot-starter-data-redis, Boot auto-config already supplies a StringRedisTemplate when none exists (@ConditionalOnMissingBean), so this bean can likely be deleted to use the default auto-configured one.

`@Configuration`
public class RedisConfig {

    `@Bean`
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
        return new StringRedisTemplate(connectionFactory);
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/rocketcrew/pocatbatch/config/RedisConfig.java` around lines
11 - 14, The RedisConfig class defines a redundant StringRedisTemplate bean
(method stringRedisTemplate(RedisConnectionFactory)) that just wraps the
provided RedisConnectionFactory; remove that bean (or delete the RedisConfig
class if it contains only this method) so Spring Boot's auto-configured
StringRedisTemplate (provided by spring-boot-starter-data-redis with
`@ConditionalOnMissingBean`) is used instead; ensure there are no other
customizations depending on this explicit StringRedisTemplate before deleting.
docs/RUNBOOK.md (2)

108-110: ⚡ Quick win

Add language specifier to fenced code block.

The JVM option code block is missing a language specifier.

📝 Proposed fix
-```
+```text
 -Duser.timezone=Asia/Seoul
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/RUNBOOK.md around lines 108 - 110, Update the fenced code block
containing the JVM option "-Duser.timezone=Asia/Seoul" to include a language
specifier (for example use "text" or "properties") so the block becomes text (or properties) followed by -Duser.timezone=Asia/Seoul and then the closing

the chosen language token immediately after the opening backticks.

55-59: ⚡ Quick win

Add language specifier to fenced code block.

The log output code block is missing a language specifier.

📝 Proposed fix
-```
+```text
 Executing SQL script from class path resource [org/springframework/batch/core/schema-mysql.sql]
 HikariPool-1 - Starting...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/RUNBOOK.md` around lines 55 - 59, The fenced code block containing the
log lines starting with "Executing SQL script from class path resource
[org/springframework/batch/core/schema-mysql.sql]" should include a language
specifier (e.g., add ```text) on the opening fence so the block is rendered as
plain text; update the opening fence for that snippet and keep the existing
closing triple-backticks unchanged.
docs/ARCHITECTURE.md (2)

61-76: ⚡ Quick win

Add language specifier to fenced code block.

The text diagram code block is missing a language specifier.

📝 Proposed fix
-```
+```text
 BatchScheduler(`@Scheduled` fixedDelay=60s)
   ├─ runFreePostRankingJob()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ARCHITECTURE.md` around lines 61 - 76, The fenced diagram block showing
BatchScheduler and its jobs (references: BatchScheduler, runFreePostRankingJob,
runViewCountFlushJob, FreePostRankingTasklet, ViewCountFlushTasklet) lacks a
language specifier; update the fenced code block in ARCHITECTURE.md by adding a
language tag (e.g., "text") after the opening triple backticks so the diagram
renders correctly as a plain-text code block.

7-31: ⚡ Quick win

Add language specifier to fenced code block.

The package tree code block is missing a language specifier, which markdown linters flag as a best practice violation.

📝 Proposed fix
-```
+```text
 com.rocketcrew.pocatbatch/
 ├── PocatBatchApplication
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ARCHITECTURE.md` around lines 7 - 31, The fenced code block containing
the package tree (starting with "com.rocketcrew.pocatbatch/") is missing a
language specifier; update the opening triple-backtick for that block to include
a language (e.g., use ```text) so Markdown linters accept it and keep the
closing ``` unchanged; target the fenced block that contains the package tree
lines and adjust only the opening fence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build.gradle`:
- Line 3: The build.gradle pins the Spring Boot Gradle plugin to an EOL/insecure
version; update the plugin declaration "id 'org.springframework.boot' version
'3.3.5'" to a supported release (preferably a current line like 3.5.x or 4.0.x)
or, if you must stay on 3.3, bump it to at least 3.3.18 to include the
CVE-2026-22733 fix; modify the version string in the id
'org.springframework.boot' declaration, run a dependency build to resolve
breaking changes, and adjust any Spring Boot-specific configuration or plugin
usages if upgrading across major/minor lines.

In
`@src/main/java/com/rocketcrew/pocatbatch/domain/freepost/repository/FreePostRepository.java`:
- Around line 17-23: Change the two bulk-update repository methods in
FreePostRepository (increaseViewCount and updateCommentCount) to return an int
(the number of affected rows) instead of void so callers can detect zero-row
updates; update method signatures for increaseViewCount(Long postId, int count)
and updateCommentCount(Long postId, int delta) to return int and ensure any
service-level callers check the returned value and handle 0 (retry/log/fail) per
the batch-flush policy.

In
`@src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java`:
- Around line 15-18: The increaseViewCount method in FreePostFlushService
currently allows negative counts which would decrease view_count; add a
validation at the start of increaseViewCount(Long postId, int count) to reject
negative count values (e.g., if count < 0 throw an IllegalArgumentException with
a clear message) before calling freePostRepository.increaseViewCount(postId,
count) so the repository is never invoked with a negative increment.

In
`@src/main/java/com/rocketcrew/pocatbatch/job/viewcount/ViewCountFlushTasklet.java`:
- Around line 39-44: The runSafely helper in ViewCountFlushTasklet currently
swallows exceptions by catching and only logging them; update runSafely(String
label, Runnable task) to rethrow the caught exception after logging (wrap in a
RuntimeException if needed) so Spring Batch will mark the step/job as failed;
keep the log.error call (including label and exception) and then throw the
original or wrapped exception to propagate failure to the framework.

In `@src/main/java/com/rocketcrew/pocatbatch/scheduler/BatchScheduler.java`:
- Around line 47-49: The current code calls jobLauncher.run(job, params) and
logs success unconditionally; change it to capture the returned JobExecution
(e.g., JobExecution jobExecution = jobLauncher.run(job, params)), check its
status via jobExecution.getStatus() (compare to BatchStatus.COMPLETED) and only
log "{} 실행 완료" when the status is COMPLETED; otherwise log a failure message
including jobExecution.getStatus() and any failureExceptions from
jobExecution.getAllFailureExceptions(); keep the existing catch(Exception e) to
handle thrown errors but do not rely on exceptions for non-COMPLETED outcomes.

---

Outside diff comments:
In `@gradlew.bat`:
- Around line 1-94: The gradlew.bat file uses Unix LF line endings which can
break Windows batch parsing (affecting labels like :findJavaFromJavaHome,
:execute, :fail, :mainEnd); convert the file to CRLF line endings and commit the
change. Fix by either regenerating the wrapper (run ./gradlew wrapper
--gradle-version 8.8) or converting gradlew.bat to CRLF (use dos2unix -n
gradlew.bat gradlew.bat.crlf and replace, or set git core.autocrlf=true then
re-add the file, or save with CRLF in an editor like VS Code/Notepad++), then
verify labels (:findJavaFromJavaHome, :execute, :fail, :mainEnd) parse correctly
on Windows before pushing.

---

Nitpick comments:
In @.env.example:
- Around line 1-7: Update .env.example to document production security
requirements: add comments near DB_PASSWORD and REDIS_PASSWORD advising to
replace the default "password" with a strong secret, to set REDIS_PASSWORD if
Redis requirepass is enabled, and to never commit credentials in
DB_URL/DB_USERNAME/DB_PASSWORD; also instruct operators to use environment
variable injection or a secrets manager (KMS/Vault/secret store) to inject
DB_URL, DB_USERNAME, DB_PASSWORD, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD and any
BATCH_SERVER_PORT overrides, and include a short note to review network/ACL and
encryption settings for production deployments.

In `@docs/ARCHITECTURE.md`:
- Around line 61-76: The fenced diagram block showing BatchScheduler and its
jobs (references: BatchScheduler, runFreePostRankingJob, runViewCountFlushJob,
FreePostRankingTasklet, ViewCountFlushTasklet) lacks a language specifier;
update the fenced code block in ARCHITECTURE.md by adding a language tag (e.g.,
"text") after the opening triple backticks so the diagram renders correctly as a
plain-text code block.
- Around line 7-31: The fenced code block containing the package tree (starting
with "com.rocketcrew.pocatbatch/") is missing a language specifier; update the
opening triple-backtick for that block to include a language (e.g., use ```text)
so Markdown linters accept it and keep the closing ``` unchanged; target the
fenced block that contains the package tree lines and adjust only the opening
fence.

In `@docs/RUNBOOK.md`:
- Around line 108-110: Update the fenced code block containing the JVM option
"-Duser.timezone=Asia/Seoul" to include a language specifier (for example use
"text" or "properties") so the block becomes ```text (or ```properties) followed
by -Duser.timezone=Asia/Seoul and then the closing ```; locate the fenced block
in RUNBOOK.md where the JVM option is shown and add the chosen language token
immediately after the opening backticks.
- Around line 55-59: The fenced code block containing the log lines starting
with "Executing SQL script from class path resource
[org/springframework/batch/core/schema-mysql.sql]" should include a language
specifier (e.g., add ```text) on the opening fence so the block is rendered as
plain text; update the opening fence for that snippet and keep the existing
closing triple-backticks unchanged.

In `@gradle/wrapper/gradle-wrapper.properties`:
- Line 3: Update the Gradle wrapper distributionUrl in gradle-wrapper.properties
from 8.8 to the current stable release (9.5.1) and regenerate the wrapper to
ensure all wrapper scripts and checksums are consistent; specifically, change
the distributionUrl value and run the Gradle wrapper command (e.g., use the
wrapper task with --gradle-version 9.5.1 and --distribution-type=bin) so files
referenced by distributionUrl and the wrapper (gradle-wrapper.jar /
gradle-wrapper.properties) are updated together and verified for compatibility.

In `@src/main/java/com/rocketcrew/pocatbatch/config/RedisConfig.java`:
- Around line 11-14: The RedisConfig class defines a redundant
StringRedisTemplate bean (method stringRedisTemplate(RedisConnectionFactory))
that just wraps the provided RedisConnectionFactory; remove that bean (or delete
the RedisConfig class if it contains only this method) so Spring Boot's
auto-configured StringRedisTemplate (provided by spring-boot-starter-data-redis
with `@ConditionalOnMissingBean`) is used instead; ensure there are no other
customizations depending on this explicit StringRedisTemplate before deleting.

In `@src/main/java/com/rocketcrew/pocatbatch/PocatBatchApplication.java`:
- Line 15: Replace the Korean inline comment in PocatBatchApplication (the
comment "// OS 환경변수가 이미 존재하면 덮어쓰지 않음") with an English equivalent such as "// Do
not overwrite existing OS environment variables" so the intent is clear to
international contributors; keep the comment adjacent to the same code block
that checks/sets environment variables in the PocatBatchApplication class.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a272aaff-4ce1-480c-8c28-3ba245f9029e

📥 Commits

Reviewing files that changed from the base of the PR and between b9ea696 and 9b91337.

⛔ Files ignored due to path filters (1)
  • gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
📒 Files selected for processing (28)
  • .env.example
  • .gitignore
  • README.md
  • build.gradle
  • docs/ADR-001-spring-batch-separation.md
  • docs/ARCHITECTURE.md
  • docs/RUNBOOK.md
  • gradle/wrapper/gradle-wrapper.properties
  • gradlew
  • gradlew.bat
  • settings.gradle
  • src/main/java/com/rocketcrew/pocatbatch/PocatBatchApplication.java
  • src/main/java/com/rocketcrew/pocatbatch/config/BatchConfig.java
  • src/main/java/com/rocketcrew/pocatbatch/config/JpaConfig.java
  • src/main/java/com/rocketcrew/pocatbatch/config/RedisConfig.java
  • src/main/java/com/rocketcrew/pocatbatch/domain/freepost/entity/BaseEntity.java
  • src/main/java/com/rocketcrew/pocatbatch/domain/freepost/entity/FreePost.java
  • src/main/java/com/rocketcrew/pocatbatch/domain/freepost/repository/FreePostRepository.java
  • src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java
  • src/main/java/com/rocketcrew/pocatbatch/job/ranking/FreePostRankingJobConfig.java
  • src/main/java/com/rocketcrew/pocatbatch/job/ranking/FreePostRankingTasklet.java
  • src/main/java/com/rocketcrew/pocatbatch/job/viewcount/ViewCountFlushJobConfig.java
  • src/main/java/com/rocketcrew/pocatbatch/job/viewcount/ViewCountFlushTasklet.java
  • src/main/java/com/rocketcrew/pocatbatch/scheduler/BatchScheduler.java
  • src/main/resources/application.yaml
  • src/test/java/com/rocketcrew/pocatbatch/job/ranking/FreePostRankingJobConfigTest.java
  • src/test/java/com/rocketcrew/pocatbatch/job/viewcount/ViewCountFlushJobConfigTest.java
  • src/test/resources/application.yaml

Comment thread build.gradle Outdated
Comment on lines +39 to +44
private void runSafely(String label, Runnable task) {
try {
task.run();
} catch (Exception e) {
log.error("{} 실패", label, e);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t swallow task failures in runSafely.

Line 39–44 catches and logs exceptions, then continues. That makes the step/job appear successful even when Redis/DB flush fails. Re-throw after logging so Spring Batch marks execution failed.

Proposed fix
     private void runSafely(String label, Runnable task) {
         try {
             task.run();
         } catch (Exception e) {
             log.error("{} 실패", label, e);
+            throw e;
         }
     }
📝 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 void runSafely(String label, Runnable task) {
try {
task.run();
} catch (Exception e) {
log.error("{} 실패", label, e);
}
private void runSafely(String label, Runnable task) {
try {
task.run();
} catch (Exception e) {
log.error("{} 실패", label, e);
throw e;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/rocketcrew/pocatbatch/job/viewcount/ViewCountFlushTasklet.java`
around lines 39 - 44, The runSafely helper in ViewCountFlushTasklet currently
swallows exceptions by catching and only logging them; update runSafely(String
label, Runnable task) to rethrow the caught exception after logging (wrap in a
RuntimeException if needed) so Spring Batch will mark the step/job as failed;
keep the log.error call (including label and exception) and then throw the
original or wrapped exception to propagate failure to the framework.

Comment thread src/main/java/com/rocketcrew/pocatbatch/scheduler/BatchScheduler.java Outdated
- Spring Boot 3.3.5 → 3.3.18 (CVE-2026-22733 보안 패치)
- FreePostRepository: increaseViewCount/updateCommentCount void → int (0-row 감지)
- FreePostFlushService: increaseViewCount 음수 count 방어
- BatchScheduler: JobExecution.status 체크 후 분기 로그
- RedisConfig 삭제 (Boot auto-config 중복)
- .env.example 운영 보안 가이드 주석 추가
- ARCHITECTURE.md, RUNBOOK.md fenced block language specifier 추가

[거절] runSafely rethrow: 두 flush 독립 실행 보장이 설계 의도
[거절] gradlew.bat CRLF: WSL/Linux 환경 전용, 실사용 없음
[거절] Gradle 9.5.1: Spring Boot 3.x 호환성 미검증
[거절] 한국어 주석 영어화: 한국어 팀 프로젝트

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java (1)

20-20: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle zero-row updates as failures in flush writes.

Line 20 and Line 25 ignore affected-row counts, so stale/missing postId writes become silent success and can drop increments instead of moving them to the failed buffer path.

🔧 Suggested fix
 `@Transactional`(propagation = Propagation.REQUIRES_NEW)
 public void increaseViewCount(Long postId, int count) {
     if (count < 0) {
         throw new IllegalArgumentException("View count increment must not be negative: " + count);
     }
-    freePostRepository.increaseViewCount(postId, count);
+    int affected = freePostRepository.increaseViewCount(postId, count);
+    if (affected == 0) {
+        throw new IllegalStateException("increaseViewCount failed: post not found, postId=" + postId);
+    }
 }
 
 `@Transactional`(propagation = Propagation.REQUIRES_NEW)
 public void updateCommentCount(Long postId, int delta) {
-    freePostRepository.updateCommentCount(postId, delta);
+    int affected = freePostRepository.updateCommentCount(postId, delta);
+    if (affected == 0) {
+        throw new IllegalStateException("updateCommentCount failed: post not found, postId=" + postId);
+    }
 }

Also applies to: 25-25

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java`
at line 20, The call to freePostRepository.increaseViewCount(postId, count) must
check the affected-row return value and treat a zero result as a failure instead
of a silent success: capture the returned int (e.g., int updated =
freePostRepository.increaseViewCount(postId, count)); if updated == 0 then route
the increment to the existing failed-write path (enqueue/move to the failed
buffer or call the method used elsewhere to record failed writes) and log the
condition; otherwise continue as success. Also apply the same check to the other
similar call on line 25 so zero-row updates are consistently handled, and ensure
exceptions still fall back to the failed buffer path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 16-21: Move inline comments out of the variable lines in
.env.example so dotenv parsers and linters don't misinterpret them: put
explanatory comments on their own lines above the keys (e.g., a comment line
explaining DB_PASSWORD before DB_PASSWORD=), ensure DB_PASSWORD has only the
example value (no trailing inline comment), and make the empty Redis password
explicit by setting REDIS_PASSWORD= (no inline comment) with a preceding comment
line explaining when to fill it; also remove trailing spaces after values for
DB_PASSWORD, REDIS_HOST, and REDIS_PORT.

---

Duplicate comments:
In
`@src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java`:
- Line 20: The call to freePostRepository.increaseViewCount(postId, count) must
check the affected-row return value and treat a zero result as a failure instead
of a silent success: capture the returned int (e.g., int updated =
freePostRepository.increaseViewCount(postId, count)); if updated == 0 then route
the increment to the existing failed-write path (enqueue/move to the failed
buffer or call the method used elsewhere to record failed writes) and log the
condition; otherwise continue as success. Also apply the same check to the other
similar call on line 25 so zero-row updates are consistently handled, and ensure
exceptions still fall back to the failed buffer path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 11555edb-36af-4b07-8d2a-78b9337cfeae

📥 Commits

Reviewing files that changed from the base of the PR and between 9b91337 and f430012.

📒 Files selected for processing (7)
  • .env.example
  • build.gradle
  • docs/ARCHITECTURE.md
  • docs/RUNBOOK.md
  • src/main/java/com/rocketcrew/pocatbatch/domain/freepost/repository/FreePostRepository.java
  • src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java
  • src/main/java/com/rocketcrew/pocatbatch/scheduler/BatchScheduler.java
✅ Files skipped from review due to trivial changes (2)
  • docs/RUNBOOK.md
  • docs/ARCHITECTURE.md

Comment thread .env.example Outdated
- .env.example: inline comment 제거 (dotenv-java inline comment 미지원)
- FreePostFlushService: 0-row UPDATE 시 IllegalStateException throw
  (Tasklet catch에서 failedKey 재적재로 이어짐)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java`:
- Around line 21-23: Replace the current IllegalStateException thrown in
FreePostFlushService when updated == 0 with a terminal handling path: either (A)
log a warning (processLogger.warn / logger.warn) and return from the method so
the missing/deleted postId is not retried, or (B) create and throw a dedicated
unchecked exception (e.g., NonRetryableFlushException) instead of
IllegalStateException and update ViewCountFlushTasklet.flushKey(...) to treat
NonRetryableFlushException as non-retryable (do not requeue to :failed). Locate
the updated==0 checks in FreePostFlushService (both occurrences) and implement
one of these fixes and, if you choose option B, add the new exception type and
adjust the tasklet's catch/requeue logic to exclude it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 727337ea-a503-407c-9249-db3c7e4a6698

📥 Commits

Reviewing files that changed from the base of the PR and between f430012 and 33a7494.

📒 Files selected for processing (2)
  • .env.example
  • src/main/java/com/rocketcrew/pocatbatch/domain/freepost/service/FreePostFlushService.java

- FreePostFlushService: 0-row UPDATE 시 IllegalStateException 제거
  IllegalStateException → failedKey 재적재 → 무한 루프 문제
  삭제된 게시글은 SQLRestriction으로 영원히 0-row → 재시도 불필요
  log.warn + return으로 교체 (non-retryable terminal path)
@cdkkyj123
cdkkyj123 merged commit a952831 into main May 24, 2026
1 check passed
@cdkkyj123
cdkkyj123 deleted the feat/spring-batch-infra branch May 24, 2026 03:20
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