You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
이슈 #70(PHOTO-02/03 파일 수·용량 제한) 검증 과정에서 발견한 별도 문제입니다.
PhotoUploadService.submitThumbnailJobsAfterCommit은 PHOTO-03 트랜잭션 커밋 직후, 완료 등록을 처리하던 바로 그 요청 스레드에서 각 사진의 썸네일 생성 작업을 thumbnailExecutor(ThreadPoolTaskExecutor, core=4/max=6/큐 100 — 전부 미검증 하드코딩 기본값, application.yaml 등 어디에도 오버라이드나 근거 없음)에 동기적으로 제출합니다.
ThreadPoolTaskExecutor의 기본 RejectedExecutionHandler는 AbortPolicy라, 스레드 6개가 모두 사용 중이고 큐(100개)도 가득 차면 제출 시 RejectedExecutionException을 던집니다. 문제는 이 시점에 사진은 이미 DB에 정상적으로 생성·커밋된 뒤라는 점입니다. 즉 실제로는 업로드가 성공했는데도, 썸네일 작업 제출이 큐 초과로 예외를 던지면 그 예외가 그대로 전파되어 HTTP 응답이 500으로 나갈 위험이 있습니다(사진 row는 이미 커밋되어 살아있는데 응답만 실패로 보이는 상태 불일치).
실측 결과 (조사용 벤치마크, PhotoUploadBatchLoadBenchmarkTest)
Object Storage 지연 80ms를 흉내 낸 환경에서 동시 사용자 3명이 각자 20개 배치를 완료 등록했을 때:
완료 직후 thumbnailExecutor 활성 스레드 4, 대기 큐 56/100
이 서비스는 같은 모임·행사에서 여러 명이 비슷한 시점에 몰아서 업로드하는 패턴이 자연스러운데, 사용자 56명만 겹쳐도 (56 × 20 = 100~120) 큐가 가득 차는 시나리오가 드문 예외가 아니라 흔한 정상 케이스일 수 있습니다.
✅ Tasks
큐 초과(RejectedExecutionException) 시 응답이 깨지지 않도록 안전하게 처리 — 로그만 남기고 thumbnailStatus를 PENDING으로 남겨 기존 PhotoSweepScheduler 복구 경로(재시작으로 작업 유실 시와 동일한 경로)를 타도록 수정. 리뷰 과정에서 PhotoSweepScheduler.resubmitStaleThumbnailJobs()(재수거 스윕 자체)에도 같은 결함이 있는 걸 발견해 함께 수정했습니다.
위 수정 이후, 동시 사용자 6~10명 × 20개 배치 등 현실적인 부하로 재측정해 큐가 실제로 어느 시점에 얼마나 차는지, 그 상태에서 썸네일 준비 지연이 체감상 허용 가능한지 확인 → #107로 분리
측정 근거로 thumbnail.executor.queue-capacity(및 필요 시 core/max-pool-size)를 application.yaml에 명시적으로 설정하고 근거를 의사결정 로그에 기록 → #107로 분리
📎 ETC
대기 중인 큐 항목은 이미지 바이트가 아니라 photoId(UUID) 참조만 들고 있어 큐 용량 자체를 늘리는 메모리 비용은 크지 않음 — 실행 중인 작업(최대 4~6개)의 메모리 비용(파일 크기가 아니라 해상도에 비례, 관련 조사는 이슈 [Feat] 사진 업로드 파일 수 및 용량 제한 조정 #70 참고)과는 별개 문제.
재배포 시 큐에 남은 작업은 유실되고 스윕이 나중에 복구하는 구조라(docs/architecture/backend-architecture.md 4.3), 큐를 넉넉히 잡아도 치명적이진 않으나 유실 폭이 커지는 트레이드오프는 있음.
PhotoUploadService.completeUpload()의 afterCommit() 콜백에서 thumbnailProcessingService.process() 호출을 try/catch로 감싸, RejectedExecutionException(Spring의 TaskRejectedException 포함) 발생 시 로그만 남기고 넘어가도록 수정. 사진은 기본값인 PENDING으로 남아 PhotoSweepScheduler가 재수거합니다.
리뷰 과정에서 같은 결함이 PhotoSweepScheduler.resubmitStaleThumbnailJobs()(썸네일 재수거 스윕 자체)에도 있는 것을 발견해 같은 방식으로 수정했습니다. 큐가 지속적으로 가득 찬 상태에서는 스윕 배치의 첫 사진에서 예외가 나면 나머지 사진들이 그 사이클에서 조용히 누락되는 문제였는데, 이 결함이 바로 이 이슈가 우려하던 "안전망"(PhotoSweepScheduler가 재수거한다는 전제) 자체에 있었습니다.
단위 테스트 2개, 통합 테스트 1개(실제 커밋 후 HTTP 201 확인) 추가. 전체 테스트 스위트 통과.
남은 작업
동시 사용자 부하 재측정과 thumbnail.executor.queue-capacity 명시적 설정은 실제 인프라에서의 측정이 필요해 이번 수정 범위에서 분리해 #107로 옮겼습니다.
Activity
hamtorygoals commentedon Jul 17, 2026
확인 결과 이 이슈는 최근 변경들과 무관하게 여전히 유효했고, 아래와 같이 수정했습니다.
수정 내용
PhotoUploadService.completeUpload()의afterCommit()콜백에서thumbnailProcessingService.process()호출을 try/catch로 감싸,RejectedExecutionException(Spring의TaskRejectedException포함) 발생 시 로그만 남기고 넘어가도록 수정. 사진은 기본값인PENDING으로 남아PhotoSweepScheduler가 재수거합니다.PhotoSweepScheduler.resubmitStaleThumbnailJobs()(썸네일 재수거 스윕 자체)에도 있는 것을 발견해 같은 방식으로 수정했습니다. 큐가 지속적으로 가득 찬 상태에서는 스윕 배치의 첫 사진에서 예외가 나면 나머지 사진들이 그 사이클에서 조용히 누락되는 문제였는데, 이 결함이 바로 이 이슈가 우려하던 "안전망"(PhotoSweepScheduler가 재수거한다는 전제) 자체에 있었습니다.남은 작업
동시 사용자 부하 재측정과
thumbnail.executor.queue-capacity명시적 설정은 실제 인프라에서의 측정이 필요해 이번 수정 범위에서 분리해 #107로 옮겼습니다.🐛 fix: 썸네일 큐 초과 시 성공한 업로드가 500으로 응답되는 문제 수정