Skip to content

fix: 키워드 알림 스레드 점유 문제 개선#2236

Merged
Soundbar91 merged 10 commits into
developfrom
fix/2191-keyword-notification-thread
May 8, 2026
Merged

fix: 키워드 알림 스레드 점유 문제 개선#2236
Soundbar91 merged 10 commits into
developfrom
fix/2191-keyword-notification-thread

Conversation

@Soundbar91
Copy link
Copy Markdown
Collaborator

@Soundbar91 Soundbar91 commented May 7, 2026

🔍 개요


🚀 주요 변경 내용

키워드 알림 전송 로직 비동기 처리

  • ArticleKeywordEventListener@Async("keywordNotificationTaskExecutor") 적용
  • POST /articles/keyword/notification 요청을 처리하는 Tomcat 스레드가 FCM 전송 완료까지 점유되지 않도록 분리
  • 키워드 알림 전송은 DB 조회, FCM 호출, 발송 이력 저장을 포함하는 I/O 중심 작업이므로 별도 executor에서 처리하도록 변경

키워드 알림 전용 ThreadPoolTaskExecutor 추가

  • corePoolSize: 2
    • executor가 기본적으로 유지하는 스레드 수
    • 최근 1년간 배치 크롤링 게시글 수의 평균은 2.51개, P50은 2개
    • 실제 executor 작업은 전체 크롤링 게시글이 아니라 키워드가 매칭된 게시글만 대상이므로 기본 처리 스레드는 2개로 설정
  • maxPoolSize: 6
    • queue가 가득 찼을 때 executor가 추가로 생성할 수 있는 최대 스레드 수
    • 최근 1년간 배치 크롤링 게시글 수의 P95가 6개
    • 피크 상황에서 키워드가 매칭된 게시글이 여러 개 발생해도 일정 수준까지 병렬 처리할 수 있도록 설정
    • 운영 서버가 2 vCPU / 2GB 메모리인 점을 고려해 과도한 스레드 증가는 제한
  • queueCapacity: 5
    • 순간적으로 core thread보다 많은 작업이 들어오는 경우를 위한 짧은 버퍼
    • 큐를 크게 잡으면 알림 작업이 오래 대기할 수 있으므로, 지연을 줄이기 위해 작은 값으로 설정
  • keepAliveSeconds: 60
    • core thread를 초과해 생성된 스레드는 작업 처리 이후 60초 동안 유휴 상태가 지속되면 종료
    • 배치 호출이 특정 시간대에 집중되는 특성을 고려해 불필요한 스레드 유지 시간을 제한
  • threadNamePrefix: keyword-notification-
    • 로그와 APM에서 키워드 알림 executor에서 실행되는 작업을 구분하기 위한 접두어 설정
  • RejectedExecutionHandler: CallerRunsPolicy
    • queue와 thread가 모두 찬 경우 호출자 스레드가 직접 작업을 처리
    • 알림 작업이 reject로 유실되지 않도록 설정

-> 초기값을 여유롭게 잡았으며, 프로덕션에 적용을 한 후 모니터링을 통해 값을 수정할 예정입니다.


💬 참고 사항


✅ Checklist (완료 조건)

  • 코드 스타일 가이드 준수
  • 테스트 코드 포함됨
  • Reviewers / Assignees / Labels 지정 완료
  • 보안 및 민감 정보 검증 (API 키, 환경 변수, 개인정보 등)

Summary by CodeRabbit

  • Keyword notification processing now runs asynchronously, improving system responsiveness and preventing potential bottlenecks
  • Notifications are guaranteed to complete after database transaction commits, ensuring data consistency and reliability
  • System efficiently handles keyword notifications without blocking other critical operations

@Soundbar91 Soundbar91 self-assigned this May 7, 2026
@github-actions github-actions Bot added 성능개선 기능개선과 관련된 이슈입니다. 버그 정상적으로 동작하지 않는 문제상황입니다. labels May 7, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Unit Test Results

672 tests   669 ✔️  1m 19s ⏱️
167 suites      3 💤
167 files        0

Results for commit f2bc962.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown
Contributor

