# 8. 애그리거트 트랜잭션 관리

# 8.1 애그리거트와 트랜잭션

위와 같은 경우는 추가적인 트랜잭션 처리 기법(선점 잠금 : 비관적 락, 비선점 잠금 : 낙관적 락)으로 해결 가능

# 8.2 선점 잠금

  • 비관적 락
  • 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 때까지 다른 스레드가 해당 애그리거트를 수정하지 못하게 막는 방식
  • 잠금 해제시까지 스레드2가 블로킹 됨 (블로킹 : 스레드2가 스레드1이 락을 반환하기 전까지 대기)
  • 보통 DBMS가 제공하는 행단위 잠금 사용
  • ex) for update 쿼리
  • 스프링에서는?
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select e from EventWithLock e where e.id = :id")
Optional<EventWithLock> findByIdWithPessimisticLock(Long id);

@Transactional
public void joinEventPessimistic(Long eventId, Long memberId) {
    EventWithLock event = eventRepository.findByIdWithPessimisticLock(eventId)
            .orElseThrow(() -> new EntityNotFoundException("이벤트를 찾을 수 없습니다."));
    Member member = memberRepository.findById(memberId)
            .orElseThrow(() -> new EntityNotFoundException("회원을 찾을 수 없습니다."));
    event.increaseParticipants();

    EventWithLockParticipant participant = EventWithLockParticipant.builder()
            .event(event)
            .member(member)
            .build();
    participantRepository.save(participant);
}
  • 트랜잭션이 끝나고 커밋되면서 락도 반환됨

# 8.2.1 선점 잠금과 교착 상태

  • 잠금 순서에 따른 교착 상태가 발생하지 않도록 주의해야 함
  • 교착 상태를 방지하지 위해 잠금을 구할 때 최대 대기 시간을 지정해야 함
Map<String, Object> hints = new HashMap();
hints.put("jakarta.persistence.lock.timeout", 1000);
entityManager.find(Member.class, memberId, LockModeType.PESSIMISTIC_WRITE, hints);
  • 위와 같은 방식으로 해결 가능
  • Spring Data JPA 로는
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({
    @QueryHint(name = "jakarta.persistence.lock.timeout", value = "2000")
})
@Query("select m from Product p where p.id = :id")
Optional<Product> findProductForUpdate(@Param("id") ProductId productId);

# 8.3 비선점 잠금

  • 이런 경우에는 비관적 락으로 해결 불가능

  • 비선점 잠금(낙관적 락)으로 해결 가능

  • 애그리거트에 버전으로 사용할 숫자 타입 프로퍼티 추가

UPDATE aggtable SET version = version + 1, colx= ?, coly = ?
WHERE aggid = ? and version = 현재버전
  • 위 쿼리와 같이 애그리거트 수정할 때마다 버전으로 사용한 프로퍼티 값이 1씩 증가
  • 처음 조회한 버전과 다를 시(5!=6) 예외 발생
@Entity
public class Stock {

    @Id
    private Long id;

    @Version
    private Long version;

    ...
  • @Version 어노테이션 사용
  • 위 사진과 같이 버전값을 같이 보내주어 데이터의 변경 여부 확인(조회와 수정 트랜잭션이 분리되어 있기 때문에)
@Transactional
public void startShipping(StartShippingRequest req) {
    Order order = orderRepository.findById(new OrderNo(req.getOrderNumber()));
    checkOrder(order);
    if (!order.matchVersion(req.getVersion)) {
        throw new VersionConflictException();
    }
    order.startShipping();
}

# 8.3.1 강제 버전 증가

  • 애그리거트의 루트가 아닌 다른 엔티티가 수정되었을 때 버전이 증가되어야 하지만 실제로는 증가 안됨
@Repository
public class JpaOrderRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public Order findByIdOptimisticLockMode(OrderNo id) {
        return entityManager.find(Order.class, id, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
    }
}
  • 위와 같은 방식으로 트랜잭션 종료 시점에 강제 버전 증가 가능

# 8.4 오프라인 선점 잠금

  • 여러 트랜잭션에 걸쳐 동시 변경을 막음
  • 잠금 유효방식을 가져야 함. 그래야 무한 잠금이 방지됨
  • ex) 수정 폼에서 1분단위로 잠금 유효시간 연장 요청 보내기
  • 직접 manual 하게 구현

# 8.4.2 DB를 이용한 LockManager 구현

...

→ 근데 이렇게 할바엔 더 빨리 조회가능하고 TTL을 사용할 수 있는 레디스 기반 네임드 락 사용하는게 나을듯

  • 지금은 RDB 기반으로 조회, 삽입, 수정 쿼리를 직접 넣고 있고, TTL이 없기 때문에 expiration date를 직접 지정해서 관리하고 있는데, 레디스 기반 네임드락 사용 시 인메모리 기반 db기 때문에 그리고 key-value 기반 해시 구조로 더 빨리 조회 가능, 그리고 TTL로 expiration date 간단하게 관리가능.

# 레디스는 싱글 스레드 기반인데 더 빠른 이유?

  1. 인메모리 데이터 구조로 모든 연산이 렘에서 일어나서 디스크 io 대기 시간이 없음
  2. 싱글 스레드 기반이기 때문에 락 경합, 컨텍스트 스위칭 오버헤드가 없음
  3. io 멀티플렉싱 : 파이프라이닝 그리고 lua 스크립트 등의 방식으로 rtt 오버헤드가 감소