내일배움캠프 Spring 61일차(목) TIL - Lock

Skadi·2024년 3월 21일
0

락(Lock)이란?

데이터베이스에서 락은 동시성 제어를 위해 사용됩니다. 동시에 여러 트랜잭션이 하나의 데이터에 접근할 때, 데이터의 일관성과 무결성을 유지하기 위해 도입된 메커니즘입니다. 락을 통해 특정 데이터나 자원에 대한 접근을 제어하고, 동시에 발생할 수 있는 충돌을 방지합니다.

락의 종류

낙관적 락(Optimistic Lock)

  • 데이터를 읽을 때는 충돌이 발생하지 않을 것이라고 "낙관적"으로 가정하고, 실제로 데이터를 변경할 때만 충돌 여부를 검사합니다.
  • 주로 데이터의 버전 번호를 사용하여 구현되며, 데이터를 변경하려는 시점에 버전 번호를 확인하여 충돌을 감지합니다.
  • 충돌이 빈번하지 않은 환경에 적합합니다.
  • 사용 방법(스프링 데이터 JPA 예시)
@Entity
public class EntityWithOptimisticLock {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version
    private int version; // 버전을 통한 낙관적 락 구현

    // 나머지 필드 및 메서드
}

비관적 락(Pessimistic Lock)

  • 데이터를 읽거나 변경하기 전에 미리 락을 획득하여, 다른 트랜잭션이 동시에 해당 데이터에 접근하지 못하도록 "비관적"으로 가정하고 락을 걸어둡니다.
  • 데이터베이스에서 지원하는 락 메커니즘을 사용하여 구현됩니다.
  • 충돌이 자주 발생하는 환경에 적합합니다.
  • 사용 방법(스프링 데이터 JPA 예시)
@Repository
public interface EntityRepository extends JpaRepository<EntityWithPessimisticLock, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT e FROM EntityWithPessimisticLock e WHERE e.id = :id")
    Optional<EntityWithPessimisticLock> findByIdForUpdate(@Param("id") Long id);
}

분산 락(Distributed Lock)

  • 분산 락은 여러 서버나 애플리케이션 인스턴스 간에 자원에 대한 동시 접근을 조율하기 위해 사용됩니다. 이는 분산 시스템에서 일관성을 유지하고, 동시에 자원을 안전하게 사용하기 위한 메커니즘입니다. 분산 락은 중앙 집중식 저장소(예: Redis, ZooKeeper 등)를 사용하여 락 상태를 관리합니다.

Redisson을 사용하여 분산 락을 구현하는 예제 코드입니다.

import org.redisson.api.RedissonClient;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class DistributedLockService {

    @Autowired
    private RedissonClient redissonClient;  // Redisson 클라이언트 자동 주입

    public void executeCriticalTaskWithLock(Long taskId) {
        RLock lock = redissonClient.getLock("taskLock:" + taskId);  // 고유한 락 이름
        try {
            // 10초 내에 락 획득 시도, 최대 60초 동안 락 유지
            boolean isLocked = lock.tryLock(10, 60, TimeUnit.SECONDS);
            if (isLocked) {
                // 여기에서 중요한 작업 실행
                // 예: 데이터베이스 업데이트, 파일 작업 등
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();  // 락 해제
            }
        }
    }
}
  • 이 코드는 Redisson을 사용하여 분산 락을 구현한 예제입니다. RedissonClient를 통해 특정 자원("taskLock:" + taskId)에 대한 락을 얻으려고 시도하며, 락을 성공적으로 획득하면 중요한 작업(예: 데이터베이스 업데이트 등)을 수행한 후에 락을 해제합니다. tryLock 메소드는 주어진 시간 내에 락을 획득하려고 시도하며, 지정된 시간이 초과되면 실패합니다. 이 방식은 여러 인스턴스가 동시에 같은 자원에 접근하려고 할 때 데이터 일관성을 유지하는 데 도움이 됩니다.

  • 분산 시스템을 다룰 때 분산 락은 필수적인 요소 중 하나입니다. Redisson 외에도 ZooKeeper, Etcd 등 다른 도구를 사용한 분산 락 구현도 가능합니다. 선택하는 도구는 애플리케이션의 요구 사항과 기술 스택에 따라 달라질 수 있습니다.

락을 사용한 테스트에서 문제 발생

  1. Redisson을 이용한 동시성제어를 프로젝트에 적용하였다.
  2. 테스트코드에서 DB에 데이터를 저장하는게 싫어 @Transactional을 테스트코드에 적용시켰다.
  3. 락관련 log가 생성되지 않았다.
  4. ExecutorService가 문제였다.
  5. 동시성제어문제를 시현해보기 위해서 멀티쓰레드를 사용한 것이 문제가 되었다.
  • ExecutorService를 사용하여 생성된 쓰레드 또는 쓰레드 풀에서 실행되는 작업은 스프링의 @Transactional 어노테이션에 의한 트랜잭션 관리의 영향을 받지 않는 주된 이유는 스프링의 트랜잭션 관리가 스레드에 바인딩되어 작동하기 때문입니다. 즉, 스프링의 트랜잭션 관리는 호출 스레드 기반으로 동작하며, 별도의 스레드에서는 이러한 관리가 적용되지 않습니다.
  1. 테스트를 위한 별도의 프로필을 생성하는 것으로 기존 DB에 적용되지 않도록 하였다.

0개의 댓글