Home 동시성 문제
Post
Cancel

동시성 문제

E-커머스 에서 실무하면서 경험했던 동시성 문제에 대해 기록을 해보겠습니다.
가장 중요하고 대표적인 동시성이 적용되는 도메인중 하나가 재고입니다.
그전에 동시성 문제란 뭔지 알고 넘어가겠습니다.

동시성 문제

동시성 문제란 두 개 이상의 세션(트랜젝션)이 공통된 자원에 대해 모두 읽고 쓰는 작업(Read→Write) 을 하려고 하는 경우 발생할 수 있는 문제를 말합니다. 이때 발생하는 두가지 현상은 “일관성 없는 읽기” 와 “손실되는 업데이트” 입니다.

일관성 없는 읽기

http://jaynewho.com/asset/media/image/post/42/4.png

세션 2는 동시에 실행되고 있는 세션 1 때문에, 데이터에 접근하는 시점마다 다른 값을 읽게 됩니다. 이를 “일관성 없는 읽기” 라고 합니다.

일관성 없는 읽기의 해결 방법은 “불변성”, 복사본을 이용하면 됩니다. 세션 2가 최초로 데이터를 조회할 때 해당 데이터를 복사한 후, 이후에도 계속 사용하는 것입니다. 비록 세션 1이 변경한 데이터의 원장은 감지하지 못하더라도, 세션 2 내에서는 계속 동일한 복사본을 바라볼 수 있습니다.

손실되는 업데이트

http://jaynewho.com/asset/media/image/post/42/1.png

더 늦게 시작한 세션 2에 의해 세션 1의 변경사항이 무시되는 현상을 “손실되는 업데이트” 라고 합니다.

“손실되는 업데이트” 를 방지하기 위한 방법으로는 크게 2가지가 있습니다. 바로 “낙관적 잠금” 과 “비관적 잠금” 입니다.

트랜젝션 격리수준

위에서 언급한 session 은 데이터베이스에서 트랜젝션입니다.
동시성 제어에서는 트렌잭션 ACID 에서 Isolation 가 중점적 입니다.
간단하게 ACID 는 다음과 같습니다,

트랜젝션 ACID

  • Atomicity(원자성)
    • 모든 작업이 반영되거나 모두 롤백되는 특성입니다
  • Consistency(일관성)
    • 데이터는 미리 정의된 규칙에서만 수정이 가능한 특성을 의미합니다. 숫자컬럼에 문자열값을 저장이 안되도록 보장해줍니다.
  • Isolation(고립성)
    • A와 B 두개의 트랜젝션이 실행되고 있을 때, A의 작업들이 B에게 보여지는 정도를 의미합니다.
  • Durability(영구성)
    • 한번 반영(커밋)된 트랜젝션의 내용은 영원히 적용되는 특성을 의미합니다.

Isolation 은 4가지 level 이 있습니다.

Isolation level

  • Read Uncommitted Isolation Level

    • SELECT 문장을 수행하는 경우 해당 데이터에 Shared Lock이 걸리지 않는 Level입니다. 따라서, 어떤 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 B라는 아직 완료되지 않은(Uncommitted 혹은 Dirty) 데이터 B를 읽을 수 있습니다.
  • Read Committed Isolation Level
    • SQL Server가 Default로 사용하는 Isolation Level입니다. 이 Level에선 SELECT 문장이 수행되는 동안 해당 데이터에 Shared Lock이 걸립니다. 그러므로, 어떠한 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 해당 데이터(B)에 접근할 수 없습니다.
  • Repeatable Read Isolation Level
    • 트랜잭션이 완료될 때까지 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정이 불가능합니다. 가령, Select col1 from A where 을 수행하였고 이 조건에 해당하는 데이터가 2건이 있는 경우(col1=1과 5) 다른 사용자가 col1이 1이나 5인 Row에 대한 UPDATE이 불가능합니다. 하지만, 이 조건에 해당하는 Row를 INSERT하는 것은 가능합니다. 따라서 동일 트랜잭션에서 Read되는 데이터 수가 달라질 수 있습니다.
  • Serializable Isolation Level
    • 트랜잭션이 완료될 때까지 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정 및 입력이 불가능합니다. 예를 들어, Repeatable Read의 경우 조건을 만족하는 데이터가 INSERT는 가능하였습니다. 하지만 이 Level에서는 INSERT 작업도 허용하지 않습니다.

전 Mysql 을 썼으며 InnoDB 스토리지 엔진의 기본 Isolation Level이 REPEATABLE-READ입니다.

낙관적, 비관적 잠금

낙관적 잠금(Optimistic Lock)

http://jaynewho.com/asset/media/image/post/43/2.png

