1. 낙관적 잠금(Optimisstic Lock)이란?
💡 낙관적 잠금은 데이터의 일관성을 유지하면서 다중 클라이언트 환경에서 동시에 데이터를 업데이트하고 처리할 수 있는 방법입니다
이 방법은 성능 면에서 우수하며, 동시성 제어를 위해 사용되는 비관적 잠금에 비해 더 유연한 처리 방식을 제공합니다.
2. 낙관적 잠금의 이점
- 성능 향상
낙관적 잠금은 데이터 업데이트 시 락을 거는 대신, 업데이트를 시도할 때 충돌이 발생하면 예외를 발생시키고, 해당 예외를 처리하는 방식을 사용합니다. 이러한 방식은 업데이트 시 락을 걸어두는 비관적 잠금보다 더 높은 성능을 보장합니다.
- 유연한 처리 방식
낙관적 잠금은 동시에 업데이트를 시도하는 경우 충돌이 발생하면 예외를 발생시키고, 이 예외를 처리하는 방식으로 데이터 일관성을 유지합니다. 이 방식은 업데이트 대상 데이터가 다른 클라이언트에 의해 변경되었는지 여부를 확인하는 방법으로, 더욱 유연한 처리 방식을 제공합니다.
- 병행성 제어가 필요하지 않음
낙관적 잠금은 업데이트 시 락을 걸지 않으므로, 병행성 제어가 필요하지 않습니다. 이는 데이터베이스 성능을 향상시키며, 더 많은 클라이언트에서 동시에 데이터를 처리할 수 있습니다.
📌 따라서, 낙관적 잠금은 다중 클라이언트 환경에서 동시에 데이터를 업데이트하고 처리하는데 유용하며, 데이터 일관성을 보장하면서 성능을 향상시킬 수 있습니다.
3. 낙관적 잠금이 유용한 경우
스프링에서 낙관적 잠금은 보통 다음과 같은 상황에서 사용됩니다.
- 데이터 업데이트가 자주 발생하지 않는 경우
낙관적 잠금은 데이터 업데이트 시 충돌이 발생할 가능성이 높기 때문에, 업데이트가 자주 발생하지 않는 경우에 사용됩니다.
- 충돌이 발생할 가능성이 낮은 경우
낙관적 잠금은 충돌이 발생할 가능성이 높은 경우보다는 충돌이 발생할 가능성이 낮은 경우에 사용됩니다. 예를 들어, 데이터베이스에 대한 업데이트 요청이 매우 드문 경우에는 낙관적 잠금이 유용합니다.
- 데이터 일관성을 보장하는데 충분한 방법이 될 수 있는 경우
낙관적 잠금은 업데이트 대상 데이터가 다른 클라이언트에 의해 변경되었는지 여부를 확인하는 방법으로 데이터 일관성을 보장합니다. 따라서, 데이터 일관성을 보장하는데 충분한 방법이 될 수 있는 경우에 낙관적 잠금이 사용됩니다.
- 락 경합이 적은 경우
낙관적 잠금은 락 경합이 적은 경우에 사용됩니다. 락 경합이 많은 경우 비관적 잠금을 사용하는 것이 더 적합합니다.
❓ 왜, 낙관적 잠금은 데이터 업데이트가 자주 발생하지 않는 경우에 유용할까요?
- 락 경합이 적음
낙관적 잠금은 데이터를 업데이트하기 전에 락을 걸지 않고, 업데이트 시 충돌이 발생하는지 여부를 확인합니다. 따라서, 락 경합이 적은 경우에는 낙관적 잠금이 더 적합합니다.
- 성능 개선
락 경합이 적을 경우, 락을 걸지 않고 데이터를 업데이트할 수 있기 때문에, 낙관적 잠금은 성능 개선에 도움이 됩니다. 비관적 잠금은 업데이트 대상 데이터에 락을 걸고, 다른 클라이언트가 해당 데이터를 업데이트할 때까지 대기해야 하지만, 낙관적 잠금은 락을 걸지 않기 때문에 이러한 대기 시간을 줄일 수 있습니다.
- 일관성 보장
낙관적 잠금은 업데이트 대상 데이터를 조회하고, 해당 데이터가 다른 클라이언트에 의해 변경되지 않았는지 확인합니다. 따라서, 데이터 일관성을 보장할 수 있습니다. 또한, 데이터베이스에 대한 업데이트 요청이 많이 발생하지 않는 경우에는 충돌이 발생할 가능성이 적기 때문에, 낙관적 잠금으로 충분히 데이터 일관성을 보장할 수 있습니다.
📌따라서, 스프링에서 낙관적 잠금은 데이터 업데이트가 자주 발생하지 않는 경우, 충돌이 발생할 가능성이 낮은 경우, 데이터 일관성을 보장하는데 충분한 방법이 될 수 있는 경우, 락 경합이 적은 경우에 사용됩니다.
4. 스프링 데이터(Spring Data)로 낙관적 잠금을 구현하기 위한 필수 요소는?
- @Version 애노테이션
낙관적 잠금은 버전 정보를 사용하여 처리합니다. 따라서, 엔티티 클래스에서 버전 정보를 나타내는 필드에 @Version 애노테이션을 붙여야 합니다.
- 데이터 저장소의 지원
스프링 데이터는 JPA, MongoDB, Redis, Cassandra 등 다양한 데이터 저장소를 지원합니다. 따라서, 낙관적 잠금을 사용하기 위해서는 데이터 저장소가 이를 지원해야 합니다.
- 동시성 제어
낙관적 잠금은 동시성 제어를 통해 동작합니다. 따라서, 여러 사용자가 동시에 동일한 엔티티를 변경하는 경우, 충돌을 방지하기 위해 적절한 동시성 제어 방식을 사용해야 합니다. 스프링 데이터는 이를 위해 @Lock 애노테이션을 지원합니다.
- 예외 처리
낙관적 잠금은 데이터 충돌이 발생할 경우, 예외를 던지는 방식으로 동작합니다. 따라서, 이에 대한 적절한 예외 처리를 수행해야 합니다. 스프링 데이터는 이를 위해 OptimisticLockingFailureException 예외를 제공합니다.
- 트랜잭션 처리
낙관적 잠금은 트랜잭션 내에서 수행되어야 합니다. 따라서, 스프링 데이터를 사용할 때는 트랜잭션 처리를 명시적으로 수행해야 합니다. 스프링 데이터는 이를 위해 @Transactional 애노테이션을 지원합니다.
5. 코드로 보는 낙관적 잠금 구현
- Entity
@Document(collection="products")
public class PublicEntity {
private String id;
@Version
private Integer version;
@Indexed(unique = true)
private int productId;
private String name;
private int weight;
}
- Document(collection="products") 어노테이션은 이 클래스가 MongoDB엔티티 클래스이며, products라는 이름의 MongoDB 컬렉션에 매핑된다는 것을 표시한다.
- @Id, @Version은 스프링 데이터의 id 및 version 필드 라는 것을 표시한다.
- @Indexed(uniqu=true) 어노테이션은 비즈니스 키, productId에 생성된 고유 색인을 가져온다.
💡 version 필드는 낙관적 잠금을 구현하고자 사용한다. 즉, 스프링 데이터가 동시 업데이트에 의한 겹쳐 쓰기를 확인하고 엔티티를 업데이트하고자 사용한다.
데이터베이스에 저장된 version 필드 값이 업데이트 요청에 있는 version 필드 값보다 높다면 업데이트 하려는 데이터가 이미 다른 사용자에 의해 업데이트 된 스테일 데이터(stale data)에 대한 업데이트라는 것을 알 수 있다. 스프링 데이터는 스테일 데이터의 업데이트를 허용하지 않는다.
- Test
@Test
public void optimisticLockError() {
ProductEntity entity1 = repository.findById(savedEntity.getId()).get();
ProductEntity entity2 = repository.findById(savedEntity.getId()).get();
entity1.setName("n1");
repository.save(entity1);
try {
entity2.setName("n2");
repository.save(entity2);
fail("Expected an OptimisticLockingFailureException");
} catch (OptimisticLockingFailureException e) {}
ProductEntity updatedEntity = repository.findById(savedEntity.getId()).get();
assertEquals(1, (int)updatedEntity.getVersion());
assertEquals("n1", updatedEntity.getName());
}
- entity1 변수의 엔티티를 업데이트 할때 스프링 데이터는 엔티티의 버전 필드를 자동으로 증가시킨다. 다른 변수인 entity2의 버전 필드에 저장된 값은 데이터베이스에 저장된 값보다 낮은 값이 저장된 상태이다.
- entity2를 업데이트할 때 낙관적 잠금 오류가 발생한다.