Spring Batch 이용한 API 호출횟수 집계 - 1. 구상

Kim Dong Kyun·2023년 6월 12일
2

Spring Batch

목록 보기
1/6
post-custom-banner

개요

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"
        }
    ]
}

작업 프로세스 정리(pseudo)

1. JOB

  1. JOB 을 구성 할 때, JobParameterBuilder로 new Date() 이용해서 해당 날짜의 패러미터를 구성한다. ( 새벽 4시마다 반복 해 줄 생각 )
  2. JOB이 가지는 스텝은 두 단계일 것이다.
  • 집계한다
  • 집계를 마쳤으면 수치를 초기화한다 ( 내일까지의 통계를 위해 )
  1. JobParameter 는 날짜별로 집계하고, @Scheduled 어노테이션을 사용해서 매일 콜되도록 한다
@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);
    }
}

위는 실제 코드

  • jobLauncher.run("실행할 잡"(여기서는 영화를 아이디마다 카운트하는 잡), 우리가 설정한 잡패러미터)

2. Step

  1. 집계하는 부분
  • Map 객체를 사용해서 호출된 API 마다 계수를 집계하면 좋을 것 같다.
  • AOP를 사용해서 카운트 할 수 있을 것 같다. 컨트롤러 자체에서 관심사를 분리 하려고 함
@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);
        // 콜 될때마다 해시맵에다가 호출 횟수를 관리하는거지
    }
}
  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 이상 버전에서 어떻게 작동하는질 모른다


내일 해야 할 일

  1. Job 실행시켜보기 ( 일단 패러미터는 테스트용으로 해서 실행시키기 )
  2. Movie 프로젝트는 H2 써서 스키마 설정은 필요 없는 상태
  3. 실제로 찍어보고, 도커로 돌리면서 확인해보자 (패러미터들)
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 6월 12일

정리가 깔끔하네요! 설명이 잘 되어있어서 알아보기도 좋아요 잘 보고 갑니다 ~.~

1개의 답글