트랜잭션은 ACID(원자성, 일관성, 격리성, 지속성)을 보장. 트랜잭션은 원자성, 일관성, 지속성을 보장하지만 문제는 격리성. 트랜잭션간 원벽한 격리를 보장하기 위해서는 동시성 측면에서 손해 봄.
ASNI 표준에서 트랜잭션 격리 수준을 4단계로 구분하여 병행성과 격리성을 설정 가능
격리성과 병행성을 반비례 관계 & 격리수준을 최대로 높으면 성능 악화 우려

트랜잭션 격리 수준으로 해결 X => 갱신 분실 문제
*) 유저1과 유저2는 동시에 수정 화면에 진입했고, 원하는 내용을 작성한 뒤, 수정 버튼을 클릭했다. 그리고 위 그림과 같이 유저1의 트랜잭션이 먼저 커밋되고 그 이후에 유저2의 트랜잭션이 커밋되었다. 이 경우 유저1의 트랜잭션의 변경 내용은 사라지게 되고, 유저2의 트랜잭션만이 데이터베이스에 반영
트랜잭션의 격리 수준으로는 ‘마지막 커밋만 인정하기’ 외의 정책을 구현할 수 없음.

두 트랜잭션에서 데이터를 변경, 최종적으로 한 트랜잭션의 결과만 남을 것을 두번의 갱신 분실 문제
*) 계좌에서 한 명은 돈을 사용하고 한 명은 돈을 저축, 그 동작이 동시 발생
User A와 B는 각각 트랜잭션을 시작할 때, 데이터베이스로부터 잔액 10,000원이라는 결과 & 각각 7,000원 사용과 5,000원 추가 저축이라는 행위 => 잔액이 8,000원
돈을 사용한 트랜잭션의 결과에 돈을 저축한 결과가 덮어 쓰여져서 돈을 사용한 결과가 분실되게 되었다. 이러한 문제를 두 번의 갱신 분실 문제
이 트랜잭션 격리 레벨로 풀자면 ? : REPEATBALE_READ는 조회한 데이터가 트랜잭션 동안 일관적으로 같은 값을 읽어올 수 있는 정도의 격리 수준 제공 하지만 그 이상의 레벨이 필요하다면 SERIALIZABLE이다.
MySQL 기준으로 SERIALIZABLE은 읽기 작업을 하는 데이터에 대해서도 shared lock을 건다. 일관된 읽기 뿐 아니라 실제 읽고 있는 데이터가 다른 트랜잭션에 의해서 변경되지 않음을 보장할 수 있다.
이 경우 두 트랜잭션이 같은 데이터에 대해 s-lock을 걸고 그 직후 서로 x-lock을 거는 상황이 연출
이 경우 데드락 발생 => s-lock과 x-lock은 양립 할 수 없음
트랜잭션은 x-lock을 걸기 위해 서로가 s-lock을 해제하는 시점을 무한히 대기하며 타임아웃될 것이다.
물론 동시성 이슈는 당장 해결은 되었지만 데드락이 발생하는 해결방법은 정상적 방법이라고는 볼 수 X
대부분의 트랜잭션은 출동이 발생한다고 가정, 트랜잭션 시작, => 데이터베이스에 락을 걸어 다른 트랜잭션이 접근하지 못하게 하는 방법.
참고로 JPA는 데이터베이스의 트랜잭션 격리 수준을 READ COMMITTED 정도로 가정함
"Shared Lock" & "Exclusive Lock"
Shared Lock (공유 락, S Lock)
특정 Row를 읽을때 사용되는 Lock
여러 트랜잭션이 동시에 한 Row에 Shared Lock을 걸 수 있음. => 하나의 Row에 트랜잭션이 동시에 읽을 수 있음
Shared Lock이 설정된 Row에는 Exclusive Lock을 사용 할 수 없다.
InnoDB에서 일반적이 Select 쿼리는 Lock 사용 X, 하지만 SELECT ... For Share등의 일부 쿼리는 각 Row에 Shared Lock을 건다
Exclusive Lock(베타 락, X Lock)
특정 Row에 변경(write) 할때 사용
특정 Row에 Exclusive Lock이 걸려 있을 경우, 다른 트랜잭션의 읽기 작업을 위해 Shared Lock을 걸거나, 쓰기 작업을 위해 Exclusive Lock을 걸 수 없음
=> 쓰기 작업을 하고 있는 Row에는 모든 접근이 불가
SELECT … FOR UPDATE, UPDATE, DELETE 등의 수정 쿼리들이 실행될 때 Row에 걸림
JPA에서 낙관적 락과 비관적 락을 사용하기 위해서는 Repo 인터페이스에 지정된 커스텀 메서드에 @Lock 어노테이션을 붙여주고, Lock 어노테이션의 설정 값인 Value 설정하고자 할때, LockModeType 지정하면 됨
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(value = LockModeType.OPTIMISTIC)
Optional<Account> findByName(String name);
}
비관적 락에는 => PESSIMISTIC_READ, PESSIMISTIC_WRITE, PESSIMISTIC_FORCE_INSERT
PESSIMISTIC_READ
데이터를 반복적으로 읽기만하고 수정 X
일반적으로 사용하지 않음
SELECT ... FOR SHARE를 통해 공유 락을 건다
PESSIMISTIC_WRITE
비관적 락을 걸 때 일반적으로 사용
NON-REPEATABLE_READ 방지 X
SELECT … FOR UPDATE 를 통해 베타 락을 건다
PESSIMISTIC_FORCE_INSERT
유일하게 버전 정보를 사용하는 락 & 버전 정보를 강제로 증가
하이버네이트의 경우 nowait를 지원, 데이터베이스에 대해 FOR UPDATE NOWAIT 옵션을 적용 & 그렇지 않다면 FOR UPDATE 적용
트랜잭션 A가 X테이블의 1번 데이터 row에 Lock을 건다.
트랜잭션 B가 Y테이블의 1번 데이터 row에 Lock을 건다.
트랜잭션 A가 Y테이블의 1번 데이터 row에 접근한다.
트랜잭션 B가 이미 Lock을 걸어놔서 대기한다.
트랜잭션 A가 이미 Lock을 걸어놔서 대기한다.
이렇게 되면 서로 다른 트랜잭션이 각자 자원을 점유하고, 상대방이 가진 자원을 얻기위해 무한히 대기하는 데드락이 발생.
트랜잭션이 row에 락을 걸어버리니까 데드락 문제가 발생할 수 없지 않을까라고 생각 할 수 있음
비관적 락을 적용해도 상황에 따라 데드락 발생 할 수 있음
대부분의 트랜잭션은 충돌이 발생하지 않는다고 낙관적으로 가정
데이터베이스가 제공하는 락이 아닌 애플리케이션 레벨에서 락을 구현, JPA에서는 버전 관리 기능 @Version을 통해 구현
애플리케이션에서 충돌을 관리하기에 트랜잭션을 커밋하기 전까지 충돌 알수 없음
해당 어노테이션이 붙은 필드를 포함하는 엔티티를 정의, 해당 엔티티 테이블을 읽는 각 트랜잭션은 업데이트를 수행하기 전에 버전 속성을 확인. 만약 데이터를 읽고 업데이트를 하기 이전에 버전 값이 변경 -> OptimisticLockException을 발생 & 해당 updated 취소
@Version 필드를 선언 할때의 규칙
@Entity
@NoArgsConstructor
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Long money;
@Version
private int version;
public Account(String name, Long money) {
this.name = name;
this.money = money;
}
}