@dh2906 dh2906 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!
아래는 제가 스레드 풀 설정 관련 궁금한 내용을 질문으로 남깁니다!!

  1. ThreadPoolTaskExecutor는 유저에게 알림을 보내기 위한 스레드 풀이 맞나요?
  2. 1번이 맞다면 corePoolSize 값은 결국 게시글 1개에 키워드가 매칭된 유저가 n명이면 알림을 n번 보내야 하니 "게시글 하나에 매칭된 유저가 몇 명인지" 를 기준으로 봐야하는 것 아닌가? 라는 생각이 듭니다.

Copy link
Copy Markdown
Collaborator Author

@Soundbar91 Soundbar91 left a comment

Choose a reason for hiding this comment

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

ThreadPoolTaskExecutor는 유저에게 알림을 보내기 위한 스레드 풀이 맞나요?

키워드가 매칭된 게시글에 대해서 발행된 이벤트를 처리하기 위한 스레드 풀입니다 ! 게시글이 4개가 들어왔을 때, 키워드가 매칭된 게시글이 2개가 존재한다면 2개의 알림 이벤트가 발생되고 이를 위한 스레드를 스레드 풀에서 생성해서 가져옵니다.

PR를 보니 설명이 많이 부족한 감이 있었네요.. 다음 PR부터는 보완하겠습니당

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

Warning

Rate limit exceeded

@Soundbar91 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 13 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 971cf517-ed1d-4cab-8d8f-54eed2cf88f1

📥 Commits

Reviewing files that changed from the base of the PR and between 049bab9 and f2bc962.

📒 Files selected for processing (1)
  • src/main/java/in/koreatech/koin/global/config/AsyncConfig.java
📝 Walkthrough

Walkthrough

ArticleKeywordEventListener is reconfigured to execute keyword event processing asynchronously via a dedicated ThreadPoolTaskExecutor bean. Keyword event handling is deferred until after the surrounding database transaction commits, separating Tomcat request thread handling from background FCM notification delivery.

Changes

Async Keyword Notifications

Layer / File(s) Summary
Async Infrastructure & Executor Bean
src/main/java/in/koreatech/koin/global/config/AsyncConfig.java
New keywordNotificationTaskExecutor() bean provides a ThreadPoolTaskExecutor configured with core pool size 2, max pool size 5, queue capacity 100, keep-alive time 60s, and CallerRunsPolicy rejection handling for keyword notification delivery.
Event Handler Async Configuration
src/main/java/in/koreatech/koin/domain/notification/eventlistener/ArticleKeywordEventListener.java
onKeywordRequest() method is decorated with @Async("keywordNotificationTaskExecutor") and @TransactionalEventListener(phase = AFTER_COMMIT) to defer processing until transaction commit and execute on the dedicated executor. Imports updated to explicitly reference ARTICLE_KEYWORD, LOST_ITEM_KEYWORD, and AFTER_COMMIT constants.
Annotation Verification Test
src/test/java/in/koreatech/koin/unit/domain/notification/eventlistener/ArticleKeywordEventListenerTest.java
New unit test uses reflection to assert @Async and @TransactionalEventListener(AFTER_COMMIT) annotations are correctly applied to onKeywordRequest() method.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

공통

Suggested reviewers

  • kih1015
  • kongwoojin
  • skdud0629

Poem

📬 A rabbit hops through async threads,
Defers the keyword work 'til commit,
Tomcat threads freed from FCM's threads—
Now notification flows fit 🚀
P95 drops, performance is lit! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 title 'fix: 키워드 알림 스레드 점유 문제 개선' (Fix: Improve keyword notification thread occupancy issue) accurately summarizes the main change of making keyword notification processing asynchronous to resolve thread blocking.
Linked Issues check ✅ Passed All objectives from issue #2191 are met: ArticleKeywordEventListener is now async [#2191], ThreadPoolTaskExecutor is configured [#2191], and unit tests verify async behavior [#2191].
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue objectives: adding @Async annotation, configuring ThreadPoolTaskExecutor, and adding tests for async behavior verification.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2191-keyword-notification-thread

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

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

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: 1

🧹 Nitpick comments (1)
src/main/java/in/koreatech/koin/global/config/AsyncConfig.java (1)

34-34: CallerRunsPolicy can reintroduce the original Tomcat thread-occupancy problem under saturation.

