분산락 이란 동시성 문제를 해결하기 위한 방법 중 하나로 같은 자원에 접근할 시 락을 제공하여 접근이 완료되면 락을 해제하여 다음 순번에게 넘어가는 방식이다.
분산락을 사용하는 이유는 분산된 서버 및 분산된 DB환경에서도 동시성 이슈를 해결하기 위함이다.
분산락을 Redis로 구현하는 방법에는 lettuce, redisson 라이브러리가 있는데 lettuce방식은 스핀락(락을획득하지 못할 경우 계속 요청) 방식으로 redis에 부하가 갈 수 있어 redisson을 사용하여 구현해보았다.
RedissonConfig를 작성하여 Bean에 등록하고
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
private static final String REDISSON_HOST_PREFIX = "redis://";
@Bean
public RedissonClient redissonClient() {
RedissonClient redisson = null;
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
redisson = Redisson.create(config);
return redisson;
}
}
Plain Text
복사
BookApplyDonationService에 아래와 같이 구현하였다.
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("책 나눔 신청이 완료되었습니다.");
}
Plain Text
복사
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
복사
잘 구현은 되나 bookApplyDonation2 를 추가로 생성하여 만드는 찝찝함이 있는데 이에 대한 내용은 별도 트러블슈팅 문서에 정리하겠다.