-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 콘텐츠 수집 - 배치 작업 구현 #244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
6a7244e
feat: Spring Batch 의존성 추가 및 Spring Batch 메타데이터 테이블 추가
plzslp b7302ba
feat: Spring Batch 콘텐츠 수집 Reader 구현
plzslp 4ead325
feat: Spring Batch 콘텐츠 수집 Processor 구현
plzslp 71d2f10
feat: TMDB/SportsDB 공통 ContentItemWriter 추가
plzslp 1f012a4
feat: BatchConfig 추가
plzslp bb07e0a
feat: SportsDB 일별 경기 수집 배치 추가
plzslp ebf8084
feat: Spring Batch 콘텐츠 수집 스케줄러 구현
plzslp cfec809
refactor: 스케줄링 설정 분리 및 배치 비동기 실행 환경 구성
plzslp 1118d42
refactor: 콘텐츠 수집 로직을 Async 서비스에서 Spring Batch Job으로 전환
plzslp 4ad169c
refactor: ContentCollectionControllerTest 리펙토링
plzslp f580580
feat: 콘텐츠 수집 엔드포인트 입력값 검증 추가
plzslp 2e71aad
refactor: Writer 로직에서 externalId 중복 제거 로직 추가
plzslp 4518fbc
feat: SportsDB 일별 수집에 날짜 파라미터 추가 및 엔드포인트 추가
plzslp 4e50848
refactor: PageRangeRequest 파라미터를 필수값으로 변경
plzslp e168306
test: ItemReader 테스트 코드 작성
plzslp f0e8590
test: ItemProcessor 테스트 코드 작성
plzslp 933d69c
test: ItemWriter 테스트 코드 작성
plzslp f8b67ba
fix: ContentCollectionScheduler도 비동기 처리로 수정
plzslp 9df8d67
fix: TMDB Reader 빈 페이지에서 Job 조기 종료 버그 수정
plzslp 4248686
refactor: ContentItemWriter 썸네일 저장 방식 개선 및 dead code 제거
plzslp ed86c25
docs: 주석 수정
plzslp 06fb5ba
refactor: 배치 Job 실행 스레드 관리 및 Reader 상태 격리 개선
plzslp e0b5d2c
fix: SportsDB 일별 수집 date 파라미터가 실존하지 않는 날짜를 통과시키는 문제 수정
plzslp 41d3fcf
feat: SportsDB 일별 수집 리그별 재시도 및 상태코드 기반 재시도 정책 추가
plzslp dd5a877
fix: TMDB 빈 페이지 응답 시 조기 종료하도록 수정
plzslp f0d34c4
fix: Scheduled, LocalDate.now에 타임 존 설정 추가
plzslp 8ac9103
fix: 중복 썸네일은 별도의 BinaryContent로 연결되도록 수정
plzslp 0ee5494
fix: Job 실행 실패 시 예외를 삼키지 않고 BatchJobLaunchException으로 던짐
plzslp 0696250
fix: PageRangeRequest 필드 검증과 중복되는 클래스 레벨 검증 제거
plzslp 6c64153
test: TMDB endPage 클램핑 테스트가 실제로 검증하도록 수정
plzslp 41a0e50
test: ContentItemWriter 태그 조회/생성/연결 테스트 추가
plzslp d162395
refactor: MethodArgumentTypeMismatchException 핸들러 메서드 추가로 인한 UserCont…
plzslp e4e9bf2
docs: 기존 콘텐츠 테스트 코드 given/when/then 주석 추가
plzslp 4c78bf8
refactor: TMDB Reader의 외부 API 호출을 BeforeStep으로 이동
plzslp 4ff1eeb
fix: noSkip이 재시도 정책의 상태코드 판단을 무력화하던 문제 수정
plzslp e60b992
fix: BatchJobLaunchException이 예외 원인을 유지하도록 수정
plzslp 7e1cf5e
fix: 스케줄러 Job 실패 로그에 스택트레이스 추가, 잘못된 league 값 테스트 추가
plzslp 5cfaafd
fix: V14, V15 schema.sql 추가로 인한 Spring Batch 메타데이터 sql문 버전 변경
plzslp 1c60e82
fix: TMDB genreIds 누락 시 NPE 방지
plzslp 1096668
fix: SportsDbEventItemReader에 재시도 로직 누락 수정
plzslp c1809a4
fix: 콘텐츠 수집 컨트롤러, 스케줄러 예외 로깅 개선
plzslp a334cad
refactor: 미사용 코드 정리 및 테스트 보강
plzslp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 22 additions & 26 deletions
48
src/main/java/com/codeit/team5/mopl/config/AsyncConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 228 additions & 0 deletions
228
src/main/java/com/codeit/team5/mopl/config/BatchConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| package com.codeit.team5.mopl.config; | ||
|
|
||
| import com.codeit.team5.mopl.binarycontent.repository.BinaryContentRepository; | ||
| import com.codeit.team5.mopl.content.batch.dto.ContentWithMetaData; | ||
| import com.codeit.team5.mopl.content.batch.processor.SportsDbEventItemProcessor; | ||
| import com.codeit.team5.mopl.content.batch.processor.TmdbMovieItemProcessor; | ||
| import com.codeit.team5.mopl.content.batch.processor.TmdbTvSeriesItemProcessor; | ||
| import com.codeit.team5.mopl.content.batch.reader.SportsDbDayItemReader; | ||
| import com.codeit.team5.mopl.content.batch.reader.SportsDbEventItemReader; | ||
| import com.codeit.team5.mopl.content.batch.reader.TmdbMovieItemReader; | ||
| import com.codeit.team5.mopl.content.batch.reader.TmdbTvSeriesItemReader; | ||
| import com.codeit.team5.mopl.content.batch.retry.LoggingSkipListener; | ||
| import com.codeit.team5.mopl.content.batch.retry.SelectiveRetryPolicy; | ||
| import com.codeit.team5.mopl.content.batch.retry.SelectiveSkipPolicy; | ||
| import com.codeit.team5.mopl.content.batch.writer.ContentItemWriter; | ||
| import com.codeit.team5.mopl.content.client.sportsdb.SportsDbApiClient; | ||
| import com.codeit.team5.mopl.content.client.tmdb.TmdbApiClient; | ||
| import com.codeit.team5.mopl.content.dto.external.sportsdb.SportsDbEventDto; | ||
| import com.codeit.team5.mopl.content.dto.external.tmdb.TmdbMovieDto; | ||
| import com.codeit.team5.mopl.content.dto.external.tmdb.TmdbTvDto; | ||
| import com.codeit.team5.mopl.content.repository.ContentRepository; | ||
| import com.codeit.team5.mopl.content.repository.ContentStatsRepository; | ||
| import com.codeit.team5.mopl.tag.repository.TagRepository; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.batch.core.Job; | ||
| import org.springframework.batch.core.Step; | ||
| import org.springframework.batch.core.configuration.annotation.StepScope; | ||
| import org.springframework.batch.core.job.builder.JobBuilder; | ||
| import org.springframework.batch.core.launch.JobLauncher; | ||
| import org.springframework.batch.core.launch.support.TaskExecutorJobLauncher; | ||
| import org.springframework.batch.core.repository.JobRepository; | ||
| import org.springframework.batch.core.step.builder.StepBuilder; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.retry.backoff.FixedBackOffPolicy; | ||
| import org.springframework.retry.support.RetryTemplate; | ||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||
| import org.springframework.transaction.PlatformTransactionManager; | ||
|
|
||
| @Configuration | ||
| @RequiredArgsConstructor | ||
| public class BatchConfig { | ||
|
|
||
| private static final int CHUNK_SIZE = 100; | ||
| private static final int RETRY_LIMIT = 3; | ||
| private static final int TMDB_SKIP_LIMIT = 10; | ||
| private static final int SPORTS_DB_SKIP_LIMIT = 3; | ||
| private static final int EXTERNAL_FETCH_MAX_ATTEMPTS = 2; | ||
| private static final long EXTERNAL_FETCH_BACKOFF_MS = 200; | ||
|
|
||
| private final JobRepository jobRepository; | ||
| private final PlatformTransactionManager transactionManager; | ||
| private final TmdbApiClient tmdbApiClient; | ||
| private final SportsDbApiClient sportsDbApiClient; | ||
| private final ContentRepository contentRepository; | ||
| private final ContentStatsRepository contentStatsRepository; | ||
| private final BinaryContentRepository binaryContentRepository; | ||
| private final TagRepository tagRepository; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| // ── 비동기 JobLauncher ─────────────────────────────── | ||
|
|
||
| @Bean | ||
| public JobLauncher asyncJobLauncher(JobRepository jobRepository, | ||
| ThreadPoolTaskExecutor batchJobTaskExecutor) throws Exception { | ||
| TaskExecutorJobLauncher launcher = new TaskExecutorJobLauncher(); | ||
| launcher.setJobRepository(jobRepository); | ||
| launcher.setTaskExecutor(batchJobTaskExecutor); | ||
| launcher.afterPropertiesSet(); | ||
| return launcher; | ||
|
plzslp marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // ── TMDB 영화 Job ────────────────────────────────────────────── | ||
|
|
||
| @Bean | ||
| public Job tmdbMovieJob() { | ||
| return new JobBuilder("tmdbMovieJob", jobRepository) | ||
| .start(tmdbMovieStep()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public Step tmdbMovieStep() { | ||
| return new StepBuilder("tmdbMovieStep", jobRepository) | ||
| .<TmdbMovieDto, ContentWithMetaData>chunk(CHUNK_SIZE, transactionManager) | ||
| .reader(tmdbMovieItemReader()) | ||
| .processor(tmdbMovieItemProcessor()) | ||
| .writer(contentItemWriter()) | ||
| .faultTolerant() | ||
| .retryPolicy(new SelectiveRetryPolicy(RETRY_LIMIT)) | ||
| .skipPolicy(new SelectiveSkipPolicy(TMDB_SKIP_LIMIT)) | ||
| .listener(new LoggingSkipListener()) | ||
| .build(); | ||
|
plzslp marked this conversation as resolved.
|
||
| } | ||
|
plzslp marked this conversation as resolved.
|
||
|
|
||
| @Bean | ||
| @StepScope | ||
| public TmdbMovieItemReader tmdbMovieItemReader() { | ||
| return new TmdbMovieItemReader(tmdbApiClient, externalFetchRetryTemplate()); | ||
| } | ||
|
|
||
| @Bean | ||
| public TmdbMovieItemProcessor tmdbMovieItemProcessor() { | ||
| return new TmdbMovieItemProcessor(contentRepository, objectMapper); | ||
| } | ||
|
|
||
| // ── TMDB TV 시리즈 Job ───────────────────────────────────────── | ||
|
|
||
| @Bean | ||
| public Job tmdbTvSeriesJob() { | ||
| return new JobBuilder("tmdbTvSeriesJob", jobRepository) | ||
| .start(tmdbTvSeriesStep()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public Step tmdbTvSeriesStep() { | ||
| return new StepBuilder("tmdbTvSeriesStep", jobRepository) | ||
| .<TmdbTvDto, ContentWithMetaData>chunk(CHUNK_SIZE, transactionManager) | ||
| .reader(tmdbTvSeriesItemReader()) | ||
| .processor(tmdbTvSeriesItemProcessor()) | ||
| .writer(contentItemWriter()) | ||
| .faultTolerant() | ||
| .retryPolicy(new SelectiveRetryPolicy(RETRY_LIMIT)) | ||
| .skipPolicy(new SelectiveSkipPolicy(TMDB_SKIP_LIMIT)) | ||
| .listener(new LoggingSkipListener()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| @StepScope | ||
| public TmdbTvSeriesItemReader tmdbTvSeriesItemReader() { | ||
| return new TmdbTvSeriesItemReader(tmdbApiClient, externalFetchRetryTemplate()); | ||
| } | ||
|
|
||
| @Bean | ||
| public TmdbTvSeriesItemProcessor tmdbTvSeriesItemProcessor() { | ||
| return new TmdbTvSeriesItemProcessor(contentRepository, objectMapper); | ||
| } | ||
|
|
||
| // ── SportsDB 리그-시즌 Job ─────────────────────────────────────────────── | ||
|
|
||
| @Bean | ||
| public Job sportsDbEventJob() { | ||
| return new JobBuilder("sportsDbEventJob", jobRepository) | ||
| .start(sportsDbEventStep()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public Step sportsDbEventStep() { | ||
| return new StepBuilder("sportsDbEventStep", jobRepository) | ||
| .<SportsDbEventDto, ContentWithMetaData>chunk(CHUNK_SIZE, transactionManager) | ||
| .reader(sportsDbEventItemReader()) | ||
| .processor(sportsDbEventItemProcessor()) | ||
| .writer(contentItemWriter()) | ||
| .faultTolerant() | ||
| .retryPolicy(new SelectiveRetryPolicy(RETRY_LIMIT)) | ||
| .skipPolicy(new SelectiveSkipPolicy(SPORTS_DB_SKIP_LIMIT)) | ||
| .listener(new LoggingSkipListener()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| @StepScope | ||
| public SportsDbEventItemReader sportsDbEventItemReader() { | ||
| return new SportsDbEventItemReader(sportsDbApiClient, externalFetchRetryTemplate()); | ||
| } | ||
|
|
||
| @Bean | ||
| public SportsDbEventItemProcessor sportsDbEventItemProcessor() { | ||
| return new SportsDbEventItemProcessor(contentRepository, objectMapper); | ||
| } | ||
|
|
||
| // ── SportsDB 일별 Job ────────────────────────────────────────── | ||
|
|
||
| @Bean | ||
| public Job sportsDbDayJob() { | ||
| return new JobBuilder("sportsDbDayJob", jobRepository) | ||
| .start(sportsDbDayStep()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public Step sportsDbDayStep() { | ||
| return new StepBuilder("sportsDbDayStep", jobRepository) | ||
| .<SportsDbEventDto, ContentWithMetaData>chunk(CHUNK_SIZE, transactionManager) | ||
| .reader(sportsDbDayItemReader()) | ||
| .processor(sportsDbEventItemProcessor()) | ||
| .writer(contentItemWriter()) | ||
| .faultTolerant() | ||
| .retryPolicy(new SelectiveRetryPolicy(RETRY_LIMIT)) | ||
| .skipPolicy(new SelectiveSkipPolicy(SPORTS_DB_SKIP_LIMIT)) | ||
| .listener(new LoggingSkipListener()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| @StepScope | ||
| public SportsDbDayItemReader sportsDbDayItemReader() { | ||
| return new SportsDbDayItemReader(sportsDbApiClient, externalFetchRetryTemplate()); | ||
| } | ||
|
|
||
| // @BeforeStep에서 여러 건을 순회 조회하는 Reader(TMDB 영화/TV, SportsDB)가 공용으로 쓰는 | ||
| // 재시도 정책 (최대 2회 시도, 200ms 고정 백오프). @BeforeStep은 청크 단위 faultTolerant 재시도의 | ||
| // 보호 범위 밖이라, 한 건의 일시적 오류로 전체 수집이 실패해 이미 모은 데이터까지 유실되는 것을 | ||
| // 막기 위해 재시도해도 성공 가능성이 있는 오류만 재시도한다. | ||
| @Bean | ||
| public RetryTemplate externalFetchRetryTemplate() { | ||
| RetryTemplate retryTemplate = new RetryTemplate(); | ||
| retryTemplate.setRetryPolicy(new SelectiveRetryPolicy(EXTERNAL_FETCH_MAX_ATTEMPTS)); | ||
|
|
||
| FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); | ||
| backOffPolicy.setBackOffPeriod(EXTERNAL_FETCH_BACKOFF_MS); | ||
| retryTemplate.setBackOffPolicy(backOffPolicy); | ||
|
|
||
| return retryTemplate; | ||
| } | ||
|
|
||
| // ── 공통 Writer ──────────────────────────────────────────────── | ||
|
|
||
| @Bean | ||
| public ContentItemWriter contentItemWriter() { | ||
| return new ContentItemWriter(contentRepository, contentStatsRepository, | ||
| binaryContentRepository, tagRepository); | ||
| } | ||
| } | ||
9 changes: 9 additions & 0 deletions
9
src/main/java/com/codeit/team5/mopl/config/ScheduleConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.codeit.team5.mopl.config; | ||
|
|
||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.scheduling.annotation.EnableScheduling; | ||
|
|
||
| @Configuration | ||
| @EnableScheduling | ||
| public class ScheduleConfig { | ||
| } |
11 changes: 11 additions & 0 deletions
11
src/main/java/com/codeit/team5/mopl/content/batch/dto/ContentWithMetaData.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.codeit.team5.mopl.content.batch.dto; | ||
|
|
||
| import com.codeit.team5.mopl.content.entity.Content; | ||
| import java.util.List; | ||
|
|
||
| public record ContentWithMetaData( | ||
| Content content, | ||
| String thumbnailUrl, | ||
| List<String> tagNames | ||
| ) { | ||
| } |
12 changes: 12 additions & 0 deletions
12
src/main/java/com/codeit/team5/mopl/content/batch/exception/BatchJobLaunchException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.codeit.team5.mopl.content.batch.exception; | ||
|
|
||
| import com.codeit.team5.mopl.content.exception.ContentException; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public class BatchJobLaunchException extends ContentException { | ||
|
|
||
| public BatchJobLaunchException(String jobName, Throwable cause) { | ||
| super(HttpStatus.INTERNAL_SERVER_ERROR, "배치 작업(" + jobName + ") 실행에 실패했습니다."); | ||
| initCause(cause); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.