Skip to content

[refactor] @Async 풀 분리, 거부정책, MDC 전파로 비동기 작업 격리 - #167

Merged
gihhyeon merged 5 commits into
developfrom
refactor/166-async-pool-separation
Sep 14, 2026
Merged

gihhyeon merged 5 commits into
developfrom
refactor/166-async-pool-separation

Conversation

@gihhyeon

Copy link
Copy Markdown
Member

요약

단일 공용 스레드 풀을 성격별로 2개로 분리(격벽)하고, 풀 포화 시 거부정책을 지정하며, 스레드 경계를 넘어도 요청 traceId가 이어지도록 MDC를 전파한다.

What: 무엇을 바꿨나

  • @Async 스레드 풀을 2개로 분리
    • externalExecutor: Discord 전용(외부 I/O 격리), core 1 / max 2 / queue 30, DiscardPolicy
    • generalExecutor(기본): 분석, 조회수, 응모, core 3 / max 4 / queue 200, log & discard
  • 4개 @Async 지점에 사용할 풀을 명시적으로 지정
  • graceful shutdown 설정(종료 시 진행 중 작업 대기)
  • MdcTaskDecorator로 두 풀에 MDC(traceId) 전파 + 실행 후 정리
  • 테스트 추가: 풀 구성 검증(AsyncConfigurationTest), MDC 전파 단위 검증(MdcTaskDecoratorTest)

Why: 왜 필요한가

  • 풀이 하나라, 느린 외부 HTTP(Discord .block())가 스레드를 오래 점유하면 조회수, 분석 같은 짧은 내부 작업까지 같은 큐에 밀려 지연, 유실됐다.
  • 거부정책이 기본값(AbortPolicy)이라 풀 포화 시 RejectedExecutionException이 요청 흐름으로 전파될 수 있었다.
  • @Async로 스레드가 바뀌면 MDC가 끊겨 async 내부 로그를 원 요청과 연결할 수 없었다.

How: 어떻게 구현했나

  • 커밋 2개로 분리: refactor(풀 분리, 거부정책) 다음 feat(MDC 전파), 안정성 변경과 관측성 변경을 나눔
  • 풀 크기: generalExecutor는 DB 트랜잭션을 점유하므로, Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 max 4로 제한(웹용 커넥션 확보)
  • MDC 전파: TaskDecorator가 제출 스레드의 MDC를 복사해 풀 스레드에서 복원, 재사용 오염 방지를 위해 실행 후 clear, [feature] 요청별 traceId MDC 로깅 도입 #163(요청별 traceId)과 연결되어 실효

체크리스트

  • 불필요한 주석/코드 제거
  • 테스트 추가/갱신 및 통과 (./gradlew build)
  • 공개 계약 영향 확인 (엔드포인트, 응답 형태 변경 없음)

관련 이슈

기타 / 리뷰 포인트

  • generalExecutor 거부정책 확정 필요 (#166의 "결정 필요" 항목): 현재 log & discard로 구현
    • ViewCountService가 @Transactional(REQUIRES_NEW)이고 getPlaceDetail의 활성 트랜잭션 안에서 호출되어, CallerRunsPolicy는 커넥션 2개 동시 요구로 Hikari(10) 포화 시 데드락 위험이 있어 배제했다.
    • 무손실이 꼭 필요하면 Hikari 상향과 CallerRunsPolicy가 세트여야 하나 t2.micro라 별도 범위로 둔다. 이 선택이 적절한지 리뷰 요망
  • 풀 크기(3/4/200, 1/2/30)는 측정값이 아니라 제약(Hikari 10)에서 역산한 시작값, 운영 지표(hikaricp.connections.pending 등) 보고 조정 필요

외부 I/O와 내부 DB 작업을 별도 스레드 풀로 분리해 상호 간섭을 막는다.
- Discord는 externalExecutor, 내부 작업(분석·조회수·응모)은 generalExecutor로 격리
- 풀 포화 시 예외가 요청으로 전파되지 않도록 거부정책 지정
- Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 general을 max 4로 제한
- 종료 시 진행 중 작업을 기다리도록 graceful shutdown 설정
@async는 작업을 다른 풀 스레드로 넘겨 ThreadLocal인 MDC가 끊긴다. 부모 스레드의 MDC를 복사·복원해 async 로그에도 traceId가 남게 한다.
- MdcTaskDecorator를 general·external 풀에 적용
- 풀 스레드 재사용 시 이전 요청 컨텍스트가 남지 않도록 실행 후 MDC 정리
@jcw1031
jcw1031 self-requested a review September 12, 2026 12:16

@jcw1031 jcw1031 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

취중리뷰

executor.setRejectedExecutionHandler(rejectedHandler);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(SHUTDOWN_AWAIT_SECONDS);
executor.initialize();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

참고용) 스프링 빈으로 등록하는 경우에는 자동으로 initialize()가 호출되긴 해

.threadNamePrefix("Async-")
.build();
public ThreadPoolTaskExecutor externalExecutor() {
return buildExecutor("ext-", 1, 2, 30, new ThreadPoolExecutor.DiscardPolicy());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

커스텀으로 정의한 loggingDiscardPolicy는 로그로 풀 포화 및 작업 유실을 인지할 수 있는데 이거는 조용히 무시하는 거라 인지가 어려울 것 같네

external 스레드 풀은 인지할 필요가 없다고 생각해서 DiscardPolicy를 사용한 거야?

@gihhyeon gihhyeon Sep 14, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

알림 쪽이라 유실되는 거 몰라도 상관 없다고 생각해서 사용한 거 맞습니당
근데 다시 생각해보니 유실되는 거 알면 좋을 것 같아서 로그 남기는 쪽으로 바꾸는 것도 괜찮을 것 같네여

}

@Async
@Async("generalExecutor")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

상수화하는 건 어떨까?
스프링 빈 정의할 때도 메서드 네이밍으로 빈 이름을 지정한 것 같은데 만약 메서드 이름이 변경되는 경우 @Async에 설정된 이름을 같이 변경해주는 걸 누락하면 빈 이름이 틀어질 수 있을 것 같아

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

생각 못한 부분이네요 반영하겠습니다!!

@Bean
public ThreadPoolTaskExecutor generalExecutor() {
// Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 max 4로 제한
return buildExecutor("async-", 3, 4, 200, loggingDiscardPolicy());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

풀 사이즈와 큐 용량은 운영해보면서 설정 조정해봐도 좋을 것 같아

generalExecutor는 DB에서 조회수 증가시키는 작업을 수행하는 것 같은데, 만약 DB 지연이 발생하면(사용자들 조회가 많다고 가정했을 때) 스레드 3개로 부족할 거고 큐가 빠른 속도로 찰 수 있는데 max는 4개밖에 안 돼서 쉽게 유실될 수 있을 것 같아

물론 조회수 증가라 엄청 크리티컬하지는 않지만

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

넵 이거는 일단 이렇게 나눠두고 운영하면서 조정해보려고 했습니다!

@gihhyeon gihhyeon self-assigned this Sep 14, 2026
@gihhyeon
gihhyeon merged commit be86d7a into develop Sep 14, 2026
2 checks passed
@gihhyeon
gihhyeon deleted the refactor/166-async-pool-separation branch September 15, 2026 04:22
@leeleeleeleejun

Copy link
Copy Markdown
Member

취중리뷰

ㄹㅈㄷ 비호

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.

[refactor] @Async 풀 분리 · 거부정책 · MDC 전파로 비동기 작업 격리

3 participants