Blog

[Spring][258] redisson 분산 락 구현 문제

Category
Author
Tags
PinOnMain
1 more property

1. 문제

처음에 bookApplyService.java 에서 redisson으로 분산락을 구현한 코드는 아래와 같다
@Transactional public MessageDto createBookApplyDonationV4(BookApplyDonationRequestDto bookApplyDonationRequestDto) { RLock lock = redissonClient.getLock(String.valueOf(bookApplyDonationRequestDto.getBookId())); try { if (!lock.tryLock(3, 3, TimeUnit.SECONDS)) { log.info("락 획득 실패"); throw new IllegalArgumentException("락 획득 실패"); } log.info("락 획득 성공"); Book book = bookRepository.findById(bookApplyDonationRequestDto.getBookId()) .orElseThrow(() -> new IllegalArgumentException("나눔 신청한 책이 존재하지 않습니다.")); if (book.getBookApplyDonation() != null) { throw new IllegalArgumentException("이미 누군가 먼저 신청했습니다."); } BookDonationEvent bookDonationEvent = bookDonationEventRepository.findFetchJoinById(bookApplyDonationRequestDto.getDonationId()) .orElseThrow(() -> new IllegalArgumentException("해당 이벤트가 존재하지 않습니다.")); if (LocalDateTime.now().isBefore(bookDonationEvent.getCreatedAt()) || LocalDateTime.now().isAfter(bookDonationEvent.getClosedAt())) { throw new IllegalArgumentException("책 나눔 이벤트 기간이 아닙니다."); } User user = userRepository.findFetchJoinById(SecurityUtil.getPrincipal().get().getUserId()).orElseThrow( () -> new IllegalArgumentException("해당 사용자는 도서관 사용자가 아닙니다.") ); BookApplyDonation bookApplyDonation = new BookApplyDonation(bookApplyDonationRequestDto); bookApplyDonationRepository.save(bookApplyDonation); bookApplyDonation.addBook(book); user.getBookApplyDonations().add(bookApplyDonation); bookDonationEvent.getBookApplyDonations().add(bookApplyDonation); book.changeStatus(BookStatusEnum.SOLD_OUT); } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } finally { log.info("finally문 실행"); if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); log.info("언락 실행"); } } return new MessageDto("책 나눔 신청이 완료되었습니다."); }
Plain Text
복사
위와 같이 작성 시 기존 10개정도 발생하는 동시성 문제가 2~3개로 줄어들었으나,
2~3개의 동시성 문제가 발생되는 문제가 있었다.

2. 원인

