동시성 제어의 중요성
데이터베이스나 분산 시스템에서는 여러 트랜잭션이 동시에 하나의 데이터에 접근할 때, 데이터의 일관성과 무결성을 보장하기 위해 동시성 제어가 필수적입니다.
올바른 동시성 제어가 이루어지지 않으면 더티 리드(Dirty Read), 레이스 컨디션(Race Condition)과 같은 동시성 관련 문제가 발생할 수 있습니다.
낙관적 락과 비관적 락
낙관적 락과 비관적 락은 트랜잭션 제어를 통한 동시성 제어의 대표적인 락 기법으로, 데이터 무결성과 일관성을 유지하기 위해 사용됩니다.이 글에서는 낙관적과 비관적 락에 대한 정의와 동작원리 그리고 구현 방법에 대해 정리해 보겠습니다.
낙관적 락 (Optimistic Lock)
정의와 동작 원리
낙관적 락은 동시성 제어 방식 중 하나로, 데이터의 충돌 가능성이 낮다고 가정하고 트랜잭션을 처리하는 방법입니다.
트랜잭션이 데이터를 수정하기 전에 다른 트랜잭션이 그 데이터를 수정할 가능성이 적다는 전제로 작업을 시작하며, 이후 트랜잭션이 커밋될 때 데이터 충돌 여부를 확인하고, 충돌이 발생한 경우는 롤백하여 다시 시도하는 방식입니다.
낙관적 락은 데이터 충돌을 후처리 방식으로 처리하기 때문에, 트랜잭션 시작 시 락을 잡지 않고, 트랜잭션이 끝날 때 충돌을 감지하는 방식입니다.
구체적인 동작 방식을 살펴보겠습니다.
1. 데이터 읽기
- 트랜잭션이 데이터를 읽을 때, 버전 번호(Version Number) 혹은 타임스탬프(Timestamp)와 같은 추가적인 메타데이터도 함께 읽습니다. 이 메타데이터는 해당 데이터가 언제 수정되었으며 몇 번째 버전인지를 나타냅니다.
2. 데이터 수정 단계
- 트랜잭션이 데이터를 수정하려고 할 때, 수정하려는 데이터의 버전 번호를 다시 확인합니다.
3. 충돌 감지
- 데이터의 버전 번호나 타임스탬프가 수정되었을 경우, 이는 다른 트랜잭션이 해당 데이터를 수정했다는 의미이며 이를 '충돌'로 간주하여 트랜잭션은 롤백처리합니다.
4. 트랜잭션 커밋
- 충돌이 없으면, 트랜잭션은 데이터를 수정하고 커밋을 합니다.
장점과 단점
낙관적 락의 장점 ⚡ | 낙관적 락의 단점 ⚠️ |
락으로 인한 오버헤드가 없어 성능 최적화 가능 | 충돌 처리 로직 구현 및 충돌로 인한 성능 저하 |
데드락 이슈에서 자유로움 | 락을 통한 실시간 충돌 방지 불가 (롤백을 통해서만 충돌 처리) |
버전 번호나 타임 스탬프를 이용한 단순하고 직관적인 구현 가능 | 애플리케이션 로직에 대한 의존성 증가 |
구현 방법
데이터베이스 이용
1. 버전 정보 추가
- 데이터베이스 테이블에 버전(version) 혹은 타임스탬프(timestamp) 컬럼을 추가합니다.
2. 데이터 읽기 시 버전 정보 포함
- 트랜잭션에서 데이터를 읽을 때, 해당 행의 버전 정보도 함께 읽어옵니다.
- 이 정보를 나중에 업데이트할 때 기준으로 사용합니다.
3. 업데이트 시 버전 체크
- 수정하려는 데이터에 대해 업데이트 쿼리의 WHERE 조건에 기존에 읽어온 버전 값을 포함시킵니다.
- 예를 들어, SQL 업데이트 쿼리는 아래와 같이 구성할 수 있습니다.
UPDATE my_table
SET column1 = 'newValue', version = version + 1
WHERE id = ? AND version = ?;
- 만약 WHERE 조건(id와 version)에 해당하는 행이 존재하지 않는다면, 이는 다른 트랜잭션이 이미 데이터를 수정했음을 의미합니다.
4. 충돌 감지 및 처리
- 업데이트 쿼리 실행 결과 수정된 행의 수가 0이라면, 낙관적 락 충돌이 발생한 것으로 판단합니다.
- 이 경우, 애플리케이션은 예외를 발생시키거나, 사용자에게 충돌 사실을 알리고 재시도 혹은 보상 트랜잭션 처리를 수행할 수 있습니다.
Spring Data JPA 이용
Spring Data JPA에서는 낙관적 락 구현을 위해 @Version 어노테이션을 지원하고 있습니다.
해당 어노테이션을 사용한 엔티티를 조회 및 수정후 저장하는 코드에서는 OptimisticLockException을 핸들링하여 충돌상황을 처리할 수 있습니다.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
// 낙관적 락을 위한 버전 정보 필드
@Version
private Integer version;
}
문제 상황과 해결 방법
낙관적 락을 사용하면서 발생할 수 있는 문제로는 처음 예상과 달리 높은 충돌률로 인하여, 재시도 로직과 보상트랜잭션으로 인한 성능 저하가 있습니다.
이를 해결하기 위해서는 낙관적 락을 통한 동시성 제어가 적합한지에 대해 다시한번 고민하며 비관적 락의 도입을 고민해보고, 트랜잭션 범위를 축소시켜 다른 트랜잭션과의 충돌 가능성을 상대적으로 낮추도록 유도할 수 있습니다.
낙관적 락 사용이 적합한 환경
- 읽기 작업이 많은 환경
- 데이터 충돌이 드물고, 충돌을 처리할 능력이 있다고 판단된 경우
- 트랜잭션의 크기가 작고, 짧은 시간 동안 데이터 수정이 이루어지는 경우
- 사용자 경험을 중요시하는 환경
비관적 락 (Pessimistic Lock)
정의와 동작 원리
비관적 락은 데이터에 대한 동시 접근이 발생할 때 항상 충돌이 발생할 수 있다고 가정하고, 데이터에 접근 하기 전에 해당 데이터에 대한
락(lock)을 걸어 다른 트랜잭션이 동시에 수정하거나 읽지 못하도록 제어하는 방법입니다.
주로 동시 수정 가능성이 높은 환경이나, 데이터 무결성이 매우 중요한 경우에 사용됩니다.
비관적 락의 동작원리는 다음과 같습니다.
1. 락 획득
- 트랜잭션이 데이터를 읽거나 수정하기 전에, 해당 데이터(행 또는 테이블)에 대해 락을 요청합니다.
- DB는 요청받은 락을 할당하여, 다른 트랜잭션이 동일한 데이터에 대해 접근 할 수 없도록 합니다.
2. 락 유지
- 락은 해당 트랜잭션이 커밋 또는 롤백 될 때까지 유지됩니다.
- 이 기간 동안 다른 트랜잭션은 락이 걸린 데이터에 접근하려고 대기 상태가 됩니다.
3. 대기 및 타임아웃
- 다른 트랜잭션이 락을 획득하기 위해 대기할 수 있으며, 일정 시간 이상 대기하면 타임아웃(timeout) 혹은 예외가 발생할 수 있습니다.
장점과 단점
비관적 락의 장점 ⚡ | 비관적 락의 단점 ⚠️ |
데이터 무결성 보장 | 락으로 인한 오버헤드 발생 |
단순한 동시성 제어(동시성 문제 처리가 명시적이고 직관적) | 데드락 위험 |
낙관적 락보다 충돌 처리가 단순하며, 별도 로직 구현 필요 없음 | 낮은 동시성 환경에서는 비효율적임 |
구현 방법
데이터베이스 이용
데이터베이스에서 특정 행을 조회하면서 동시에 베타적 락(exclusive lock)을 획득합니다.
해당 행은 현재 트랜잭션이 커밋하거나 롤백하기 전까지 다른 트랜잭션이 읽거나 수정 할 수 없습니다.
BEGIN;
-- 특정 행을 조회하면서 락 획득
SELECT * FROM orders
WHERE order_id = 1001
FOR UPDATE;
-- 이후 업데이트 수행
UPDATE orders
SET status = 'PROCESSED'
WHERE order_id = 1001;
COMMIT;
LOCK TABLE 명령을 통해 테이블 단위의 락을 이용할 수도 있으며, 이 방식은 단일 행이 아니라 전체 테이블에 락이 걸리므로, 주의하여 사용해야합니다.
Spring Data JPA 이용
Spring Data JPA에서는 @Lock 어노테이션을 제공하여 비교적 쉽게 비관적 락을 구현할 수 있습니다.
public interface OrderRepository extends JpaRepository<Order, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT o FROM Order o WHERE o.id = :id")
Optional<Order> findByIdForUpdate(@Param("id") Long id);
}
문제 상황과 해결 방법
비관적 락을 잘못 사용하거나 환경에 따라 몇 가지 문제가 발생할 수 있습니다.
1. 성능 저하
락을 사용함으로 다른 트랜잭션이 자원에 접근하지 못함으로 인해 성능 저하가 발생할 수 있습니다.
해결 방법
트랜잭션의 크기를 가능한 작게 유지하여 락 유지 시간을 최소화하고, 락을 획득하기 전에 필요한 데이터만을 조회하여 락 대기 시간을 줄이도록 할 수 있습니다. 결과적으로 락을 통해 소유한 자원의 크기와 락의 유지 기간을 줄이는 것에 초점을 맞추어 성능 저하를 줄일 수 있습니다.
2. 데드락
락을 사용함으로 발생할 수 있는 대표적인 트레이드 오프관계의 이슈입니다.
여러 트랜잭션이 서로 다른 순서로 락을 획득할 경우, 데드락 상태가 발생할 수 있습니다.
하나의 트랜잭션 자원을 소유한채로 다른 자원을 요구하는 상황에서, 해당 자원을 가진 다른 트랜잭션이 처음 트랜잭션이 가지고 있는 자원을 요구할 때 데드락이 발생할 수 있습니다.
해결 방법
애플리케이션 전반에서 동일한 순서로 리소스에 락을 획득하도록 로직을 설계하고, 트랜잭션이나 DB 커넥션에 타임아웃을 설정하여 일정 시간 이상 대기하면 실패하도록 하여 데드락 발생 시 자동 회복되도록 합니다.
또한 데드락이 발생하며 발생한 예외를 적절하게 캐치하여 재시도하거나 적절히 처리합니다.
'Database' 카테고리의 다른 글
[Database] 트랜잭션 격리 수준(Isolation Level) (0) | 2024.04.17 |
---|---|
[Database] 트랜잭션(Transaction)과 ACID 개념 (1) | 2024.04.13 |
[Database] Ms-SQL DeadLock 이슈 처리 (0) | 2023.08.22 |