본문 바로가기

Design Patterns

애그리거트 트랜잭션관리

문제상황 - Transaction의 충돌

예시 : 커머스의 주문 시스템에서 아래의 두 요청이 동시에 발생하면 어떤 문제가 발생할까?

  • 운영자가 배송준비중 에서 배송으로 주문상태를 변경하려고 한다.
  • 고객은 배송지 정보를 변경하려고 한다.

위의 예시를 순차적으로 살펴보면 이렇다.

  1. 운영자가 주문 애그리거트를 가져온다.
  2. 고객이 주문 애그리거트를 가져온다.
  3. 운영자가 주문 상태를 변경한다.
  4. 고객이 배송지를 변경한다.

위의 상황에서는 애그리거트의 일관성이 깨지게 된다.

 

이를 해결하려면 두 가지 중 하나를 선택해야한다.

  • 운영자가 배송지 정보를 조회하고 상태를 변경하는 동안 고객이 애그리거트를 수정하지 못하게 막는다. (선점 잠금 Pessimistic Lock)
  • 운영자가 배송지 정보를 조회한 이후에 고객이 정보를 변경하면 운영자가 애그리거트를 다시 조회한 뒤 수정하도록 한다. (비선점 잠금 Optimistic Lock)

 

선점 잠금 Pessimistic Lock

선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하는 것을 막는 방식이다.

 

선점잠금 방식으로 위의 예시를 다시 한 번 생각해보자.

  1. (Admin)운영자가 주문 애그리거트를 가져온다. (선점잠금 시작)
  2. (Customer)고객이 주문 애그리거트를 가져오려고 시도한다. (잠금된 동안 블로킹)
  3. (A)주문 상태를 배송중으로 변경한다.
  4. (A)트랜잭션 커밋 잠금 해제
  5. (C)주문 애그리거트 구함 접근 잠금)
  6. (C)고객이 배송지 변경을 시도하고 실패한다. (배송중 상태)
  7. (C)트랜잭션 실패 잠금 해제

 

JPA EntityManager는 LockModeType을 인자로 받는 find() 메서드를 제공하는데 아래와같이 선점 잠금 방식을 적용할 수 있다.

Order order = entityManager.find(Order.class, orderNo, LockModeType.PESSIMISTIC_WRITE);

 

선점 잠금과 교착 상태 (DeadLock)

선점 잠금 기능을 사용할 때는 잠금 순서에 따른 교착 상태가 발생하지 않도록 주의해야 한다.

아래의 예시와 같은 순서로 두 스레드가 잠금 시도를 한다고 해보자.

  1. 스레드1: A 애그리거트에 대한 선점 잠금 구함
  2. 스레드2: B 애그리거트에 대한 선점 잠금 구함
  3. 스레드1: B 애그리거트에 대한 선점 잠금 시도
  4. 스레드2: A 애그리거트에 대한 선점 잠금 시도

이런 교착 상태에 빠지는 문제가 발생하지 않도록 하려면 잠금을 구할 때 최대 대기 시간을 지정해야 한다. JPA에서 선점 잠금을 시도할 때 최대 대기 시간을 지정하려면 힌트를 사용하면 된다.

Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.lock.timeout", 2000);
Order order = entityManager.find(Order.class, orderNo, LockModeType.PESSIMISTIC_WRITE,hints);

 

선점 잠금이 강력해 보이긴 하지만 선점 잠금으로 모든 트랜잭션 충돌 문제가 해결 되는 것은 아니다.

다음의 예시를 살펴보자.

  1. 운영자는 배송을 위해 주문 정보를 조회한다. 시스템은 정보를 제공한다.
  2. 고객이 배송지 변경을 위해 변경 폼을 요청한다. 시스템은 변경 폼을 제공한다.
  3. 고객이 새로운 배송지를 입력하고 폼을 전송해서 배송지를 변경한다.
  4. 운영자가 1번에서 조회한 주문 정보를 기준으로 배송지를 정하고 배송 상태 변경을 요청한다.

여기서 문제는 운영자가 배송지 정보를 조회하고 배송 상태로 변경하는 사이에 고객이 배송지를 변경한다는 것이다. 즉, 배송 상태 변경 전에 배송지를 한 번 더 확인하지 않으면 운영자는 다른 배송지로 물건을 발송하게 된다.

 

비선점 잠금 (Optimistic Lock)

비선점 잠금 방식은 잠금을 해서 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다.  비선점 잠금을 구현하려면 애그리거트에 버전으로 사용할 숫자 타입의 프로퍼티를 추가해야 한다. 애그리거트를 수정할 때마다 버전으로 사용할 프로퍼티의 값이 1씩 증가한다.

 

JPA는 버전을 이용한 비선점 잠금 기능을 지원한다. 버전으로 사용할 필드에 @Version 애노테이션을 붙이고 매핑되는 테이블에 버전을 저장할 칼럼을 추가하면 된다.

 

오프라인 선점 잠금

아틀라시안의 컨플루언스 위키는 문서를 편집하려고 할 때, 누군가 먼저 편집을 하는 중이면 다른 사용자가 문서를 수정하고 있다는 안내 문구를 보여준다. 충돌 여부를 알려주지만 동시에 수정하는 것을 막지는 않는다. 더 엄격하게 데이터 충돌을 막고 싶다면 누군가 수정 화면을 보고 있을 때 수정 화면 자체를 실행하지 못하도록 해야 한다. 이는 선점이나 비선점 잠금 방식으로 구현할 수 없다. 이때 필요한 것이 오프라인 선점 잠금 방식(Offline Pessimistic Lock)이다.

 

단일 트랜잭션에서 동시 변경을 막는 선점 잠금 방식과 달리 오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다. 오프라인 선점 잠금에서는 잠금을 선점하고 장 시간 자리를 비우거나, 프로그램을 종료한다면 오랜 시간 혹은 영원히 잠금을 구할 수 없는 상황이 발생한다. 따라서 잠금의 유효시간을 가져야 한다.

 

 

'Design Patterns' 카테고리의 다른 글

게이트웨이 패턴, 서비스 스텁 패턴  (0) 2020.10.08
지연로드(Lazy Load) 패턴  (0) 2020.10.04
식별자 맵 패턴  (0) 2020.10.03
작업단위 패턴  (0) 2020.10.03
애그리거트  (0) 2020.09.12