들어가기
개발을 하다보면 문득 이런 생각을 할 수 있다.
내가 데이터를 수정해야하는 상황이 생겼는데, 만약에 이 데이터와 관련해서 동
시에 충돌이 일어난다면 어떻게 해야할까? 어떻게 될까? 라는 고민을 해볼 수 있다. 대표적으로 동시성 문제인데, 예시를 들자면 서비스에 있는 좋아요 기능을 계속 누르게되면 찰나의 순간에 똑같은 데이터가 들어갈 여지가 생긴다. 이러한 상황 속에서 백엔드 개발자는 이러한 상황을 예측해서 개발을 진행해야 한다.
이러한 상황속에서 고민한 내용에 관한 학습 내용을 정리하고 기록하고자 한다.
일반적으로 우리는 예기치 못한 상황이 발생할 경우를 대비해서 "예외처리" 라는 것을 한다. 말 그대로 예외가 생겼을 때, next step으로 넘어갈 수 있게 도와주는데 이 상황 역시 마찬가지이다.
그래서 이렇게 데이터의 수정 혹은 비슷한 상황에서 해결책으로 제시할 수 있는 방법은 2가지로 생각된다.
1. 테이블의 row에 접근할 때, Lock을 걸고, Lock이 걸려있지 않은 경우에만 수정이 가능토록 한다.
2. 수정에 관한 순서를 보장해서 동일한 조건의 값으로 수정할 수 없게 한다.
낙관적 락 그리고 비관적 락
"들어가기" 에서 Lock이라는 단어를 언급했다.
" 그럼 Lock이 무엇일까? "
Lock은 말 그대로 잠그다 라는 뜻을 가지게 되는데 DB의 개념과 접목시키면 트랜잭션의 격리 수준과 트랜잭션이라는 개념을 알고 있어야 한다.
트랜잭션은 데이터베이스와 관련된 작업을 수행하는 단위라고 생각하면 쉽다.
이 트랜잭션은 ACID( 원자성,일관성,격리성,영속성 )이라는 속성을 보장해야 하는데, 트랜잭션 자체에서 원자성과 일관성, 지속성은 보장해주지만 격리성에서 문제가 발생한다.
트랜잭션의 격리 수준 (Transaction Isolation Level)은 과거에 정보처리기사를 공부할 때, 학습한 내용이였는데, 간략하게설명하자면 동시에 여러 트랜잭션을 처리해야할 때, 트랜잭션이 얼마나 서로 격리되어있는지를 말한다. 쉽게 말해 이 트랜잭션이 저 트랜젝션에서 변경한 데이터를 볼 수 있는 기준을 결정하는 것이다.
격리 수준은 4개로 나눌 수 있다.
- READ UNCOMMITED
- READ COMMITED
- REPETABLE READ
- SERIALIZABLE
각각을 간단하게 설명하자면
우선 첫번째 READ UNCOMMITED은 말 그대로 아직 커밋되지 않은 데이터를 읽을 수 있는데, 이 때 Dirty Read, Dirty Write가 발생할 수 있다
- Dirty Read : 트랜잭션에 의해 아직 수정되었지만 아직 커밋되지 않은 데이터를 읽는 것
- Dirty Write : 내가 데이터를 수정했지만 수정한 내용이 반영되지 않고 처음인 상태로 돌아가는 것
두번째 READ COMMITED는 커밋된 데이터만 읽을 수 있으며, 오라클 DBMS에서 표준으로 사용하고 있고 가장 많이 선택한다. 이 때 Lost Update, Write Skew, Reds Skew가 발생할 수 있다.
- Lost Update : 업데이트한 내용이 사라지는 현상
- ex ) 내 계좌엔 1000원, 친구의 계좌엔 500원, 내가 친구에게 500을 이체하는 트랜잭션 1, 내가 200을 입금하는 트랜잭션 2가 동시에 실행됐다고 가정했을 때, 정상적으로 동작했다면 내 계좌엔 700, 친구의 계좌엔 1000, 그러나 트랜잭션의 결과를 확인했을 때, 내 계좌엔 500, 친구의 계좌엔 1000 -> 트랜잭션 2의 내용이 반영되지 않음
- Write Skew : 쓰기를 실행하는 시점에, 읽은 시점에 내린 결정의 상태와 달라 참이 아닌 상태
세번째 REPETABLE READ는 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다 안보였다 하는 현상으로 Undo를 사용해 시점을 보장하지만, 새로 삽입된 레코드는 Undo가 없기 때문에 발생한다.
(innoDB 에서 넥스트 키 락을 이용해 Phantom read 문제를 해결한다)
마지막으로 SERIALIZABLE는 모든 트랜잭션을 순서대로 실행한다.
이러한 트랜잭션의 격리 수준이 높아질 수록 자원을 많이 사용하게되고, 성능이 떨어지는데, 그래서 적절한 격리를 선택해서 DB의 특성도 보호하면서 성능도 체크해야하는 이슈들이 있다.
----------이제 본론으로 넘어가보자 ----------
비관적 락(pessimistic lock)
비관적 락은 트랜잭션이 충돌한다는 가정에서 락을 걸게 되는데 데이터 수정 시 즉시 트랜잭션의 충돌여부를 확인할 수 있다. 비관적 락은 Repeatable Read, Serializable 정도의 격리성을 제공하며, 트랜잭션이 시작될 때 Shared Lock 또는 Exclusive Lock을 걸고 시작하는 방법이다.
이렇게 Shared Lock을 걸게 되면 write를 하기 위해서 Excelucive Lock을 얻어야 하는데, Shared Lock이 다른 트랜잭션에 의해 걸려있으면 해당 Lock을 얻지 못해 업데이트가 불가능하다. 그래서 수정을 하기 위해선 해당 트랜잭션을 제외한 모든 트랜잭션이 Commit 되어야 한다.
ex )
1번 트랜잭션은 1번 데이터 kakoa를 읽고있다.
2번 트랜잭션도 마찬가지로 1번 데이터를 읽고 있다.
2번 트랜잭션은 1번 데이터를 naver로 수정하고 싶어 한다.
이 때, Transaction 1에서 이미 shared lock을 잡고 있기때문에 이 요청은 막힌다(Blocking)
이 후 1번 트랜잭션이 commit 되면 이제 blocking 되어있던 요청이 정상 처리된다.
이렇게 트랜잭션을 이용해 충돌을 예방하는 방법이 비관적 락이다.
이렇게 Shared Lock을 걸게 되면 write를 하기 위해서 Excelucive Lock을 얻어야 하는데, Shared Lock이 다른 트랜잭션에 의해 걸려있으면 해당 Lock을 얻지 못해 업데이트가 불가능하다. 그래서 수정을 하기 위해선 해당 트랜잭션을 제외한 모든 트랜잭션이 Commit 되어야 한다.
낙관적 락(Optimistic lock)
낙관적 락은 수정사항이 생겼을 때, 해당 값을 수정했다고 명시해서 다른 트랜잭션에서 동일한 조건으로 값을 수정할 수 없게 하는 것인데, 이는 DB의 특징을 이용하는 것이 아닌 Applicaiton Level에서 catch하는 Lock이다.
트랜잭션이 충돌하지 않는다고 가정을 하며, 자원에 락을 걸어서 선점하는 것이 아닌, 커밋시 동시성 문제가 발생하면 그 시점에서 처리하자는 방법이다. 그래서 트랜잭션을 커밋하기 전까지 충돌 여부를 알 수 없다. 그래서 낙관적 락은 version이나 timestamp 등을 이용해서 데이터의 버전을 관리해야 한다.
ex)
1. 내가 테이블의 2번 ID를 읽었다 ( kakao ,version 1)
2. 다른 사람이 테이블의 2번을 읽었다 (kakao, version 2)
3. 다른 사람이 테이블 2번의 데이터를 변경했다 ( naver, version 2) - 성공
4. 내가 테이블 2번의 데이터를 수정하고자 한다 (naver, version 2) - 실패
이미 테이블 2번의 데이터가 version 2로 업데이트 되었기에 나는 데이터를 수정할 수 없다.
그래서 낙관적 락은 version 등의 구분 컬럼을 이용해서 충돌을 예방하는 방법이다.
그럼 어떤 상황에서 어떤 락이 효율적이고, 어떻게 사용해야할까?
먼저 윗 내용을 간단하게 정리하자면 낙관적 락은 트랜잭션을 필요로 하지 않는다. 이 말은 성능적으로 비관적 락보다 좋다. 그래서 아래처럼 로직의 흐름을 가질 때도 충돌 감지를 할 수 있다.
- 클라이언트가 서버에 정보를 요청
- 서버에서는 정보를 반환
- 클라이언트에서 이 정보를 이용하여 수정 요청
- 서버에서는 수정 적용 ( 충돌 감지 가능 )
여기서 만약에 내가 비관적 락이라면 1~3번의 트랜잭션을 유지할 수 없다.
하지만 낙관적 락의 최대 단점은 롤백인데, 만약 충돌이 발생했을 때, 이를 해결하려면 개발자가 직접 롤백 처리를 해야한다. 트랜잭션을 이용하는 비관적 락이면 트랜잭션을 롤백해버리면 되지만 낙관적 락은 그렇지 않다. 그래서 수동으로의 롤백 처리는 구현하기에도 번거롭고 성능적으로도 추가적인 update가 발생할 여지가 생긴다
그래서 낙관적 락은 충돌이 많이 발생하거나 비용이 많이 발생할 것이라고 판단되는 곳에는 사용하지 않는 것이 좋다고 생각한다.
마무리
참고 레퍼런스 주소
https://jaehoney.tistory.com/159
https://sabarada.tistory.com/175
https://hudi.blog/jpa-concurrency-control-optimistic-lock-and-pessimistic-lock/
결국 적재적소에 맞는 Lock을 사용해서 데이터를 보호하고, 성능적인 이슈도 챙겨가야함을 알게 되었다.
JPA에서는 @Version을 통해 낙관적 락을 관리할 수 있으며, ( 사용 및 관련 속성은 검색해볼 것 )
비관적 락을 사용하려고 하면 @Lock 을 사용하면 된다 .( 사용 및 관련 속성은 검색해볼 것 )
또한 JPA의 동시성 제어 메커니즘은 특정 엔티티에 관한 동시 접근을 막기 위해 사용하지만, 트랜잭션의 격리 수준은 트랜잭션 동안의 일관성 있는 데이터 읽기를 고려하기 위해 적용한다는 점.
우리가 오늘 학습한 락들은 엔티티에 관한 동시 접근에 관한 내용을 담고 있다.
'기술면접 관련 및 참고하기' 카테고리의 다른 글
Java 8 vs Java 11 ? (0) | 2023.03.05 |
---|---|
JPA의 N+1 문제 (1) | 2023.03.04 |
도커(Dokcer)가 뭔데? 도커 사용기 (2) | 2023.03.03 |
[ 10분 테코톡 ] - Stream 그리고 For Loop (0) | 2022.12.26 |
불변 객체(Immutable Object) 그리고 DTO(Data Transfer Object) (2) | 2022.12.22 |