When the executor is fully saturated (maxPoolSize=6 threads busy + queueCapacity=5 slots full = 11 concurrent tasks), the 12th submission is executed directly by the submitting thread. In Spring's async event infrastructure, that submitting thread is the post-commit synchronization thread — effectively the Tomcat request thread. Under a burst of keyword-match events, this silently falls back to synchronous FCM execution on the request thread, which is exactly the behaviour this PR aims to eliminate.

Consider whether a larger queueCapacity, an AbortPolicy with a logged drop metric, or a secondary bounded queue + async drain would better meet the resilience requirement without the silent fall-back.

🤖 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/in/koreatech/koin/global/config/AsyncConfig.java` at line 34,
The current use of CallerRunsPolicy in AsyncConfig
(executor.setRejectedExecutionHandler(new CallerRunsPolicy())) can force work
onto the Tomcat request thread; replace it with a bounded-failure strategy:
change the rejected handler to ThreadPoolExecutor.AbortPolicy or a custom
RejectedExecutionHandler that logs the rejection and increments a drop metric
(e.g., implement new RejectedExecutionHandler that calls
metrics.counter("async.rejected").increment() and processLogger.warn(...) before
throwing RejectedExecutionException), or alternatively increase queueCapacity on
the executor to a higher sensible value; update the executor configuration (in
AsyncConfig where setRejectedExecutionHandler is called) to use one of these
approaches to avoid silent synchronous fallback.
🤖 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/in/koreatech/koin/global/config/AsyncConfig.java`:
- Around line 26-37: The ThreadPoolTaskExecutor bean
keywordNotificationTaskExecutor currently doesn't wait for in-flight tasks on
shutdown; update the bean construction to call
executor.setWaitForTasksToCompleteOnShutdown(true) and set an appropriate
timeout via executor.setAwaitTerminationSeconds(<seconds>) so notification FCM
calls and keywordService.createNotifiedArticleStatus() complete during shutdown;
ensure these calls are applied on the ThreadPoolTaskExecutor instance before
executor.initialize() to enable graceful shutdown.

---

Nitpick comments:
In `@src/main/java/in/koreatech/koin/global/config/AsyncConfig.java`:
- Line 34: The current use of CallerRunsPolicy in AsyncConfig
(executor.setRejectedExecutionHandler(new CallerRunsPolicy())) can force work
onto the Tomcat request thread; replace it with a bounded-failure strategy:
change the rejected handler to ThreadPoolExecutor.AbortPolicy or a custom
RejectedExecutionHandler that logs the rejection and increments a drop metric
(e.g., implement new RejectedExecutionHandler that calls
metrics.counter("async.rejected").increment() and processLogger.warn(...) before
throwing RejectedExecutionException), or alternatively increase queueCapacity on
the executor to a higher sensible value; update the executor configuration (in
AsyncConfig where setRejectedExecutionHandler is called) to use one of these
approaches to avoid silent synchronous fallback.
🪄 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

Run ID: 143d4e16-096e-4590-b81f-30ae4f6123b3

📥 Commits

Reviewing files that changed from the base of the PR and between d7d3fbf and 049bab9.

📒 Files selected for processing (3)
  • src/main/java/in/koreatech/koin/domain/notification/eventlistener/ArticleKeywordEventListener.java
  • src/main/java/in/koreatech/koin/global/config/AsyncConfig.java
  • src/test/java/in/koreatech/koin/unit/domain/notification/eventlistener/ArticleKeywordEventListenerTest.java

Comment thread src/main/java/in/koreatech/koin/global/config/AsyncConfig.java
@Soundbar91 Soundbar91 merged commit ff6fb80 into develop May 8, 2026
6 checks passed
@Soundbar91 Soundbar91 deleted the fix/2191-keyword-notification-thread branch May 8, 2026 04:31
@dh2906
Copy link
Copy Markdown
Contributor

dh2906 commented May 8, 2026

키워드가 매칭된 게시글에 대해서 발행된 이벤트를 처리하기 위한 스레드 풀입니다 ! 게시글이 4개가 들어왔을 때, 키워드가 매칭된 게시글이 2개가 존재한다면 2개의 알림 이벤트가 발생되고 이를 위한 스레드를 스레드 풀에서 생성해서 가져옵니다.

아하! 이해했습니다 감사합니당

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

버그 정상적으로 동작하지 않는 문제상황입니다. 성능개선 기능개선과 관련된 이슈입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[공통] 키워드 알림 스레드 점유 문제 개선

2 participants