[내일배움캠프 Spring 4기 - 최종 프로젝트] 94일차 TIL : 동시성 제어 - 분산 락 | JMeter

서예진·2024년 4월 4일
0

📍 오늘의 학습 키워드

JMeter
동시성 제어

쿠폰 발급 기능의 문제점

  • 선착순 쿠폰에 있어서 동시에 유저가 접근하는 경우도 당연히 고려해야했다.
  • 또한, 쿠폰 발급에 접근한 선착순대로 쿠폰이 발급되어야 한다.
  • Jmeter를 활용하여 동시에 많은 유저가 접근하는 경우를 테스트 했다.
  • 선착순 쿠폰이 500개의 재고가 있다고 가정할 때, 505명의 유저가 동시에 접근했다고 가정하고 테스트 했다.

시나리오

  • 505명의 유저는 모두 회원가입 된 상황
    1. 로그인: API 호출하여 Access Token 받기
    2. 쿠폰 발행 : 쿠폰 발행 API 호출하면서 1에서 받은 Access Token을 헤더로 전달

예상 결과

  • 500개 수량의 쿠폰에 505명의 유저가 접근했으니 50개의 발행 쿠폰이 만들어지고 쿠폰의 수량은 0이 된다. 500번째 까지는 순차적으로 쿠폰 발행이 성공하고 501-505번째까지는 쿠폰 발행이 실패한다.

📖 JMeter


로그인 API

  • 로그인을 위해 505명의 유저 정보를 텍스트 파일로 준비한다.
  • 테스트를 위해 Test Plan에 User Defined Variables를 추가하고 username, password를 변수로 선언한다.
  • CSV Data Set Config 추가
    • 위에서 준비한 텍스트 파일을 적용한다.
  • HTTP Request Defaults 추가
  • HTTP Header Manager 추가 (토큰이 필요한 경우 추가한다.)
  • Thread Group 추가
    • 505명의 유저를 테스트 하기 때문에 505개의 threads를 입력한다.
  • HTTP Request 추가 (HTTP Request - 로그인)
  • Regular Expression Extractor 추가 (토큰 추출)
    • 로그인할 때 토큰을 추출하기 위해서 추가한다.
  • 결과를 보기 위해 Summary Report, View Results Tree, Graph Results를 추가한다.

쿠폰 발행 API

  • HTTP Request 추가 (HTTP Request - 쿠폰 발행)

테스트 결과

  • 그러나 예상 결과와는 달랐다.
  • Jmeter를 사용해서 500개의 쿠폰에 505명의 유저가 접근했을 때 dedlock이 발생한 것을 알 수있다.
  • 또한, 500개의 쿠폰에 505개의 유저가 접근했으면 0개가 남아야 제대로 작동했다는 것인데 11개가 남은 것을 확인할 수 있다.
  • 한번 더 테스트를 실행했더니 이번에는 24개가 남은 것을 확인할 수 있다.

Deadlock(데드락)

  • Deadlock에 대해서는 우선, 아래만큼 알아보고 다음에 더 자세히 알아보려고 한다.

Deadlock 이란?

  • 다수의 프로세스나 스레드가 서로가 점유한 자원을 대기하면서 진행이 멈춰버리는 상태를 가리킨다.
  • 다시 말해, 서로의 자원을 대기하면서 상호간의 락(lock)을 걸고 있어 더 이상 진행할 수 없게 되는 상황을 말한다.

Deadlock 발생 원인

  • 데드락은 아래 네 가지 조건이 동시에 충족될 때 발생한다.
    • 상호 배제(Mutual Exclusion): 자원이 한 번에 하나의 스레드만 사용할 수 있음을 보장한다.
    • 점유 대기(Hold and Wait): 스레드가 이미 보유한 자원을 가진 채 다른 자원을 기다린다.
    • 비선점(No Preemption): 스레드가 다른 스레드가 보유한 자원을 강제로 해제할 수 없다.
    • 순환 대기(Circular Wait): 두 개 이상의 스레드가 자원을 보유하고, 각 스레드가 다음 스레드가 보유한 자원을 기다린다.

Deadlock 과 동시성 문제

  • Deadlock은 동시성 문제의 한 종류이기 때문에 Deadlock이 발생했으면 동시성 제어가 필요하다.

📖 동시성 제어


동시성 제어를 해야하는 이유

  • Race Condition
    • 두 개 이상의 스레드가 동시에 같은 데이터를 접근하여 값을 변경하고자 할 때, 데이터의 예상치 못한 변경이 발생할 수 있다.
  • Dead Lock
    • 두 개 이상의 스레드가 서로의 작업이 완료될 때까지 기다리면서 결국 아무도 완료되지 않는 문제가 발생할 수 있다.
  • Data Corruption
    • 두 개 이상의 스레드가 동시에 같은 데이터에 접근하여 값을 변경할 때, 예상치 못한 데이터의 변형이 발생할 수 있다.

동시성 제어 - Lock

Lock

  • 한 번에 하나의 스레드만이 자원에 접근할 수 있도록 하기 위해 lock(락)을 건다.
  • 여기서 lock은 상호 배제 메커니즘

Lock을 활용한 동시성 제어 기법

비관적 락 (Pessimistic Lock)

  • 주로 데이터베이스에서 사용된다.
  • 비관적 락은 데이터를 읽고 변경하기 위해 락을 획득하는 방식이다. 즉, 동시에 여러 스레드가 데이터를 변경할 수 없다.
  • 데이터를 읽을 때나 변경할 때 락을 획득하고, 작업이 완료될 때까지 락을 유지한다.
  • 데이터를 읽을 때 다른 스레드가 데이터를 변경할 수 없도록 방지하며, 데이터를 변경할 때 다른 스레드의 접근을 차단한다.
  • 주로 동시에 데이터를 변경할 가능성이 높은 상황에서 사용된다.
  • 시간은 없는데 빠르게 락을 구현하고 싶을 때 사용한다.