@Transactional public MessageDto createBookApplyDonationV4(BookApplyDonationRequestDto bookApplyDonationRequestDto) { //생략 log.info("언락 실행"); } } return new MessageDto("책 나눔 신청이 완료되었습니다."); }
Plain Text
복사
위에서 작성한 코드의 일부이다.
@Transactional로 만들어진 트랜잭션이 닫히는 시점은 메소드가 끝나는 시점인데,
락을 해제하는 부분은 메소드가 끝나는 시점 이전이다.
따라서 락을 해제하고 메소드가 끝나는 시점 사이에 트랜잭션이 닫히지 않아서 동시성 이슈가 일부(1~2개) 발생하게 된다.

3. 해결

처음엔 클래스 내에서 추가 메소드를 작성하여 아래와 같이 로직 부분만 @Transactional을 걸어주었으나,
@Transactional 이 붙지 않은 메소드에서 같은 클래스 내 @Transactional 이 붙은 메소드를 실행하면 @Transactional이 적용되지 않는다고 한다.
public MessageDto createBookApplyDonationV4(BookApplyDonationRequestDto bookApplyDonationRequestDto) { RLock lock = redissonClient.getLock(String.valueOf(bookApplyDonationRequestDto.getBookId())); try { if (!lock.tryLock(3, 3, TimeUnit.SECONDS)) { log.info("락 획득 실패"); throw new IllegalArgumentException("락 획득 실패"); } log.info("락 획득 성공"); createBookApply(bookRepository.findById(bookApplyDonationRequestDto.getBookId()), bookApplyDonationRequestDto); } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } finally { log.info("finally문 실행"); if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); log.info("언락 실행"); } } return new MessageDto("책 나눔 신청이 완료되었습니다."); } @Transactional// 이렇게 작성하면 @Transactional 이 반영이 안됨private void createBookApply(Optional<Book> bookRepository, BookApplyDonationRequestDto bookApplyDonationRequestDto) { Book book = bookRepository .orElseThrow(() -> new IllegalArgumentException("나눔 신청한 책이 존재하지 않습니다.")); if (book.getBookApplyDonation() != null) { throw new IllegalArgumentException("이미 누군가 먼저 신청했습니다."); } BookDonationEvent bookDonationEvent = bookDonationEventRepository.findFetchJoinById(bookApplyDonationRequestDto.getDonationId()) .orElseThrow(() -> new IllegalArgumentException("해당 이벤트가 존재하지 않습니다.")); if (LocalDateTime.now().isBefore(bookDonationEvent.getCreatedAt()) || LocalDateTime.now().isAfter(bookDonationEvent.getClosedAt())) { throw new IllegalArgumentException("책 나눔 이벤트 기간이 아닙니다."); } User user = userRepository.findFetchJoinById(SecurityUtil.getPrincipal().get().getUserId()).orElseThrow( () -> new IllegalArgumentException("해당 사용자는 도서관 사용자가 아닙니다.") ); BookApplyDonation bookApplyDonation = new BookApplyDonation(bookApplyDonationRequestDto); bookApplyDonationRepository.save(bookApplyDonation); bookApplyDonation.addBook(book); user.getBookApplyDonations().add(bookApplyDonation); bookDonationEvent.getBookApplyDonations().add(bookApplyDonation); book.changeStatus(BookStatusEnum.SOLD_OUT); }
Plain Text
복사
Controller에서 락을 걸고 Service에는 로직만 남기면 정상 작동하나, Controller단에서 redis 관련 config를 등록하여 try-catch문을 쓰는 것은 객체지향 설계에 어긋난다고 생각되었다.
그냥 별도 클래스를 임시로 추가하여 그곳에 로직을 작성하였다.
public MessageDto createBookApplyDonationV4(BookApplyDonationRequestDto bookApplyDonationRequestDto) { RLock lock = redissonClient.getLock(String.valueOf(bookApplyDonationRequestDto.getBookId())); try { if (!lock.tryLock(3, 3, TimeUnit.SECONDS)) { log.info("락 획득 실패"); throw new IllegalArgumentException("락 획득 실패"); } log.info("락 획득 성공"); bookApplyDonationService2.createBookApplyDonation(bookApplyDonationRequestDto); } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } finally { log.info("finally문 실행"); if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); log.info("언락 실행"); } } return new MessageDto("책 나눔 신청이 완료되었습니다."); } @Service @RequiredArgsConstructor @Transactional(readOnly = true) @Slf4j public class BookApplyDonationService2 { private final BookRepository bookRepository; private final BookDonationEventRepository bookDonationEventRepository; private final BookApplyDonationRepository bookApplyDonationRepository; private final UserRepository userRepository; @Transactional public void createBookApplyDonation(BookApplyDonationRequestDto bookApplyDonationRequestDto) { Book book = bookRepository.findById(bookApplyDonationRequestDto.getBookId()) .orElseThrow(() -> new IllegalArgumentException("나눔 신청한 책이 존재하지 않습니다.")); if (book.getBookApplyDonation() != null) { throw new IllegalArgumentException("이미 누군가 먼저 신청했습니다."); } BookDonationEvent bookDonationEvent = bookDonationEventRepository.findFetchJoinById(bookApplyDonationRequestDto.getDonationId()) .orElseThrow(() -> new IllegalArgumentException("해당 이벤트가 존재하지 않습니다.")); if (LocalDateTime.now().isBefore(bookDonationEvent.getCreatedAt()) || LocalDateTime.now().isAfter(bookDonationEvent.getClosedAt())) { throw new IllegalArgumentException("책 나눔 이벤트 기간이 아닙니다."); } User user = userRepository.findFetchJoinById(SecurityUtil.getPrincipal().get().getUserId()).orElseThrow( () -> new IllegalArgumentException("해당 사용자는 도서관 사용자가 아닙니다.") ); BookApplyDonation bookApplyDonation = new BookApplyDonation(bookApplyDonationRequestDto); bookApplyDonationRepository.save(bookApplyDonation); bookApplyDonation.addBook(book); user.getBookApplyDonations().add(bookApplyDonation); bookDonationEvent.getBookApplyDonations().add(bookApplyDonation); book.changeStatus(BookStatusEnum.SOLD_OUT); } }
Plain Text
복사
불필요하게 BookApplyDonationService2 를 추가하였으며 해결 방법에 대해 추가로 생각해봐야 할 것 같음.