처음 스프링 배치를 시작할 때 어떻게 내 프로젝트에 적용시켜야 할지 많이 고미을 하였다. 많은 예시를 찾아보다가 당근마켓에서 알람이 하나왔다. "게시물을 올리신지 00일이 지났어요! 알림을 확인해 주세요!"라는 알림 이었다. 이 알림을 읽고 '오래전에 게시물을 올렸지만 팔리지 않은 것에 대해 알림을 주자'라고 생각이 들어 배치를 실행하였다.
먼저 배치에 대해 공부를 하고 접근 하였다. 자세한 내용은 배치 포스트에 작성하겠다.
이 배치에 대해서는 먼저 chunk
방식을 이용하였다. chunk
방식은 해당 배치를 한번에 커밋하는것이 아닌 chunk
단위로 나누어서 트랜잭션을 수행하는 것을 의미한다. 따라서 reader
, processor
, writer
로 구분지어 코드를 작성하였다.
해당 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
가 존재하지만 이 배치에서는 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부분에서는 해당 쿼리로 얻어진 결과 값의 상태 값을 'old'로 바꾸는 역할을 하였다.
@Bean
@StepScope
public ItemProcessor<BoardEntity, BoardEntity> oldBoardProcessor(){
return boardEntity -> {
boardEntity.setStatus(Status.old);
return boardEntity;
};
}
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();
}
}
}
처음에는 아래 사진처럼 내 정보 페이지에 들어가면 사이드바 쪽에 보이도록 설정하였다. 하지만 인턴십 코드 피드백을 받을 때 '만일 데이터가 무수히 많으면 문제가 된다'고 말씀 해주셔서 다른 방식을 찾아보기로 하였다.
다른 방식으로는 상단바에 알림 형태로 표시하는 방안이다. <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();
}
다음 배치는 통계 배치로 포스팅 하겠습니다.