낙관적 락 (Optimistic Lock)

  • 낙관적 락은 동시에 여러 스레드가 데이터를 읽고 변경할 수 있도록 허용하며, 변경 시에 충돌을 방지하기 위해 버전 관리를 사용한다.
  • 데이터를 읽을 때 락을 획득하지 않고, 데이터를 변경하기 전에 충돌을 검사합니다.
  • 주로 두 단계로 구성된다.
    1. 데이터를 읽을 때 버전 정보를 가져옵니다.
    2. 데이터를 변경할 때 이전에 읽은 버전과 현재 버전을 비교하여 충돌을 검사합니다.
  • 충돌이 발생하지 않으면 변경된 내용을 적용하고, 충돌이 발생하면 롤백하거나 다시 시도한다.
  • 주로 데이터의 읽기가 많은 상황이나 충돌이 적은 상황에서 사용된다.
  • 트랜잭션을 사용하지 않기 때문에 비관적 락에 비해 성능이 좋으나, 충돌이 날 가능성이 있고 이때, 복구가 힘들다.

분산 락 (Distributed Lock)

  • 분산 시스템에서 여러 노드 간에 공유 자원에 대한 접근을 조정하기 위해 사용되는 동시성 제어 메커니즘이다.
  • 여러 프로세스나 스레드가 동일한 자원에 동시에 접근하는 상황에서 데이터 일관성을 유지하고, 경쟁 조건을 방지하기 위해 사용된다.
  • 분산 락은 네트워크를 통해 통신하며 여러 노드 간에 락을 동기화 한다. 일반적으로 중앙화된 락 관리 서비스를 사용하여 분산 락을 구현한다.
  • 특징:
    • Atomicity(원자성): 분산 락은 락을 획득하거나 해제하는 동작이 원자적으로 수행되어야 한다. 즉, 락을 획득하거나 해제하는 동안 다른 클라이언트가 동시에 락을 획득하거나 해제할 수 없어야 한다.
    • Consistency(일관성): 분산 락은 여러 노드 간에 일관성 있는 락의 상태를 유지해야 한다. 모든 클라이언트는 락을 동일하게 인식하고, 동일한 락에 대한 접근 권한을 가지게 된다.
    • Availability(가용성): 분산 락은 시스템의 가용성을 보장해야 한다. 즉, 락 서비스가 항상 동작 가능해야 하며, 장애가 발생한 경우에도 시스템의 정상적인 동작을 지원해야 한다.
    • Partition Tolerance(분할 내구성): 분산 시스템에서는 네트워크 파티션과 같은 분할 상황이 발생할 수 있다. 분산 락은 이러한 분할 상황에서도 정상적으로 동작하고 데이터 일관성을 유지할 수 있어야 한다.
  • 주로 Redis를 활용하기에 분산 락을 사용하는 것은 Redis를 사용하는 것이라고 생각해도 무방하다.
  • Redis를 사용하여 분산 락이 가능한 이유는 크게 두 가지이다.
    1. Memory DB 이다.
      • 디스크를 사용하는 RDBMS 등의 DB 보다 최소 수만배, 혹은 수십만배 이상 빠르다.
    2. 싱글 스레드이다.
      • 일반적을 DB는 멀티 스레드

동시성 제어 - 분산 락 구현(Redis)

EventServiceImpl.java

private final RedissonClient redissonClient;

	private static final String LOCK_KEY = "couponLock";
	
public void issueCoupon(Long eventId, Long couponId, User user, LocalDateTime now) {
		RLock lock = redissonClient.getFairLock(LOCK_KEY);
		try {
			boolean isLocked = lock.tryLock(10, 60, TimeUnit.SECONDS);
			if(isLocked) {
				try {
					if(issuedCouponRepository.existsByCouponIdAndUserId(couponId, user.getId())) {
						throw new InvalidCouponException(INVALID_COUPON);
					}
					Event event = findEvent(eventId);
					if(now.isBefore(event.getOpenAt())) {
						throw new InvalidCouponException(INVALID_COUPON);
					}

					Coupon coupon = findCoupon(couponId);
					if (coupon.getQuantity() <= 0) {
						throw new InvalidCouponException(INVALID_COUPON);
					}
					coupon.decrease();
                    couponRepository.save(coupon);
					IssuedCoupon issuedCoupon = new IssuedCoupon(user, coupon);
					issuedCouponRepository.save(issuedCoupon);
				} finally {
					lock.unlock();
				}
			}
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
	}
  • 동시성 제어를 구현하려면 @Transactional 을 적용 해제해야한다.
  • @Transactional을 유지한채로 동시성 제어를 해보니 데드락이 여전히 발생했기 때문이다.

결과


  • 순차대로 쿠폰 발행이 이루어진 것을 확인할 수 있고 쿠폰 수량이 0개 남은 것을 확인할 수 있다.
  • 이로써, 동시성 제어가 되어 예상 결과와 똑같은 결과를 얻은 것을 확인할 수 있다.

💡 느낀점

  • 최종 프로젝트에서 분산 락을 이용하여 동시성 제어를 하면서 아래 3가지에 대해서 더 알아보면 좋을 것 같다.
  1. Deadlock
  2. 비관적 락, 낙관적 락을 이용한 동시성 제어
  3. 트랜잭션
profile
안녕하세요

0개의 댓글