락
- 락은 데이터의 일관성과 무결성을 유지하기 위해 DBMS가 사용하는 공통적인 방법이다.
- DB와 같은 시스템은 같은 데이터를 동시에 접근하는 경우가 생길 수밖에 없는데, 이럴 경우 데이터가 오염될 수 있다.
락의 크기
- 락의 크기는 어느 정도의 범위를 잠글 것인지 나타낸다.
- 로우 락(Row Lock), 페이지 락(Page Lock), 테이블 락(Table Lock)이 있다.
- 만약 락을 걸어야 하는 페이지가 너무 많다면, 차라리 테이블 전체에 락을 거는 것이 잠금 비용의 손실을 줄일 수 있다.
- 잠금 비용: 잠금을 거는 과정에서 발생하는 성능 손실을 말한다.
- 페이지 락 여러 개를 테이블 하나로 대체한다면 잠금 비용은 낮아지겠지만, 대신 동시성 비용은 높아질 것이다.
- 동시성 비용: 락을 걸면서 동시성이 낮아져서 발생하는 성능 손실을 의미한다.
락의 종류
공유잠금
- 공유잠금(Shared Lock)은 가장 낮은 강도의 잠금으로, 데이터를 읽을 때 사용되는 락이다.
- 일반적으로 SELECT를 할 때 공유잠금이 발생하며, SELECT가 완료되면 즉시 공유잠금은 해제된다.
- 이런 공유잠금은 공유잠금끼리 동시에 접근이 가능하다.
- 즉, 하나의 데이터를 읽는 것은 여러 사용자가 동시에 할 수 있다.
- 하지만 공유잠금이 설정된 데이터에 배타잠금을 사용할 수는 없다.
배타잠금
- 배타잠금(Exclusive Lock)은 가장 높은 강도의 잠금이다.
- 데이터를 변경하고자 할 때 사용되며, 트랜잭션이 완료될 때까지 유지된다.
- 배타잠금이 해제될 때까지 다른 트랜잭션은 해당 리소스를 접근할 수 없다.
- 또한 해당 락은 다른 트랜잭션이 수행되고 있는 데이터에 대해서는 접근하여 함께 락을 설정할 수 없다.
- 배타잠금은 다른 모든 종류의 잠금과 호환되지 않는다.
- 즉, 어떠한 약한 잠금이라도 걸려 있는 레코드에 대해서는 Update가 불가능하며, 반대로 Update가 진행 중인 레코드에 대해서는 SELECT를 포함한 어떠한 작업도 불가능하다.
갱신잠금
- 갱신잠금(Update Lock)은 공유잠금과 배타잠금의 중간 강도의 잠금이다.
- 공유잠금과는 호환되지만 다른 갱신잠금이나 배타잠금과는 호환되지 않는다.
- 일반적으로는 Update의 Filter(Where절)가 수행되는 단계에서 갱신잠금이 걸리며, Filter 된 결과에 대해 실제로 Update를 시도할 때 갱신잠금은 배타잠금으로 전환된다.
- 공유잠금과 배타잠금이 서로 호환되지 않기 때문에 공유잠금에서 배타잠금으로 변환될 때 락 대기시간(Lock Wait)이 발생한다.
- 이 대기 시간은 공유잠금이 해제될 때까지의 시간을 의미하는데, 이때 잠재적인 교착상태에 빠질 가능성이 생긴다.
- 예를 들어 업데이트(Update)를 위해 공유잠금 모드에서 배타잠금 모드로 변환하려고 하는데, 또 다른 트랜잭션이 업데이트를 위해 공유잠금을 필요로 하는 경우, 공유잠금을 해제하려는 트랜잭션과 필요로 하는 트랜잭션 사이에 교착 상태가 된다.
- 이런 교착 상태를 해결하기 위해서 사용되는 락이 갱신잠금 모드이다.
- 갱신잠금은 잠금 힌트를 통해 업데이트문이 아닌 SELECT문에도 걸 수 있다. 보통 컨버전 데드락을 방지하기 위해 SELECT문에 갱신잠금을 거는 경우가 많다.
집중잠금
- 집중잠금(Intent Lock)은 SQL 서버에서 데이터베이스나 테이블 내의 일부 데이터 영역에 이미 공유잠금이나 배타잠금이 걸려 있다는 것을 다른 커넥션에게 알리기 위해서 사용한다.
DB 충돌 상황을 개선할 수 있는 방법
- 첫번째, 테이블의 row에 접근시 Lock을 걸고 다른 Lock이 걸려 있지 않을 경우에만 수정을 가능하게 할 수 있다.
- 두번째, 수정할 때 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것이다.
비관적 락(pessimistic lock)
- 비관적 락은 Repeatable Read 또는 Serializable 정도의 격리성 수준을 제공한다.
- SERIALIZABLE은 가장 엄격한 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 진행시킨다.
- REPEATABLE READ는 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)를 이용해 한 트랜잭션 내에서 동일한 결과를 보장하지만, 새로운 레코드가 추가되는 경우에 부정합이 생길 수 있다.
- 비관적 락이란 트랜잭션이 시작될 때 Shared Lock(공유락) 또는 Exclusive Lock(배타락)을 걸고 시작하는 방법이다.
- 즉, Shared Lock(공유락)을 걸게 되면 write를 하기위해서는 Exclucive Lock(배타락)을 얻어야하는데 Shared Lock(공유락)이 다른 트랜잭션에 의해서 걸려 있으면 해당 Lock을 얻지 못해서 업데이트를 할 수 없다.
- 수정을 하기 위해서는 해당 트랜잭션을 제외한 모든 트랜잭션이 종료(commit) 되어야합니다.
- Transaction_1 에서 table의 Id 2를 읽음 ( name = Karol )
- Transaction_2 에서 table의 Id 2를 읽음 ( name = Karol )
- Transaction_2 에서 table의 Id 2의 name을 Karol2로 변경 요청 ( name = Karol )
- 하지만 Transaction 1에서 이미 shared Lock을 잡고 있기 때문에 Blocking
- Transaction_1 에서 트랜잭션 해제 (commit)
- Blocking 되어있었던 Transaction_2의 update 요청 정상 처리
이렇듯 Transaction을 이용하여 충돌을 예방하는 것이 바로 비관적 락(Pessimistic Lock)입니다.
낙관적 락(optimistic lock)
- 낙관적 락은 수정할 때 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것입니다.
- 이 특징은 DB에서 제공해주는 특징을 이용하는 것이 아닌 Application Level에서 잡아주는 Lock입니다.
- A가 table의 Id 2를 읽음 ( name = Karol, version = 1 )
- B가 table의 Id 2를 읽음 ( name = Karol, version = 1 )
- B가 table의 Id 2번, version 1인 row의 값 갱신 ( name = Karol2, version = 2 ) 성공
- A가 table의 Id 2번, version 1인 row의 값 갱신 ( name = Karol1, version = 2 ) 실패
- Id 2번은 이미 version이 2로 업데이트 되었기 때문에 A는 해당 row를 갱신하지 못함
- 위 flow를 통해서 같은 row에 대해서 각기 다른 2개의 수정 요청이 있었지만 1개가 업데이트 됨에 따라 version이 변경되었기 때문에 뒤의 수정 요청은 반영되지 않게 되었다.
- 이렇게 낙관적 락은 version과 같은 별도의 컬럼을 추가하여 충돌적인 업데이트를 막는다.
- version 뿐만 아니라 hashcode 또는 timestamp를 이용하기도 한다.
낙관적 락은 version 등의 구분 컬럼을 이용해서 충돌을 예방한다.
롤백(Rollback)
- 만약 업데이트를 하는 테이블이 1개가 아니라 2개의 테이블이고, 2번째 테이블을 업데이트하다 이와같은 충돌이 발생했다면 하나의 수정 요청에 대해서는 롤백이 필요하게된다.
2개의 테이블을 수정하는 비관적 락의 수도코드
- 하나의 트랜잭션으로 묶여있기 때문에 수정이 하나 실패하면 database 단에서 전체 Rollback이 일어나게된다.
- 만약
theTable
이 실패할 때. Transaction이 실패한 것이기 때문에 트랜잭션 전체에 자동으로 rollback이 일어나게 된다.
SELECT id, `name`
FROM theTable
WHERE id = 2;
{새로운 값으로 연산하는 코드}
BEGIN TRANSACTION;
UPDATE anotherTable
SET col1 = @newCol1,
col2 = @newCol2
WHERE id = 2;
UPDATE theTable
SET `name` = 'Karol2',
WHERE id = 2;
{if AffectedRows == 1 }
COMMIT TRANSACTION;
{정상 처리}
{else}
ROLLBACK TRANSACTION;
{DB 롤백 이후 처리}
{endif}
낙관적 락의 수도코드
- 비관적 락과 달리 Transaction으로 잡지 않는다.
- 만약 충돌이 발생하여 수정을 못한 부분에 대해서는 롤백에 대한 책임을 Application 단에서지며 Application에서 롤백을 수동으로 해줘야 한다.
SELECT id, `name`, `version`
FROM theTable
WHERE iD = 2;
{새로운 값으로 연산하는 코드}
UPDATE theTable
SET val1 = @newVal1,
`version` = `version` + 1
WHERE iD = 2
AND version = @oldversion;
{if AffectedRows == 1 }
{정상 처리}
{else}
{롤백 처리}
{endif}
낙관적 락과 비관적 락을 언제 사용할까?
- 낙관적 락은 트랜잭션을 필요로하지 않는다. 따라서 성능적으로 비관적 락보다 더 좋다. → 낙관적 락의 장점
- 트랜잭션을 필요로 하지 않기 때문에 아래와 같은 로직의 흐름을 가질때도 충돌 감지를 할 수 있다.
- 클라이언트가 서버에 정보를 요청
- 서버에서는 정보를 반환
- 클라이언트에서 이 정보를 이용하여 수정 요청
- 서버에서는 수정 적용 ( 충돌 감지 가능 )
- 반면 비관적 락은 1~3에서 트랜잭션을 유지할 수 없다.
따라서 충돌이 많이 일어나지 않을 것이라고 보여지는 곳에 사용하면 좋은 성능을 기대할 수 있다.
- 하지만 낙관적 락의 최대 단점은 롤백이다.
- 만약 충돌이 났을 때,이를 해결하려면 개발자가 수동으로 롤백처리를 해줘야합니다.
- 비관적 락이라면 트랜잭션을 롤백하면 끝나는 작업이지만 낙관적 락은 그렇지 않다.
- 수동으로 롤백처리는 구현하기도 까다롭고, 성능적으로 볼 때,
update
를 한번씩 더 해줘야한다. - 결론적으로 충돌 발생 확률이 낮고 성능 저하를 예방하려면 낙관적 락을 사용하면 되고, 충돌을 미연에 방지하고 데이터의 일관성을 유지하려면 비관적 락을 사용하면 된다.
JPA에서의 낙관적 락과 비관적 락
낙관적 락
@Entity
public class SampleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
private String data;
}
@Version
어노테이션을 이용하여 version 필드로 지정- 데이터를 수정할 때 같은 id 값이지만 다른 사용자에 의한 변경이 발생하면 version 값이 다르게 되고, 이때 예외가 발생하므로 충돌로부터 안전하게 처리할 수 있다.
비관적 락
@Service
public class SampleEntityService {
@Autowired
private SampleEntityRepository sampleEntityRepository;
@Autowired
private EntityManager entityManager;
@Transactional
public SampleEntity updateDataWithPessimisticLock(Long id, String newData) {
SampleEntity sampleEntity = entityManager.find(SampleEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
sampleEntity.setData(newData);
entityManager.flush();
return sampleEntity;
}
}
- EntityManager의 find 메소드에 락 타입(
LockModeType.PESSIMISTIC_WRITE
)을 지정하여 데이터에 락을 걸어두고, 변경 작업이 끝난 후에 락을 해제한다. - 이를 통해 다른 트랜잭션이 동시에 수정할 수 없어 동시성 처리 이슈를 방지할 수 있게 된다.
Reference
https://mangkyu.tistory.com/299
https://mozzi-devlog.tistory.com/37
https://hudi.blog/jpa-concurrency-control-optimistic-lock-and-pessimistic-lock/
'개발 공부 > Database' 카테고리의 다른 글
Trasaction(트랜잭션) 기본적으로 톺아보기 (2) | 2025.01.16 |
---|---|
DB | Join (0) | 2023.09.20 |
DB | 트랜잭션, 동시성 제어, 회복 (1) | 2023.08.28 |
DB | 정규화 (0) | 2023.08.28 |
DB | 데이터 모델링 (1) | 2023.08.28 |