Movie 프로젝트에는
GET /api/v1/movies/{movie_id}
다음과 같은 API가 존재한다. "특정" 영화를 조회하기 위한 API인데, movie_id 별로 얼마나 조회 되었는지 집계하면 어떤 영화가 제일 핫한지(?) 통계가 대충 나올 듯 하다. 매일 얼마나 조회되는지 보면 더 좋을듯?
API 스펙 예시는 다음과 같다
Q2. GET /api/v1/movies/{movie_id}
● 성공적인 경우, 어떤 https status code와 결과를 되돌려 줘야 할까요? : 200 code 와 id 에 해당하는 Movie 객체의 정보를 Json 형식으로 돌려줘야 합니다.
Json 데이터 예시
{
"id": 1,
"releaseDate": 1,
"movieName": "Movie 1",
"genre": "Genre 1",
"director": "Director 1",
"postImageUrl": "poster_1",
"movieImages": [
{
"id": 1,
"imageUrl": "image_1_for_movie_1",
"movieId": 1
},
{
"id": 2,
"imageUrl": "image_2_for_movie_1",
"movieId": 1
},
{
"id": 3,
"imageUrl": "image_3_for_movie_1",
"movieId": 1
},
{
"id": 4,
"imageUrl": "image_4_for_movie_1",
"movieId": 1
},
{
"id": 5,
"imageUrl": "image_5_for_movie_1",
"movieId": 1
}
],
"movieVideos": [
{
"id": 1,
"videoUrl": "video_1_for_movie_1",
"movieId": 1
},
{
"id": 2,
"videoUrl": "video_2_for_movie_1",
"movieId": 1
},
{
"id": 3,
"videoUrl": "video_3_for_movie_1",
"movieId": 1
}
],
"castMembers": [
{
"id": 1,
"movieId": 1,
"memberName": "Cast Member 1"
},
{
"id": 2,
"movieId": 1,
"memberName": "Cast Member 2"
},
{
"id": 3,
"movieId": 1,
"memberName": "Cast Member 3"
}
]
}
@Scheduled(cron = "0 0 4 * * *") // 매일 새벽 4시에 실행
public void runApiCallJob() throws JobParametersInvalidException,
JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException {
JobParameters jobParameters = new JobParametersBuilder()
.addDate("date", new Date())
.toJobParameters();
jobLauncher.run(movieCountByIdJob(), jobParameters);
}
위와 같은 형태로 정리되어야 할것이다.
@RequiredArgsConstructor
@Component
public class JobCaller {
private final JobLauncher jobLauncher;
private final Job simpleJob1; // bean으로 등록되어 있기 때문에 의존 주입 받을 수 있다.
@Scheduled(cron = "0 0 4 * * *") // 매일 새벽 4시에 실행
public void runApiCallJob() throws JobParametersInvalidException,
JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException {
JobParameters jobParameters = new JobParametersBuilder()
.addDate("date", new Date())
.toJobParameters();
jobLauncher.run(simpleJob1, jobParameters);
}
}
위는 실제 코드
@Slf4j
@Aspect
@Component
public class ExecutionTimer {
public static HashMap<String, Long> map = new HashMap<>();
// static 사용해서 정적으로 관리
...
@Pointcut("@annotation(com.example.movie.common.aop.CountExeByMovieId)")
private void count(){};
@Before("count()")
public void countMovieIdCall(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
// 무비아이디로 받고 있으므로, 패러미터는 하나이고 따라서 그냥 0번 인덱스 넣으면 될듯?
String movieId = parameterNames[0];
map.put(movieId, map.getOrDefault(movieId, 0L) + 1);
// 콜 될때마다 해시맵에다가 호출 횟수를 관리하는거지
}
}
이런 느낌으로 될듯. 어노테이션 달아주면 됨
@EnableBatchProcessing
@EnableScheduling
@Configuration
@Slf4j
@RequiredArgsConstructor
public class BatchConfig {
private final MovieRepository movieRepository;
@Bean
public Job simpleJob1(JobRepository jobRepository, Step simpleStep1, Step simpleStep2) {
return new JobBuilder("simpleJob", jobRepository)
.start(simpleStep1)
.start(simpleStep2)
.build();
}
@Bean
public Step simpleStep1(JobRepository jobRepository, Tasklet testTasklet, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("simpleStep1", jobRepository)
.tasklet(testTasklet, platformTransactionManager).build();
}
@Bean
public Tasklet testTasklet(){
return ((contribution, chunkContext) -> {
HashMap<String, Long> movieNameMap = new HashMap<>();
for (String s : ExecutionTimer.map.keySet()) {
String movieName = movieRepository.findById(Long.parseLong(s)).orElseThrow().getMovieName();
movieNameMap.put(movieName, ExecutionTimer.map.get(s));
// TODO: 2023/06/12 이곳에 movieNameMap을 파일/디비로 저장하는 로직이 필요하다.
// 현재는 로깅하도록 하자
log.info("영화 이름 :" + movieName + ", 호출 횟수 :" + ExecutionTimer.map.get(s));
}
return RepeatStatus.FINISHED;
});
}
@Bean
public Step simpleStep2(JobRepository jobRepository, Tasklet testTasklet, PlatformTransactionManager platformTransactionManager){
return new StepBuilder("simpleStep2", jobRepository)
.tasklet(testTasklet, platformTransactionManager).build();
}
@Bean
public Tasklet testTasklet2(){
return ((contribution, chunkContext) -> {
ExecutionTimer.map.clear();
// 클리어 해주기
return RepeatStatus.FINISHED;
});
}
}
일단 이런 느낌으로
스텝1은 맵을 돌면서 호출횟수별로 구해서 로깅하고
스텝2는 맵 초기화하는 식
근데 이것도 슈도코드인게...SpringBatch5 이상 버전에서 어떻게 작동하는질 모른다
정리가 깔끔하네요! 설명이 잘 되어있어서 알아보기도 좋아요 잘 보고 갑니다 ~.~