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 를 추가하였으며 해결 방법에 대해 추가로 생각해봐야 할 것 같음.