[게시판 프로젝트] 스프링 배치 적용(1)

J_Eddy·2021년 12월 28일
1

📌 Spring Batch - 오래된 게시물

처음 스프링 배치를 시작할 때 어떻게 내 프로젝트에 적용시켜야 할지 많이 고미을 하였다. 많은 예시를 찾아보다가 당근마켓에서 알람이 하나왔다. "게시물을 올리신지 00일이 지났어요! 알림을 확인해 주세요!"라는 알림 이었다. 이 알림을 읽고 '오래전에 게시물을 올렸지만 팔리지 않은 것에 대해 알림을 주자'라고 생각이 들어 배치를 실행하였다.

먼저 배치에 대해 공부를 하고 접근 하였다. 자세한 내용은 배치 포스트에 작성하겠다.

이 배치에 대해서는 먼저 chunk방식을 이용하였다. chunk방식은 해당 배치를 한번에 커밋하는것이 아닌 chunk단위로 나누어서 트랜잭션을 수행하는 것을 의미한다. 따라서 reader, processor, writer로 구분지어 코드를 작성하였다.

JobStep

해당 Job의 reader, processor, writer를 정해두는 장치이다.

 @Bean
    @JobScope
    public Step oldBoardJobStep(){
        return stepBuilderFactory.get("oldBoardJobStep")
                .<BoardEntity, BoardEntity>chunk(chunkSize)
                .reader(oldBoardReader()) //구현해야함 게시물을 올린지 1주일 이상된 보드를 읽어온다
                .processor(oldBoardProcessor()) //구현해야함 판매완료가 아닐경우 ~~~을 수행한다
                .writer(oldBoardWriter()) //구현해야함 처리된 게시물을 DB에 저장한다
                .build();
    }

Reader

다양한 reader가 존재하지만 이 배치에서는 RepositoryItemReader를 사용하였다. 이유는 JPA를 사용하면서 만들어 두었던 Repository를 이용하고 싶어서 이다. repository에 실제 구현할 Repository를, methodName에 구현된 메소드 이름을 적어둔다. 이후 arguments에는 해당인자값을, pageSize에는 페이징할 사이즈를 넣었다.

@Bean
    @StepScope
    public RepositoryItemReader<BoardEntity> oldBoardReader() {
        return new RepositoryItemReaderBuilder<BoardEntity>()
                .repository(boardRepository)
                .methodName("findByUpdatedDateBeforeAndStatusEquals")
                .arguments(LocalDateTime.now().minusWeeks(1), Status.sell)
                .pageSize(chunkSize)
                .sorts(Collections.singletonMap("updatedDate", Sort.Direction.ASC))
                .name("oldBoardReader")
                .build();
    }

Repository

//배치에 사용
    Page<BoardEntity> findByUpdatedDateBeforeAndStatusEquals(LocalDateTime localDateTime, Status status, Pageable pageable);

다음으로는 sorts부분인데 이 부분은 해당 결과값을 정렬하는 부분이다. 여기서 map을 사용한 이유는 해당 메소드가 아래 사진처럼 map형식으로 받고있어서이다.

processor

processor부분에서는 해당 쿼리로 얻어진 결과 값의 상태 값을 'old'로 바꾸는 역할을 하였다.

@Bean
    @StepScope
    public ItemProcessor<BoardEntity, BoardEntity> oldBoardProcessor(){
        return boardEntity -> {
            boardEntity.setStatus(Status.old);
            return boardEntity;
        };
    }

writer

writer를 구현할 때에도 RepositoryItemWriter를 사용하였는데 관련 자료가 많이 없어서 메소드를 뜯어보았다.

  • methodName과 repository를 set 할 수 있다.

  • 아래 두개를 이용해서 doWrite를 진행

  • 만일 methodName이 없으면 saveAll로 진행한다고 적혀있어서 repository만 set시켜 진행하였다
@Bean
    @StepScope
    public RepositoryItemWriter<BoardEntity> oldBoardWriter(){
        return new RepositoryItemWriterBuilder<BoardEntity>()
                .repository(boardRepository)
                .build();
    }

스케쥴러 사용

Scheduler를 사용하여 배치가 자동적으로 동작할 수 있게 구현하였습니다.

@AllArgsConstructor
@Component
public class JobScheduler {

    private final JobLauncher jobLauncher;

    private final OldBoardJobConfiguration oldBoardJobConfiguration;

