기한이 만료된 스토리를 체크해서 자동 비공개처리를 진행했다. 이를 위해 @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 등에 대해서도 추가적인 공부가 필요할 것 같다.