낙관적 잠금 은 저장 시 체크한다 입니다. 세션 1이 데이터 A 를 읽어왔더라도 세션 2는 자유롭게 데이터 A 를 읽어올 수 있습니다. 다만 저장하려고 할 때, version 을 검사하게 되는데 A 세션이 끝나면서 Version + 1 을 시킵니다. 그러면 현재 레코드의 version 은 2 인데 B 는 1인 상태로 비교를 하게되어 Optimistic Lock Exception 이 발생합니다.

낙관적 잠금을 구현하기 위해서는 공통된 리소스 A 에 대한 Versioning 이 되어야합니다. 세션 2가 들고 있는 A 의 Version 이, 저장하려는 대상 A 의 Version 과 같은지 체크해야하기 때문입니다.

활동성은 높일 수 있지만, 잘 진행되고 있던 프로세스가 변경 사항을 저장하려고 할 때 까지 프로세스의 성패를 예측할 수 없다는 것이 단점입니다.

비관적 잠금(Pessimistic Lock)

http://jaynewho.com/asset/media/image/post/43/1.png

비관적 잠금 은 이미 읽고 있는 사람이 있다면, 나는 못읽어 입니다. 보수적인 잠금이라고 할 수 있겠죠. 낙관적 잠금에 비관적 잠금은 세션이 실패할 확률은 줄여주지만, 여러 세션의 “활동성” 은 높여주지 못합니다.

데이터베이스가 제공하는 lock 기능을 이용해 엔티티를 영속 상태로 올릴 때부터 다른 세션에서 조회하지 못하도록 잠금을 걸어둡니다. ( select for update 구문이라고 생각할 수 있습니다. )

역시 비관적 잠금인만큼, 활동성은 매우 저하되지만 정확성과 세션의 성공은 보장됩니다.

어떻게 처리 하였는가

JPA 의 동시성 제어

일관성 없는 읽기 는 JPA 에서 자동으로 해결됩니다.

Entity 객체를 한번 조회하면 JPA 의 영속성 컨텍스트 (Persistance Context) 에 캐시되기 때문입니다.

손실되는 업데이트 는 낙관적 락을 적용하여 처리했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
//예시 코드 입니다.
public class Stocks extends BaseTimeEntity {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column
	 private Long id;
	 
	@Version
	private Integer version;
	
	@Column
	private Integer quantity;
}

현재 한 레코드에 여러 트랜잭션이 접근 했다고 가정하겠습니다.

  1. 트랜잭션이 끝날때 @Version 컬럼을 검사하여 값이 다를때 Optimistic Lock Exception 이 발생.
  2. 1번 try-catch 문에서 Exception 발생시 AWS 의 SNS 로 메세징
  3. SNS 에서 구독을 통해 엔드포인트인 SQS 로 전송
  4. SpringBatch 로 구현된 리스너로 해당 SQS 큐에 쌓인 메세지를 긁어옴
  5. 위와 같은 과정을 SQS 기본 재시도 횟수인 10회 동안 재시도후 실패시 데드큐로 빠지도록 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//리스너 예시 코드 입니다.
@Trace(dispatcher = true)
@SqsListener(value = "${application.sqs.update-stock-quantity}", deletionPolicy = ON_SUCCESS)
public void handle(UpdateStockQuantityMessage message) {
    log.debug("UPDATE-STOCK-QUANTITY-HANDLER message: {}", message);
    
    try {
        behaviorLogger.logging("재고 수량변경", logMsg(message));
    } catch (Exception exception) {
        log.error("재고 수량변경 behavior 로그 에러", exception.getMessage());
    }
    
    try {
        stockService.updateStockQuantity(message);
    } catch (DataIntegrityViolationException | OptimisticLockingFailureException | OptimisticLockException e) {
		try {
	    	behaviorLogger.logging("update-stock-quantity 메세지 처리중 optimistic lock exception 에러", logMsg(message));
		} catch (Exception exception) {
	        log.error("update-stock-quantity optimistic lock exception처리 behavior 로그 에러", exception.getMessage());
		}    
		throw OnulIllegalArgumentException.message("재고 수량변경 optimistic lock exception");
    }
}

정리

  • JPA 를 사용하여 낙관적 잠금을 적용시켰다.
  • 예외발생시 AWS SNS, SQS 큐잉 메시지로 후 처리
  • SpringBatch 리스너 로 재시도 처리

포스팅 해야할것

  • 꿀조합 이슈 ( 한트랙젝션에서 비즈니스, 이벤트 두군데에서 같은 레코드 update 시 발생한 이슈)
  • SNS, SQS 란
  • SpringBatch
  • 리스너
  • 분산락
  • ES
  • DDD 에서 이벤트 처리란?
  • Comparator.nullsLast
This post is licensed under CC BY 4.0 by the author.