    @Scheduled(cron = "0 27 * * * *") //초 분 시 일 월 요일
    public void runJob() {

        Map<String, JobParameter> map = new HashMap<>();
        map.put("time", new JobParameter(System.currentTimeMillis()));
        JobParameters jobParameters = new JobParameters(map);

        try {
            jobLauncher.run(oldBoardJobConfiguration.oldBoardJob(), jobParameters);

        } catch (JobExecutionAlreadyRunningException | JobInstanceAlreadyCompleteException
                | JobParametersInvalidException | org.springframework.batch.core.repository.JobRestartException e) {

            log.error(e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}![](https://velog.velcdn.com/images%2Falstn_dev%2Fpost%2F81c9dc01-2c67-4d25-86c4-7b84ba2637a9%2Fimage.png)

📌 배치 결과 뷰에 적용

처음에는 아래 사진처럼 내 정보 페이지에 들어가면 사이드바 쪽에 보이도록 설정하였다. 하지만 인턴십 코드 피드백을 받을 때 '만일 데이터가 무수히 많으면 문제가 된다'고 말씀 해주셔서 다른 방식을 찾아보기로 하였다.

다른 방식으로는 상단바에 알림 형태로 표시하는 방안이다. <nav top>부분에 레이어 팝업으로 div를 생성하고 include하는 방식을 택했다.

nav bar

<!--버튼-->
            <button type="button" class="notification" id="oldBoardButton">
                <span><img src="/images/oldBoardAlert.png" width="35px"></span>
                <span id="oldBoardCnt" class="badge"></span>
            </button>
            <div class="popup_bg"></div>
            <div class="popup">
                <th:block th:include="fragments/oldBoardList"></th:block>
                <a href="#" class="close">[X]</a>
            </div>

JS
해당 배지에 oldBoard의 갯수를 나타낼수 있도록 ajax를 통해 구현하였다. 갯수가 0개가 아니라면 해당 값을 input시키고 0이라면 숨기는걸 구현했다.

$.ajax({
        url : "/api/board/getMyOldBoardCnt",
        method : 'get',
        success : function(success){
            let totalCnt = success.totalCnt;
            if(!totalCnt==0){
                $("#oldBoardCnt").text(totalCnt);
            }else{
                $("#oldBoardCnt").hide();
            }

        },
        error: function (request, status, error) {
            alert("code: " + request.status + "\n" + "error: " + error);
        }
    });

결과

이후 종 버튼을 클릭 시 팝업이 열린다. 이때 사용자의 편의를 위해서 x버튼을 누르거나 배경을 눌렀을 때 자동으로 닫히게 설정하였고, show(300)을 적용하여 부드럽게 열리도록 구현하였다.

 $("#oldBoardButton").on('click',function () {    // [1]
        $(".popup_bg").show();
        $(".popup").show(300);
    });

    $(".popup_bg, .close").on('click',function () {    // [2]
        $(".popup_bg").hide();
        $(".popup").hide(200);
    });

이후 팝업이 열리면 아래와 같이 나온다. 이는 ajax를 이용하여 list형식으로 받아 2개씩 나타나도록 하였다.

아래 처럼 ajax로 구현을 해서 해당 결과값이 0이면 mypage로 이동하기 버튼이 생성되며 문구가 뜨고 만약 결과값이 0이 아니면 해당 게시물로 이동할 수 있는 링크와 함께 더보기 버튼이 제공됩니다. 더보기 버튼은 알림의 숫자가 2개 이상이면 생성이 되고 이후 부터는 myPage로 이동할수 있는 moveOldBoardPage버튼이 보여지게 됩니다.

$.ajax({
            url: "/api/board/getMyOldBoardList",
            method: "get",
            data: {startIndex: index, searchStep: searchStep},
            success: function (success) {
                const oldListCnt = success.totalCnt;
                let NodeList = "";
                let newNode = ""

                if(success.data.length==0){
                  ...
                  
                     newNode += "<h4 class='text-center'>모든 알람을 확인하였습니다!</h4><br>";
                    newNode += "<span class='text-center mb-3'>오래된 게시물을 확인하시려면 아래 버튼을 눌러주세요!</span>";
                  ...
                    $('#moveOldBoardPage').show();
                }
                for(let i = 0; i < success.data.length; i++){
                   ...
                   newNode += "<p class='card-text text-danger'>물건을 올린지 1주일이 지났어요! 물건의 가격을 변경해보세요!</p>";
                    newNode += "</div><button type='button' class='btn btn-primary move'>게시물로 이동</button>";
                  ...
                    NodeList += newNode;
                }
                $(NodeList).appendTo($("#oldList")).slideDown();

                // 더보기 버튼 삭제
                if(startIndex + searchStep > oldListCnt){
                    $('#searchMoreNotify').remove();
                }
                if(startIndex>=2){
                    $('#moveOldBoardPage').show();
                    $('#searchMoreNotify').remove();

                }

            },
            error: function (request, status, error) {
                alert("code: " + request.status + "\n" + "error: " + error);
            }
        });

알림을 읽었는지 여부 설정

만일 해당 알림이 뜨고 해당 게시물로 이동하게되면 Board Table에 있는 setAlertReading 컬림이 1로 설정되어 더이상 알람에 나타나지 않게 됩니다. 해당 메소드는 QueryDsl로 구현하였습니다.

@Transactional
    public void setAlertReading(Long id){
        queryFactory.update(QBoardEntity.boardEntity)
                .set(QBoardEntity.boardEntity.alertRead, 1)
                .where(QBoardEntity.boardEntity.id.eq(id))
                .execute();
    }

다음 배치는 통계 배치로 포스팅 하겠습니다.

profile
논리적으로 사고하고 해결하는 것을 좋아하는 개발자입니다.

0개의 댓글