Reference. 자바 ORM 표준 JPA 프로그래밍
책 목차 및 이전 글
더보기
들어가기 전 JPA 특징, Q&A
1. JPA 소개
3. 영속성 관리
4. 엔티티 매핑
4.1 - 4.3 @Entity, @Table, 다양한 매핑
4.4 - 4.5 데이터베이스 스키마 자동 생성, DDL 생성 기능
5. 연관관계 매핑 기초
6. 다양한 연관관계 매핑
7. 고급매핑
8. 프록시와 연관관계 관리
9. 값 타입
9.3~5 값 타입과 불변 객체, 값 비교, 값 타입 컬렉션
10. 객체지향 쿼리 언어
11. 웹 애플리케이션 제작
11.1 프로젝트 환경설정
11.2 도메인 모델과 테이블 설계
11.3 애플리케이션 구현
12. 스프링 데이터 JPA
12.1~3 스프링 데이터 JPA 소개, 공통 인터페이스 기능
12.5~10 명세, 사용자 정의 리포지토리, Web 확장...
13. 웹 애플리케이션과 영속성 관리
14. 컬렉션과 부가기능
15. 고급 주제와 성능 최적화
16.1 트랜잭션과 락
16.1.1 트랜잭션과 격리 수준
- 트랜잭션은 ACID라 하는 원자성(
Atomicity
), 일관성(Consistency
), 격리성(Isolation
), 지속성(Durability
)을 보장- 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 or 실패
- 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지
- Ex) 데이터베이스에서 정한 무결성 제약 조건을 항상 만족
- 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리
- 격리성은 동시성과 관련된 성닝 이슈로 인해 격리 수준을 선택
- Ex) 동시에 같은 데이터를 수정하지 못해야 함
- 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록(저장) 됌
- 중간에 시스템 문제가 발생해도 DB 로그 등을 사용해 내용 복구가 가능해야 함
- 격리성과 동시성 처리 성능은 반대로 작용 (격리성 ↑, 동시성 처리 ↓)
- 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 정의
READ UNCOMMITED
(커밋되지 않은 읽기)
READ COMMITTED
(커밋된 읽기)
REPETABLE READ
(반복 가능한 읽기)
SERIALIZABLE
(직렬화 가능)
- 순서대로
READ UNCOMMITED
의 격리 수준이 낮고SERIALIZABLE
격리 수준이 높음
트랜잭션 격리 수준과 문제점
격리 수준 | DIRT READ | NON-REPETABLE READ | PHANTOM READ |
---|---|---|---|
READ UNCOMMITTED | O | O | O |
READ COMMITTED | O | O | |
REPEATABLE READ | O | ||
SERIALIZABLE |
- 격리 수준이 낮을수록 더 많은 문제가 발생
READ UNCOMMITTED
: 커밋하지 않은 데이터를 읽을 수 있음 (DIRT READ
발생)- Ex) 트랜잭션 1이 데이터 수정 중 커밋하지 않아도 트랜잭션 2가 수정중인 데이터 조회 가능
- 이러한 현상인 DIRTY READ 발생
- 트랜잭션 2가 DIRTY READ한 데이터를 트랜잭션 1이 롤백하면 데이터 정합성 문제 발생
READ COMMITTED
: 커밋한 데이터만 읽을 수 있음 (NON-REPEATABLE READ
발생)- DIRTY READ가 발생하지 않음
- Ex) 트랜잭션 1이 회원 A를 조회 중인데 갑자기 트랜잭션 2가 회원 A를 수정하고 커밋하면 트랜잭션 1이 다시 회원 A를 조회했을 때 수정된 데이터가 조회
- 이러한 현상인 NON-REPETABLE READ 발생
REPEATABLE READ
: 한번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회- 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준
- Ex) 트랜잭션 1이 10살 이하의 회원을 조회했는데 트랜잭션 2가 5살 회원을 추가하고 커밋하면 트랜잭션1이 다시 10살 이하의 회원을 조회했을 때 회원 하나가 추가된 상태로 조회
- 이러한 현상인 PHANTOM READ 발생
SERIALIZABLE
: 가장 엄격한 트랜잭션 격리 수준- PHANTOM READ 발생하지 않음
- 동시성 처리 성능이 급격히 떨어짐
<참고>
- 트랜잭션 격리 수준에 따른 동작 방식은 DB마다 다르게 처리
- 최근 DB는 동시성 처리를 위해 락보다는 MVCC 사용
16.1.2 낙관적 락과 비관적 락 기초
- JPA 영속성 컨텍스트(1차 캐시)를 적절히 활용하면
READ COMMITTED
격리 수준이어도 애플리케이션 레벨에선REPETABLE READ
가 가능
- 엔티티가 아닌 스칼라 값을 직접 조회하면 반복 가능한 읽기 불가능 (영속성 컨텍스트 관리 X)
- JPA는 DB 트랜잭션 격리 수준을
READ COMITTED
정도로 가정
- 일부 로직에 더 높은 격리 수준이 필요하면 낙관적 락과 비관적 락 중 하나를 사용
낙관적 락
- 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법
- DB가 제공하는 락 기능을 사용하는 것이 아니라 JPA가 제공하는 버전 관리 기능을 사용
- 간단히 애플리케이션이 제공하는 락 사용
- 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알수 없다는 특징
비관적 락
- 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법
- DB가 제공하는 락 기능을 사용
- 대표적으로
select for update
구문
데이터베이스 트랜잭션 범위를 넘어서는 문제
- 두 번의 갱신 분실 문제(second lost update problem)
- Ex) A와 B가 동시에 같은 게시물을 수정할 떄, A가 먼저 수정완료를 누른 후 B가 수정을 한다면 A의 수정사항으 사라지고 B가 수정한 것만 남는 현상
- 두번의 갱신 분실 문제 3가지 선택 방법
마지막 커밋만 인정
: A의 내용은 무시하고 마지막 B의 내용만 인정
최초 커밋만 인정
: A가 이미 수정을 완료했으므로 B가 수정완료 할땐 오류 발생
충돌하는 갱신 내용 병합
: 사용자 A와 사용자B의 수정사항을 병합
- 기본은 마지막 커밋만 인정하기가 사용, 하지만 상황에 따라 최초 커밋이 합리적
- JPA가 제공하는 버전관리 기능을 사용하면 최초 커밋만 인정하기를 쉽게 구현
16.1.3 @Version
- JPA가 제공하는 낙관적 락을 사용하려면
@Version
을 사용해서 버전관리 기능을 추가
@Version
적용 가능 타입- Long (long)
- Integer (int)
- Short (short)
- Timestamp
- 엔티티에 버전 관리 추가 예시
@Entity public class Board { @Id private String id; private String title; @Version private Integer version; }
- 엔티티에 버전 관리용 필드를 하나 추가하고 @Version을 붙임
- 엔티티를 수정할 때 마다 버전이 하나씩 자동으로 증가
- 엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생
- Ex) 트랜잭션 1이 조회한 엔티티를 수정하고 있는데 트랜잭션 2에서 같은 엔티티를 수정하고 커밋해서 버전이 증가해버리면 트랜잭션 1이 커밋할 때 버전 정보가 다르므로 예외 발생
- 버전 관리 사용 예제
//트랜잭션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
- 버전 정보를 사용하면 최초 커밋만 인정하기가 적용
버전 정보 비교 방법
- 엔티티를 수정하고 트랜잭션을 커밋하면 영속성 컨텍스트를 플러시하면서
UPDATE
쿼리를 실행
- 버전을 사용하는 엔티티면 검색 조건에 엔티티 버전 정보를 추가
- 버전 사용 엔티티 SQL 예시
UPDATE BOARD SET TITLE=? VERSION=? (버전 + 1 증가) WHERE ID=? AND VERSION=? (버전비교)
- DB버전과 엔티티 버전이 같으면 데이터를 수정하면서 동시에 버전도 하나 증가
- DB 버전이 이미 증가해 엔티티 버전과 다르면
VERSION
값이 다르므로 수정 대상이 없음- 버전이 이미 증가한 것으로 판단해서 JPA가 예외 발생
- 버전은 엔티티의 값을 변경하면 증가
- 임베디드 타입과 타입 컬렉션은 논리적으로 해당 엔티티의 값이므로 버전이 증가
- 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때 버전 증가
@Version
으로 추가한 버전 관리 필드는 JPA가 관리하므로 임의 수정X (벌크 연산 제외)- 벌크 연산은 버전을 무시, 벌크 연산에서 버전을 증가하려면 버전 필드 강제 증가
update Member m set m.name = '변경' m.version = m.version + 1
- 벌크 연산은 버전을 무시, 벌크 연산에서 버전을 증가하려면 버전 필드 강제 증가
16.1.4 JPA 락 사용
- 추천하는 전략은
READ COMMITTED
+낙관적 버전관리
(두 번의 갱신내역 분실 문제 예방)
- 락을 적용할 수 있는 위치
EntityManager.lock()
,EntityManger.find()
,EntityManger.refresh()
Query.setLockMode()
(TypeQuery
포함)
@NamedQuery
- 조회하면서 즉시 락을 거는 예시
Board board = em.find(Board.class, id, LockModeType.OPTIMSTIC);
- 필요할때 락을 거는 예시
Board board = em.find(Board.class, id); ... em.lock(board, LockModeType.OPTIMISTIC);
LockMode Type 속성
락 모드 | 타입 | 설명 |
---|---|---|
낙관적 락 | OPTIMISTIC | 낙관적 락을 사용 |
낙관적락 | OPTIMISTIC_FORCE_INCREMENT | 낙관적 락 + 버전정보를 강제 증가 |
비관적 락 | PESSIMISTIC_READ | 비관적 락, 읽기 락을 사용 |
비관적 락 | PESSIMISTIC_WRTIE | 비관적 락, 쓰기 락을 사용 |
비관적 락 | PESSMISTIC_FORCE_INCREMENT | 비관적 락 + 버전정보를 강제 증가 |
기타 | NONE | 락을 걸지 않음 |
기타 | READ | JPA1.0 호환 기능 OPTIMISTC과 동일한 기능 |
기타 | WRITE | JPA1.0. 호환 기능 OPTIMISTIC_FORCE_INCREMENT와 동일 |
16.1.5 JPA 낙관적 락
- JPA가 제공하는 낙관적 락은 버전을 사용, 트랜잭션을 커밋하는 시점에 충돌을 알 수 있는 특징
- 낙관적 락에서 발생하는 예외들
javax.persistence.OptimisticLockException
(JPA 예외)
org.hibernate.StaleObjectStateException
(하이버네이트 예외)
org.springframework.orm.ObjectOptimisticLockingFailureException
(스프링 예외 추상화)
- 일부 JPA 구현체는 @Version 컬럼 없이 낙관적 락을 허용하지만 추천X
- 락 옵션 없이 @Version만 있어도 낙관적 락이 적용
NONE
- 락 옵션을 적용하지 않아도 엔티티에 @Version이 적용된 필드가 있으면 낙관적 락이 적용
- 용도: 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경되지 않음, 조회 시점부터 수정 시점까지를 보장
- 동작: 엔티티를 수정할 때 버전을 체크하면서 버전을 증가 (UPDATE 쿼리 사용) 이때 DB의 버전 값이 현재 버전이 아니면 예외가 발생
- 이점: 두 번의 갱신 분실 문제를 예방
OPTIMISTIC
@Version
만 적용하면 엔티티 수정 시 버전을 체크하지만 이 옵션을 추가하면 조회만 해도 버전을 체크
- 즉, 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에 변경하지 않음을 보장
- 용도: 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장
- 동작: 트랜잭션을 커밋할 때 버전 정보를 조회해서 현재 엔티티의 버전과 같은지 검증
- 이점:
DIRTY READ
와NON-REPEATABLE READ
를 방지
- OPTIMISTIC 예제
//트랜잭션 1 조회 title="제목A", version=1 Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC); //중간에 트랜잭션 2에서 해당 게시물을 수정해서 title="제목C", version2로 증가 //트랜잭션 1 커밋 시점에서 버전 정보 검증, 예외 발생 //DB(version=2, 엔티티 version=1) tx.commit()
- 트랜잭션 1은 엔티티를 OPTIMISTIC 락으로 조회했으므로 버전 정보를 SELECT 쿼리로 조회해서 처음에 조회한 엔티티의 버전 정보와 비교
- OPTIMISTIC 옵션은 엔티티를 수정하지 않고 단순히 조회만해도 버전을 확인
OPTIMISTIC_FORCE_INCREMENT
- 낙관적 락을 사용하면서 버전 정보를 강제로 증가
- 용도: 논리적인 단위의 엔티티 묶음을 관리
- Ex) 게시물과 첨부파일이 일대다, 다대일의 양방향 연관관계일때, 단순 첨부파일만 추가하면 게시물 버전은 증가하지 않음. 게시물은 물리적으로는 변경되지 않았지만 논리적으론 변경, 이때 게시물의 버전도 강제로 증가하려면
OPTIMISTIC_FORCE_INCREMENT
를 사용
- Ex) 게시물과 첨부파일이 일대다, 다대일의 양방향 연관관계일때, 단순 첨부파일만 추가하면 게시물 버전은 증가하지 않음. 게시물은 물리적으로는 변경되지 않았지만 논리적으론 변경, 이때 게시물의 버전도 강제로 증가하려면
- 동작: 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때
UPDATE
쿼리를 사용해 버전정보 증가 이때 DB의 버전이 엔티티의 버전과 다르면 예외 발생, 추가로 엔티티를 수정하면 수정시 UPDATE가 발생하며 따라서 총 2번의 버전 증가가 나타날 수 있음
- 이점: 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리가 가능
- 용도: 논리적인 단위의 엔티티 묶음을 관리
OPTIMISTIC_FORCE_INCREMENT
예제//트랜잭션 1 조회 title="제목A", version=1 Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC_FORCE_INCREMENT); //트랜잭션 1 커밋 시점에 버전 강제 증가 tx.commit();
16.1.6 JPA 비관적 락
- 비관적 락은 DB 트랜잭션 락 메커니즘에 의존하는 방법
- 주로 SQL 쿼리에
select for update
구문을 사용하면서 시작, 버전 정보는 사용 안함
- 비관적 락은 주로
PESSIMISTIC_WRITE
모드를 사용
- 비관적 락의 특징들
- 엔티티가 아닌 스칼라 타입을 조회할 때도 사용 가능
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지
- 비관적 락의 발생하는 예외들
javax.persistence.PessimisticLockException
(JPA 예외)
org.springframework.dao.PessimisticLockingFailureException
(스프링 예외 추상화)
PESSIMISTIC_WRITE
- 비관적 락은 일반적으로 이 옵션을 의미, 데이터베이스에 쓰기 락을 걸때 사용
- 용도: 데이터베이스에 쓰기 락을 설정
- 동작: 데이터베이스 select for update를 사용해서 락을 설정
- 이점:
NON-REPEATABLE READ
를 방지, 락이 걸린 로우는 다른 트랜잭션이 수정 못함
PESSIMISTIC_READ
- 데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸때 사용
- 일반적으로는 잘 사용 안함, DB 대부븐은 방언에 의해
PESSIMISTIC_WRITE
로 동작MySQL
: lock in share mode
PostgreSQL
: for shar
PESSIMISTIC_FORCE_INCREMENT
- 비관적 락중 유일하게 버전 정보를 사용, 버전 정보를 강제로 증가
- 하이버네이트는 nowait를 지원하는 DB에 대해서
for update nowait
옵션을 적용오라클
: for update nowait
PostgreSQL
: for update nowait
- nowiat를 지원하지 않으면 for update를 사용
16.1.7 비관적 락과 타임아웃
- 비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기
- 무한정 기다릴 수는 없으므로 타임아웃 시간 설정이 가능
- 타임아웃 예제 (10초간 대기해서 응답이 없으면
LockTimeoutException
예외 발생)Map<String, Object> properties = new HashMap<String, Object>(); //타임아웃 10초까지 대기 설정 properties.put("javax.persistence.lock.timeout", 10000); Board board = em.find(Board.class, "boardId", LockModeTpye.PESSMISTIC_WRITE, properties);
- 타임아웃은 DB 특성에 따라 동작 하지 않을 수 있음
'개발서적 > 자바 ORM 표준 JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 16.2 2차 캐시 (1) | 2021.10.25 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 15.4 성능 최적화 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 15.3 프록시 심화 주제 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 15.2 엔티티 비교 (0) | 2021.10.25 |
[자바 ORM 표준 JPA 프로그래밍] 15.1 예외 처리 (0) | 2021.10.25 |