일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- CHECK OPTION
- fetch
- shared lock
- SQL프로그래밍
- 데코레이터
- 다대일
- execute
- 비관적락
- 연결리스트
- 스프링 폼
- 낙관적락
- exclusive lock
- eager
- 동적sql
- 스토어드 프로시저
- FetchType
- 즉시로딩
- JPQL
- 일대다
- 힙
- PS
- 다대다
- querydsl
- 유니크제약조건
- 이진탐색
- BOJ
- 지연로딩
- dfs
- 연관관계
- 백트래킹
- Today
- Total
흰 스타렉스에서 내가 내리지
[JPA] ACID, 트랜잭션과 락 본문
# 트랜잭션과 락
트랜잭션은 ACID 라 하는 원자성 (Atomocity), 일관성 (Consistency), 격리성 (Isolation), 지속성 (Durability) 을 보장해야 한다
1. 원자성
트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하든가 모두 실패해야 한다.
START TRANSACTION
-- 이 블록안의 명령어들은 마치 하나의 명령어 처럼 처리됨
-- 성공하던지, 다 실패하던지 둘중 하나가 됨.
A의 계좌로부터 인출;
B의 계좌로 입금;
COMMIT
2. 일관성
모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
트랜잭션이 진행되는 동안에 데이터베이스가 변경되더라도, 업데이트된 데이터베이스로 트랜잭션이 진행되는 것이 아니라, 처음에 트랜잭션을 진행하기 위해 참조한 데이터베이스로 진행된다.
3. 독립성 (격리성)
동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리해야 한다.
어떤 하나의 트랜잭션이라도, 다른 트랜잭션의 연산에 끼어들 수 없다.
4. 지속성
트랜잭션이 성공적으로 완료됐을 경우, 결과는 영구적으로 반영되어야 한다.
트랜잭션은 원자성, 일관성, 지속성을 보장한다.
문제는 격리성인데, 트랜잭션 산에 격리성을 완벽히 보장하려면 트랜잭션을 직렬화하여 실행해야 한다.
이렇게 하면 동시성 처리 성능이 매우 나빠진다.
JPA 는 데이터베이스 트랜잭션 격리 수준을 READ COMMITTED 정도로 가정한다.
만약 일부 로직에 더 높은 격리 수준이 필요하면 낙관적 락과 비관적 락 중 하나를 사용하면 된다.
# 데이터베이스에서 락을 사용하는 이유
데이터의 무결성과 일관성을 유지하기 위해.
여러 사용자나 프로세스가 동시에 데이터베이스에 접근할 때, 적절한 동시성 제어 없이는 데이터가 예기치 않게 변경되거나 손상될 수 있다.
무결성 : 신뢰할수 있는 서비스 제공을 위해서 의도하지 않은 요인에 의해 데이터가 변경되거나 손상되지 않고 완전성, 정확성, 일관성을 유지함을 보장하는 특성
Shared Lock 은 데이터를 읽고 있는 동안에는 해당 데이터에 대한 수정이 불가능하게 하여 일관성을 보장한다.
락을 통해 트랜잭션들이 데이터에 접근하는 순서를 제어하여, 데이터베이스의 일관된 상태를 유지하도록 한다
# 낙관적 락 :: Optimistic Lock
- 수정할 때 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것
- DB 에서 제공하는 락 기능을 사용하는 것이 아니라 JPA 가 제공하는 버전 관리 기능을 사용한다. 즉, 애플리케이션이 제공하는 락.
- A 가 table 의 id 1번을 읽음 (name = Alice, version = 1)
- B 가 table 의 id 1번을 읽음 (name = Alice, version = 1)
- B 가 table 의 id 1번, version = 1 인 row 값 갱신 (name = Bob, version = 2) → 성공
- A 가 table 의 id 1번, version = 1 인 row 값 갱신 (name = Son, version = 2) → 실패
- id 1번은 이미 version 이 2로 업데이트 되었기 때문에 A 는 해당 row 를 갱신하지 못함
- 위 flow 를 통해서 같은 row 에 대해서 각기 다른 2개의 update 요청이 있었지만, 1개가 업데이트됨에 따라 version 이 변경되었기 때문에 뒤의 수정 요청은 반영되지 않게 됨
- 이렇게 낙관적 락은 version 과 같은 별도의 컬럼을 추가하여 충돌을 막는다.
- version 뿐만 아니라 hashcode 또는 timestamp 를 이용하기도 한다.
- 즉, 낙관적 락은 version 등의 구분 컬럼을 추가로 이용해서 충돌을 예방한다.
UPDATE BOARD
SET
TITLE = ?,
VERSION = ? --(버전 + 1 증가)
WHERE
ID = ?
AND VERSION = ?; -- (버전 비교)
# 비관적 락 :: Pessimistic Lock
- 비관적 락이란 트랜잭션이 시작될 때 Shared Lock 또는 Exclusive Lock 을 걸고 시작하는 방법
- 즉, Shared Lock 을 걸게 되면 write 를 하기 위해서는 Exclusive Lock 을 얻어야 하는데 Shared Lock 이 다른 트랜잭션에 의해서 걸려 있으면 해당 Lock 을 얻지 못해서 업데이트를 할 수 없다.
- 수정을 하기 위해서는 해당 트랜잭션을 제외한 모든 트랜잭션이 종료(commit) 되어야 한다.
- A 가 table 의 id 1번을 읽음 (name = Alice)
- B 가 table 의 id 1번을 읽음 (name = Alice)
- B 가 table 의 id 1번의 name 을 Bob 으로 update 요청 (name = Bob)
- 하지만 A 가 이미 Shared Lock 을 잡고 있기 때문에 Blocking
- A 에서 작업을 마치고 트랜잭션 종료 (commit)
- Blocking 되어 있었던 B 의 update 요청 정상 처리
- 이렇듯 Transaction 을 이용하여 충돌을 예방하는 것이 비관적 락이다
- 데이터베이스가 제공하는 락 기능을 사용한다.
- 대표적으로 select for update 구문이 있다.
- select ~ for update : "데이터를 수정하려고 SELECT 하는 중이야! 건들지마 !" . 동시성 제어를 위하여 특정 데이터(row)에 대해 배타적 Lock 을 거는 기능
# 동시성 제어란? :: Concurrency Control
- 동시에 실행되는 여러 개의 트랜잭션이 작업을 성공적으로 마칠 수 있도록 트랜잭션의 실행 순서를 제어하는 방법
# Shared Lock 과 Exclusive Lock
Shared Lock 과 Exclusive Lock 은 데이터베이스 시스템에서 동시성을 제어하기 위해 사용되는 두 가지 유형의 락이다.
데이터의 일관성과 무결성을 유지하면서 여러 사용자나 프로세스가 데이터베이스에 동시에 접근하는 것을 관리하는 데 필요하다.
- Shared Lock
- 정의 : Shared Lock 은 특정 데이터 항목을 여러 트랜잭션이 동시에 읽을 수 있도록 허용하는 락이다. 즉, 데이터 항목에 대한 읽기 작업을 수행할 때 주로 사용된다.
- 특징 : 한 데이터 항목에 대해 여러 개의 Shared Lock 이 존재할 수 있다. 하지만, 그 데이터에 Exclusive Lock 이 걸려있는 동안은 Shared Lock 을 획득할 수 없다.
- 사용 예 : 여러 사용자가 동시에 같은 데이터를 조회하는 상황에서 사용됨
- Exclusive Lock
- 정의 : Exclusive Lock 은 한 번에 하나의 트랜잭션만 특정 데이터 항목에 대한 접근을 허용하는 락이다. 데이터를 수정할 때 주로 사용된다.
- 특징 : 어떤 데이터 항목에 Exclusive Lock 이 걸리면, 다른 어떤 트랜잭션도 그 데이터 항목에 대해 Shared Lock 이나 Exclusive Lock 을 획득할 수 없다. 이는 데이터의 수정 중에 일관성을 보장하기 위함이다.
- 사용 예 : 데이터를 업데이트하거나 삭제하는 등의 작업을 수행할 때 사용된다.
Q. 'Transaction A' 가 데이터에 대해 Shared Lock 을 획득했다. 뒤이어 'Transaction B' 가 동일한 데이터에 대해 Shared Lock 을 획득할 수 있는가?
A. 가능하다. Shared Lock 은 여러 트랜잭션이 동시에 동일한 데이터를 읽을 수 있도록 허용하기 때문에, 한 데이터 항목에 여러 개의 Shared Lock 이 동시에 존재할 수 있다.
Q. 'Transaction A' 가 데이터에 대해 Shared Lock 을 획득했다. 뒤이어 'Transaction B' 가 데이터에 대해 Exclusive Lock 을 획득할 수 있는가?
A. 불가능하다. 어떤 트랜잭션이 데이터에 대해 Exclusive Lock 을 요구하면, 이미 걸려 있는 Shared Lock 들이 모두 해제될 때까지 Exclusive Lock 을 획득할 수 없다.
# JPA 에서 낙관적 락 사용하기 - @Version
- JPA 가 제공하는 낙관적 락을 사용하려면 @Version 어노테이션을 사용해서 버전 관리 기능을 추가해야 한다.
- @Version 적용 가능 타입은 다음과 같다
- Long (long)
- Integer (int)
- Short (short)
- Timestamp
@Entity
public class Board {
@Id
private Long id;
private String title;
@Version
private Integer version; // 버전관리 필드
}
- 위 코드와 같이 엔티티에 버전 관리용 필드를 하나 추가하고 @Version 을 붙이면 된다.
- 이제부터 엔티티를 수정할 때 마다 버전이 하나씩 자동으로 증가한다.
- 그리고 엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생한다.
// 트랜잭션 1 조회 title="제목A", version = 1
Board board = em.find(Board.class, id);
// 트랜잭션 2에서 해당 게시물을 수정해서 title="제목C", version=2 로 증가
board.setTitle("제목B"); // 트랜잭션 1 데이터 수정
save(board);
tx.commit(); // 예외 발생, 데이터베이스 version=2, 엔티티 version=1
- 트랜잭션이 데이터를 '제목B' 로 변경하고 트랜잭션을 커밋하는 순간 엔티티를 조회할 때 버전과 데이터베이스의 현재 버전 정보가 다르므로 예외가 발생한다.
- 따라서 버전 정보를 사용하면 최초 커밋만 인정하기가 적용된다.
UPDATE BOARD
SET
TITLE = ?,
VERSION = ? --(버전 + 1 증가)
WHERE
ID = ?
AND VERSION = ?; -- (버전 비교)
- 만약 데이터베이스에 버전이 이미 증가해서 수정 중인 엔티티의 버전과 다르면 UPDATE 쿼리의 WHERE 문에서 VERSION 값이 다르므로 수정할 대상이 없다. 이때는 버전이 이미 증가한 것으로 판단해서 JPA 가 예외를 발생시킨다.
- @Version 으로 추가한 버전 관리 필드는 JPA 가 직접 관리하므로 개발자가 임의로 수정하면 안 된다. (벌크 연산 제외)
벌크 연산은 버전을 무시한다. 벌크 연산에서 버전을 증가하려면 버전 필드를 강제로 증가시켜야 한다.
update Member m set m.age = m.age - 1, m.version = m.version + 1
# JPA 락 사용
JPA 를 사용할 때 추천하는 전략은 READ COMMITTED 트랜잭션 격리 수준 + 낙관적 버전 관리다.
락을 적용하는 방법
Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);
Board board = em.find(Board.class, id);
..
em.lock(board, LockModeType.OPTIMISTIC);
# JPA 낙관적 락
- JPA 가 제공하는 낙관적 락은 버전 (@Version) 을 사용한다. 따라서 낙관적 락을 사용하려면 버전이 있어야 한다.
- 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다는 특징이 있다.
- 낙관적 락에서 발생하는 예외
- javax.persistence.OptimisticLockException
- 락 옵션 없이 @Version 만 있어도 낙관적 락이 적용된다.
- LockModeType 옵션 지정을 통해 락을 더 세밀하게 제어할 수 있다
- LockModeType.OPTIMISTIC
- @Version 만 적용했을 때는 엔티티를 수정해야 버전을 체크한다.
- 이 옵션을 지정하면 엔티티를 조회만 해도 버전을 체크한다.
- LockModeType.OPTIMISTIC_FORCE_INCREMENT
- 낙관적 락을 사용하면서 버전 정보를 강제로 증가한다.
- 예시
- 게시물과 첨부파일은 일대다 관계이다.
- 단순히 첨부파일만 추가하면 게시물의 버전은 증가하지 않는다.
- 해당 게시물은 물리적으로는 변경되지 않았지만, 논리적으로는 변경되었다.
- 이때 게시물의 버전도 강제로 증가하려면 OPTIMISITIC_FORCE_INCREMENT 를 사용하면 된다.
# JPA 비관적 락
- JPA 가 제공하는 비관적 락은 데이터베이스 트랜잭션 락 매커니즘에 의존하는 방법이다.
- 주로 SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않는다.
- 비관적 락은 주로 PESSIMISTIC_WRITE 모드를 사용한다.
- 특징
- 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다
- 비관적 락에 의해 발생하는 예외
- javax.persistence.PessimisticLockException
- LockModeType.PESSIMISTIC_WRITE
- 비관적 락이라 하면 일반적으로 이 옵션을 뜻한다
- 데이터베이스에 쓰기 락을 걸 때 사용한다
- select for update 를 사용해서 락을 건다
# 비관적 락과 타임아웃
- 비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기한다
- 무한정 기다릴 수는 없으므로 타임아웃 시간을 줄 수 있다.
- 아래 코드는 10초간 대기해서 응답이 없으면 javax.persistence.LockTimeoutException 예외가 발생한다.
Map<String, Object> properties = new HashMap<String, Object>();
// 타임아웃까지 10초 대기 설정
properties.put("javax.persistence.lock.timeout", 10000);
Board board = em.find(Board.class, id,
LockModeType.PESSIMISTIC_WRITE, properties);
- 타임아웃은 데이터베이스 특성에 따라 동작하지 않을 수 있다.
'JPA' 카테고리의 다른 글
[Querydsl] ORDER BY RAND() 사용하기 (0) | 2024.05.05 |
---|---|
트랜잭션을 지원하는 "쓰기 지연"의 진짜 장점⭐️ (0) | 2024.04.28 |
[JPA] 배치 처리 (1) | 2024.04.28 |
읽기 전용 쿼리의 성능 최적화 - @Transactional(readonly=true), setHint(readonly) (1) | 2024.04.28 |
[JPA] N+1 문제를 피할 수 있는 다양한 방법 (1) | 2024.04.28 |