None
엔티티에 버전 필드가 존재 & 락 모드가 정의 X -> 적용되는 Option
조회한 엔티티를 수정하는 시점에 다른 트랜잭션으로부터 변경되지 않음을 보장
Optimistic
None의 경우 엔티티를 수정할 때만 버전을 체크, 해당 옵션을 추가하면 조회만 해도 버전 체크
즉 한번 조회한 엔티티는 종료 때까지 다른 트랜잭션에서 변경 X
READ 타입과 동일
OPTIMISTICFORCEINCREMENT
낙관적 락을 사용하면서 버전 정보를 항상 강제로 증가
엔티티가 물리적으로 변경 X, 논리적 변경 -> 버전 정보 증가 => 논리적 단위로 엔티티 관리시 사용
1 : N 관계의 엔티티에서 1의 변화는 없는데 N에 데이터가 추가되거나 제거, 1에는 물리적 변화 X & 논리적 변화는 발생 가능 => 이럴 경우 버전 변경 가능
WRITE 타입과 동일
프로세스(여기서는 트랜잭션)들이 자원을 점유(Lock을 획득)한 상태에서 서로 다른 프로세스(트랜잭션)가 점유하고 있는 자원(Lock)을 요구하며 무한정 기다리는 상황 => 낙관적 락으로 문제 해결 불가능
만약 RDB가 분산되어 있는 환경이라면 비관적 락으로도 동시성 문제 해결 할 수 없음
DB가 분산되어 있는 환경이라면 DB로 가는 요청을 단일진입점으로 만들고 순차적 요청 처리 => 분산락
단일 DB 환경에서도 분산락을 사용 할 수 있다.
분산락은 서로 다른 프로세스가 상호 배타적인 방식으로 공유 리소스와 함께 작동 => 많은 환경에서 유용한 Lock
서로 다른 프로세스가 상호 배타적인 방식으로 공유 리소스와 함께 작동해야 하는 많은 환경에서 유용한 Lock
분산락은 Lock 리소스 보안에 따라 두가지 방식으로 나눌 수 있음
1. 비동기 복제 기반 분산 시스탬 ex) MySQL, Redis, Tair
2. Paxos 기반 분산 합 시스템 ex) ZooKeeper
1번 방법은 2번 방법에 비해 데이터 손실 위험 있음. TTL 메커니즘을 통해 세분화된 Lock 제공
특히 Redis의 Redisson은 자체 TTL 메커니즘 제공
2번 방법은 합의 프로토콜을 통해 여러 데이터 복사본을 보장 & 높은 데이터 보안성 제공
Lettuce는 스핀락을 사용
스핀락에는 Lock 없는 프로세스는 락을 획득하기 위해 무한루프
CPU 쓸데없이 낭비 => 성능 저하 가능성
Redisson
스픽락을 사용하지 않고 pubsub 기능을 사ㅛㅇ
pubsub이란 인터럽트 처럼 Lock 획득을 기다리는 클라이언트에서 Lock 획득할 수 있다는 신호를 보내줌
Lock 획득을 위해 무한 루프를 돌 필요 없음
) Redis에서 분락락 작동 방법
) - SET 명령어와 NX, PX 옵션등 제공
비관적 락
데이터베이스 레벨의 락을 이용, 무결성 중요, 충돌이 많이 발생해서 잦은 롤백으로 인한 효율성 문제가 발생하는 것 -> 예상되는 시나리오에 적용하면 좋음. 단 DB상에 락을 걸면 성능상 손해 볼 수 있다.
낙관적 락
실제로 데이터 충돌 자주 일어나지 않을 것 예상되는 시나리오에 적용.
하지만 추가적 오류가 처리 필요 & 동시 접근 많이 발생하면 오류 처리를 위해 더 많은 리소스 소모
***) 참고 블로그