OverView
이번시간에는 트랜잭션, JPA에서 동시성을 처리하는 방법인 낙관적락과 비관적락에 대해서 알아보도록 하겠다.
트랜잭션의 네가지 요소
- 원자성: 트랜잭션 내 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야함
- 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야함 ex:) 무결성 조건
- 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리함 ex:) 동시에 같은 데이터를 수정하지 못하도록 해야 함
- 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 영구적으로 반영되어야 함
격리성과 동시성
위에서 설명한 원자성, 일관성, 지속성은 기본적으로 트랜잭션에서 보장하지만 격리성 같은 경우는 조금 다르다. 격리성은 동시성과 크게 연관이 있는데 얼마나 엄격하게 격리하냐 또는 얼마나 느슨하게 관리하냐에 따라 동시성 성능을 좌우한다.
격리 수준에 따른 문제점
- DIRTY READ: 다른 트랜잭션에 의해 수정됐지만 아직 커밋되지 않은 데이터를 읽는 것을 말한다. 변경 후 아직 커밋되지 않은 값을 읽었는데 변경을 가한 트랜잭션이 최종적으로 롤백된다면 그 값을 읽은 트랜잭션은 비일관된 상태에 놓이게 된다.
- NON-REPEATABLE READ: 한 트랜잭션 내에서 같은 쿼리를 두 번 수행했는데, 그 사이에 다른 트랜잭션이 값을 수정 또는 삭제하는 바람에 두 쿼리 결과가 다르게 나타나는 현상을 말한다.
- PHANTOM READ: 한 트랜잭션 내에서 같은 쿼리를 두 번 수행했는데, 첫 번째 쿼리에서 없던 유령(Phantom) 레코드가 두 번째 쿼리에서 나타나는 현상을 말한다.
ANSI/ISO 표준 트랜잭션 격리 수준
- READ UNCOMMITTED: 커밋하지 않은 데이터를 읽을 수 있음 예를 들어 트랜잭션이1이 데이터를 수정하고 있는데 커밋하지 않아도 트랜잭션 2가 수정중인 데이터를 조회할 수 있음(DIRTY READ) 트랜잭션 2가 DIRTY READ한 데이터를 사용하는데 트랜잭션 1을 롤백하면 데이터 정합성에 심각한 문제가 발생할 수 있음
- READ COMMITED: 커밋한 데이터만 읽을 수 있음 따라서 DIRTY READ가 발생하지는 않음 하지만 NON-REPEATABLE READ는 발생할 수 있음 예를 들어 트랜잭션 1이 회원 A를 조회중인데 갑자기 트랜잭션 2가 회원 A를 수정하고 커밋하면 트랜잭션 1이 다시 회원 A를 조회했을 때 수정된 데이터가 조회됨
- REPEATABLE READ: 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회됨 하지만 PHANTOM READ는 발생할 수 있음 예를 들어 트랜잭션 1이 10살 이하의 회원을 조회했는데 트랜잭션 2가 5살 회원을 추가하고 커밋하면 트랜잭션 1이 다시 10살 이하의 회원을 조회했을때 회원 하나가 추가된 상태로 조회됨
- SERIALIZABLE: 가장 엄격한 트랜잭션 격리 수준, 동시성 처리 성능이 급격히 떨어질 수 있음
Spring에서 @Transactional 애너테이션에 트랜잭션 격리 수준을 지정하는 방법
@Transactional(isolation = Isolation.READ_COMMITTED) ... package org.springframework.transaction.annotation; public enum Isolation { DEFAULT(-1), READ_UNCOMMITTED(1), READ_COMMITTED(2), REPEATABLE_READ(4), SERIALIZABLE(8); private final int value; private Isolation(int value) { this.value = value; } public int value() { return this.value; } }
JPA 낙관적락과 비관적락
낙관적 락
- JPA 낙관적 락은 @Version 이라는 version을 사용함
@Entity public class Member { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "member_id") private Long Id; @Version private int version; }
- 낙관적 락은 트랜잭션을 커밋하는 시점에서 충돌을 알 수 있다는 특징이 있음
-
낙관적 락 옵션에 따른 효과
- NONE: 락 옵션을 적용하지 않아도 @Version만 있으면 낙관적 락이 적용됨
- 용도: 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경되지 않아야 함 조회 시점부터 수정 시점까지를 보장함
- 동작: 엔티티를 수정할 때 버전을 체크하면서 버전을 증가함 update query, 이때 데이터베이스의 버전 값이 현재 버전이 아니면 예외 발생
- 이점: 두번의 갱실문제를 예방함
- OPTIMISTIC: 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크함 쉽게 이야기해서 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음을 보장
- 용도: 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야 함 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장
- 동작: 트랜잭션을 커밋할 때 버전 정보를 조회해서 SELECT, 현재 엔티티의 버전과 같은지 검증함 만약 같지 않으면 예외 발생, 트랜잭션을 커밋할 때 SELECT 쿼리로 조회해서 처음에 조회한 엔티티의 버전정보와 비교함, 엔티티를 수정하지 않고 단순히 조회만 해도 버전을 확인함
- 이점: OPTIMISTIC 옵션은 DIRTY READ와 NON-REPEATABLE READ를 방지함
- OPTIMISTIC_FORCE_INCREMENT: 낙관적 락을 사용하면서 버전 정보를 강제로 증가함
- 용도: 논리적인 단위의 엔티티 묶음을 관리할 수 있음. 예를 들어 게시물과 첨부파일이 일대다, 다대일의 양방향 연관관계이고 첨부파일이 연관관계의 주인임. 게시물을 수정하는데 단순히 첨부파일만 추가하면 게시물의 버전은 증가하지 않음 해당 게시물은 물리적으로는 변경되지 않았지만 논리적으로는 변경됨 이때 게시물의 버전도 강제로 증가하려면 이걸 사용하면 됨
- 동작: 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킴 이때 데이터베이스의 버전이 엔티티의 버전과 다르면 예외 발생 추가로 엔티티를 수정하면 수정시 버전 UPDATE 가 발생함 총 2번의 버전 증가가 나타날 수 있음
- 이점: 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있음
- NONE: 락 옵션을 적용하지 않아도 @Version만 있으면 낙관적 락이 적용됨
비관적 락
- JPA가 제공하는 비관적 락은 데이터베이스 트랜잭션 락 매커니즘에 의해 의존하는 방법
- SQL쿼리에 SELECT FOR UPDATE 구문을 사용하면서 시작하고 버전 정보는 사용하지 않음
- 비관적 락은 주로 PESSIMISTIC_WRITE 모드를 사용함
- 비관적 락의 특징
- 엔티티가 아닌 스칼라타입을 조회할 때도 사용 가능
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지
- 비관적 락 옵션
- PESSIMISTIC_WRITE: 비관적 락이라 하면 일반적으로 이 옵션을 뜻함 데이터베이스 쓰기에 락
- 용도: 데이터베이스에 쓰기 락을 검
- 동작: 데이터베이스 SELECT FOR UPDATE를 사용해서 락을 검
- 이점: NON-REPEATABLE READ를 방지함 락이 걸린 로우는 다른 트랜잭션이 수정 불가
- PESSIMISTIC_READ: 데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용 일반적으로는 잘 사용하지 않음 데이터베이스 대부분은 방언에의해 PESSIMISTIC_WRITE로 동작
- MYSQL: LOCK IN SHARE MODE
- POSTGRESQL: FOR SHARE
- PESSIMISTIC_FORCE_INCREMENT: 비관적 락중 유일하게 버전 정보를 사용함 비관적 락이지만 버전 정보를 강제로 증가시킴
- 오라클: FOR UPDATE NOWAIT
- POSTGRESQL: FOR UPDATE NOWAIT
- NOWAIT를 지원하지 않으면 FOR UPDATE가 사용
- PESSIMISTIC_WRITE: 비관적 락이라 하면 일반적으로 이 옵션을 뜻함 데이터베이스 쓰기에 락
마무리
JPA에서 추천하는 방식은 READ COMMITED 격리 수준 + 낙관적 락 옵션이다. 하지만 데이터의 성향에 따라 비관적 락도 고려해야 한다. 예를 들어 상품 구매 프로세스에서 1만명의 사용자가 동시에 구매요청을 했다고 가정했을때 재고가 있음에도 불구하고 격리수준 + 낙관적락 옵션에 의해 실패가 되는 경우는 없어야 하기 때문이다. 이때는 성능을 어느정도 포기하고 select for update 등의 락을 고려해야 한다.
비교적 이론적인 이야기가 많아서 다른글들에서 참조를 많이 했다. 문제가 되면 삭제조치하겠다.
포스팅은 여기까지 하겠습니다. 퍼가실때는 출처를 반드시 남겨주세요!
References
- 자바 표준 JPA 프로그래밍 (김영한)