# 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 간단하게 관리가능.
# 레디스는 싱글 스레드 기반인데 더 빠른 이유?
- 인메모리 데이터 구조로 모든 연산이 렘에서 일어나서 디스크 io 대기 시간이 없음
- 싱글 스레드 기반이기 때문에 락 경합, 컨텍스트 스위칭 오버헤드가 없음
- io 멀티플렉싱 : 파이프라이닝 그리고 lua 스크립트 등의 방식으로 rtt 오버헤드가 감소