기한이 만료된 스토리를 체크해서 자동 비공개
처리를 진행했다. 이를 위해 @scheduled
어노테이션을 이용했다.
하지만 이후 튜터님께 피드백 받을 때 @scheduled
을 이용하는 개발 방식이 싱글스레드에서는 문제가 없지만 이후 멀티 스레드
환경에서는 문제가 발생할 수 있다는 이야기를 해주셨다. 그래서 이에 대해 좀 더 알아보고자 한다.
@Transactional
@Scheduled(cron = "0 * * * * ?") // 분당 한번씩 체크크
public void isVisibilityExpire(){
LocalDateTime now = LocalDateTime.now();
List<Story> expiredStories = storyRepository.findByVisibilityEndBeforeAndVisibilityType(now, 1);
for(Story story : expiredStories){
story.changeVisibilityType(0);
}
storyRepository.saveAll(expiredStories);
}
Spring은 단일스레드 기반의 스케줄러를 이용, 특정 주기에 맞춰 메서드를 실행하는 방식으로 진행
스케줄러 실행 흐름
결론
@Scheduled은 단일 스레드를 기반으로 이용하기 때문에 멀티 스레드 환경에서 실행을 시키게 된다면 각각의 서버에서 별도로 @Scheduled를 실행하기 때문에 동일한 작업이 여러번 실행되는 동시성 문제가 발생할 가능성이 있음!
중복 실행 문제
storyRepository.findByVisibilityEndBeforeAndVisibilityType(now, 1)
조회 후, saveAll()
전에 동일한 작업이 다른 서버에서 실행된다면 동일한 Story 엔티티가 중복 업데이트될 가능성이 존재하여 불필요한 업데이트나 충돌이 발생할 수 있음.경쟁 상태(Race Condition)
Phantom Read (팬텀 리드)
그럼 @schedule 어노테이션을 이용하면서 위와 같은 문제를 해결할 방법은 뭐가 있을까?
1️⃣ 스케줄링 작업을 하나의 서버만 수행하도록 막기
@SchedulerLock
이용하기 @Transactional
@Scheduled(cron = "0 * * * * ?") // 분당 한번씩 체크크
@SchedulerLock(name = "isVisibilityExpire", lockAtLeastFor = "1m", lockAtMostFor = "2m")
public void isVisibilityExpire(){
LocalDateTime now = LocalDateTime.now();
List<Story> expiredStories = storyRepository.findByVisibilityEndBeforeAndVisibilityType(now, 1);
for(Story story : expiredStories){
story.changeVisibilityType(0);
}
storyRepository.saveAll(expiredStories);
}
3️⃣ 쿼리 단에서 바로 업데이트 (JPQL)
@Modifying
@Query("UPDATE Story s
SET s.visibilityType = 0
WHERE s.visibilityEnd < :now AND s.visibilityType = 1")
int updateExpiredStories(@Param("now") LocalDateTime now);
여기서 명시적 업데이트와 쿼리단에서 직접 업데이트를 실행할 때의 차이점이 뭔지 궁금해졌다.
결론적으로 가장 큰 차이는 명시적 업데이트의 경우 list에 존재하는 엔티티 개수별로 업데이트가 진행된다는 점이고, 쿼리단에서 실행할 경우에는 하나의 쿼리로 모든 데이터를 업데이트
할 수 있다는 점이다.
1️⃣ findByVisibilityEndBeforeAndVisibilityType() + saveAll()
1. 동작 방식
visibilityEnd < now AND visibilityType = 1
에 포함되는 모든 데이터를 조회함.visibilityType
을 0
으로 변경하고 saveAll로 업데이트saveAll()
이 실행될 경우 각각의 개별적인 update 쿼리가 실행됨.2. 장점
3. 단점
2️⃣ @Modifying + @Query (JPQL 직접 실행)
💡 따라서 상황에 따라 적절한 방식을 이용해야 한다. 지금 내 코드에서는 조회 후 saveAll이 아닌 바로 update해주는 방식이 더욱 효율적!
위와 같이 코드를 수정한다면 멀티스레드에서 문제를 해결할 수 있을것 같다. 그러나 하나의 서버에서 실행하도록 강제한다는 점에서 성능에 그다지 좋지 않을것 같았다. 그렇다면 @Scheduled
없이 자동 업데이트를 하는 방법에 대해 알아보고자 한다.
update
실행만료될 Story의 정보를 메시지 큐에 넣고 비동기적으로 처리
ex) visibilityEnd와 시간이 가까워지면 queue
에 Story Id 값을 넣고, worker 서비스에서 업데이트
장점
단점
대량의 데이터를 일정 기간마다 처리해야하는 경우 사용
Spring Batch Job을 설정
장점
단점
결론
공부는 하면 할수록 알아가야할 것이 많은 것 같다. 추후에는 Kafka, Redis 등에 대해서도 추가적인 공부가 필요할 것 같다.