| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 29 | 30 | 31 |
- 백엔드개발자
- 부트캠프
- Java
- JPA
- 오픈패스
- 국비지원
- 디자인챌린지
- baekjoon
- 패스트캠퍼스
- KDT
- UXUI기초정복
- 국비지원교육
- Spring
- 백엔드 부트캠프
- OPENPATH
- UXUI챌린지
- mysql
- 자바
- 내일배움카드
- 국비지원취업
- 디자인교육
- 환급챌린지
- UXUIPrimary
- 객체지향
- 백준
- 티스토리챌린지
- 오픈챌린지
- 오블완
- 디자인강의
- Be
- Today
- Total
군만두의 IT 개발 일지
[스터디10] 15. 트랜잭션과 락, 2차 캐시 본문
목차
16장. 트랜잭션과 락, 2차 캐시
16.1 트랜잭션과 락
16.1.1 트랜잭션과 격리 수준
트랜잭션은 ACID라 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 한다.
- 원자성(Atomicity): 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하든가 모두 실패해야 한다.
- 일관성(Consistency): 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
- 격리성(Isolation): 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.
- 지속성(Durability): 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.
문제는 격리성인데 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 차례대로 실행해야 한다. 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
트랜잭션 격리 수준
- READ UNCOMMITTED(커밋되지 않은 읽기): 커밋하지 않은 데이터를 읽을 수 있다. 예를 들어 트랜잭션1이 데이터를 수정하고 있는데 커밋하지 않아도 트랜잭션2가 수정 중인 데이터를 조회할 수 있다. 이것을 DIRTY READ라 한다.
- READ COMMITTED(커밋된 읽기): 커밋한 데이터만 읽을 수 있다. 따라서 DIRTY READ가 발생하지 않는다. 하지만 NON-REPEATABLE READ는 발생할 수 있다.
- REPEATABLE READ(반복 가능한 읽기): 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. 하지만 PHANTOM READ는 발생할 수 있다.
- SERIALIZABLE(직렬화 가능): 가장 엄격한 트랜잭션 격리 수준이다. 여기서는 PHANTOM READ가 발생하지 않는다. 하지만 동시성 처리 성능이 급격히 떨어질 수 있다.
격리 수준에 따른 문제점
- DIRTY READ: 커밋하지 않은 데이터를 읽을 수 있다.
- NON-REPEATABLE READ: 반복해서 같은 데이터를 읽을 수 없다.
- PHANTOM READ: 반복 조회 시 결과 집합이 달라진다.
16.1.2 낙관적 락과 비관적 락 기초
JPA는 데이터베이스 트랜잭션 격리 수준을 READ COMMITTED 정도로 가정한다. 만약 일부 로직에 더 높은 격리 수준이 필요하면 낙관적 락과 비관적 락 중 하나를 사용하면 된다.
낙관적 락(Optimistic Lock)
- 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법
- 데이터베이스가 제공하는 락 기능을 사용하는 것이 아니라 JPA가 제공하는 버전 관리 기능을 사용한다. (애플리케이션이 제공하는 락)
- 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.
비관적 락(Pessimistic Lock)
- 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법
- 데이터베이스가 제공하는 락 기능을 사용한다.
- 대표적으로 select for update 구문이 있다.
두 번의 갱신 분실 문제(Second Lost Updates Problem)
- 사용자 A와 사용자 B가 동시에 같은 데이터를 수정하면 마지막에 수정한 내용만 남고 처음 수정한 내용은 사라지는 문제다.
- 3가지 해결 방법:
- 마지막 커밋만 인정하기: 사용자 A의 내용은 무시하고 마지막에 커밋한 사용자 B의 내용만 인정한다.
- 최초 커밋만 인정하기: 사용자 A가 이미 수정을 완료했으므로 사용자 B가 수정을 완료할 때 오류가 발생한다.
- 충돌하는 갱신 내용 병합하기: 사용자 A와 사용자 B의 수정사항을 병합한다.
16.1.3 @Version
JPA가 제공하는 낙관적 락을 사용하려면 @Version 어노테이션을 사용해서 버전 관리 기능을 추가해야 한다.
@Entity
public class Board {
@Id
private String id;
private String title;
@Version
private Integer version;
}
- @Version 적용 가능 타입: Long(long), Integer(int), Short(short), Timestamp
- 엔티티를 수정할 때마다 버전이 하나씩 자동으로 증가한다.
- 엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생한다.
- 버전 정보를 사용하면 최초 커밋만 인정하기가 적용된다.
16.1.4 JPA 락 사용
JPA를 사용할 때 추천하는 전략은 READ COMMITTED 트랜잭션 격리 수준 + 낙관적 버전 관리다.
락을 적용할 수 있는 위치
- EntityManager.lock(), EntityManager.find(), EntityManager.refresh()
- Query.setLockMode() (TypeQuery 포함)
- @NamedQuery
LockModeType 종류
- 낙관적 락
- OPTIMISTIC: 낙관적 락을 사용한다.
- OPTIMISTIC_FORCE_INCREMENT: 낙관적 락 + 버전정보를 강제로 증가한다.
- 비관적 락
- PESSIMISTIC_READ: 비관적 락, 읽기 락을 사용한다.
- PESSIMISTIC_WRITE: 비관적 락, 쓰기 락을 사용한다.
- PESSIMISTIC_FORCE_INCREMENT: 비관적 락 + 버전정보를 강제로 증가한다.
- 기타
- NONE: 락을 걸지 않는다.
- READ: JPA1.0 호환 기능이다. OPTIMISTIC과 같다.
- WRITE: JPA1.0 호환 기능이다. OPTIMISTIC_FORCE_INCREMENT와 같다.
16.1.5 JPA 낙관적 락
낙관적 락은 버전을 사용한다. 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다는 특징이 있다.
NONE
- 락 옵션을 적용하지 않아도 엔티티에 @Version이 적용된 필드만 있으면 낙관적 락이 적용된다.
- 용도: 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경(삭제)되지 않아야 한다.
- 동작: 엔티티를 수정할 때 버전을 체크하면서 버전을 증가한다.
- 이점: 두 번의 갱신 분실 문제를 예방한다.
OPTIMISTIC
- @Version만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크한다.
- 용도: 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장한다.
- 동작: 트랜잭션을 커밋할 때 버전 정보를 조회해서 현재 엔티티의 버전과 같은지 검증한다.
- 이점: DIRTY READ와 NON-REPEATABLE READ를 방지한다.
OPTIMISTIC_FORCE_INCREMENT
- 용도: 논리적인 단위의 엔티티 묶음을 관리할 수 있다.
- 동작: 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킨다.
- 이점: 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있다.
16.1.6 JPA 비관적 락
JPA가 제공하는 비관적 락은 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방법이다.
- 주로 SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않는다.
- 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.
PESSIMISTIC_WRITE
- 용도: 데이터베이스에 쓰기 락을 건다.
- 동작: 데이터베이스 select for update를 사용해서 락을 건다.
- 이점: NON-REPEATABLE READ를 방지한다. 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다.
PESSIMISTIC_READ
- 데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용한다.
- 데이터베이스 대부분은 PESSIMISTIC_WRITE로 동작한다.
PESSIMISTIC_FORCE_INCREMENT
- 비관적 락중 유일하게 버전 정보를 사용한다.
- 비관적 락이지만 버전 정보를 강제로 증가시킨다.
16.1.7 비관적 락과 타임아웃
비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기한다. 무한정 기다릴 수는 없으므로 타임아웃 시간을 줄 수 있다.
Map<String, Object> properties = new HashMap<String, Object>();
// 타임아웃 10초까지 대기 설정
properties.put("javax.persistence.lock.timeout", 10000);
Board board = em.find(Board.class, "boardId", LockModeType.PESSIMISTIC_WRITE, properties);
16.2 2차 캐시
16.2.1 1차 캐시와 2차 캐시
1차 캐시
- 영속성 컨텍스트 내부에 있다. 엔티티 매니저로 조회하거나 변경하는 모든 엔티티는 1차 캐시에 저장된다.
- 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스에 동기화한다.
- 1차 캐시는 끄고 켤 수 있는 옵션이 아니다. 영속성 컨텍스트 자체가 사실상 1차 캐시다.
- 1차 캐시는 같은 엔티티가 있으면 해당 엔티티를 그대로 반환한다. 따라서 1차 캐시는 객체 동일성(a==b)을 보장한다.
2차 캐시(공유 캐시)
- 하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 공유 캐시 또는 2차 캐시라 한다.
- 2차 캐시는 애플리케이션 범위의 캐시다. 따라서 애플리케이션을 종료할 때까지 캐시가 유지된다.
- 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있다.
- 2차 캐시를 적절히 활용하면 데이터베이스 조회 횟수를 획기적으로 줄일 수 있다.
- 2차 캐시는 동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고 복사본을 만들어서 반환한다.
- 2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성(a==b)을 보장하지 않는다.
16.2.2 JPA 2차 캐시 기능
2차 캐시를 사용하려면 엔티티에 @Cacheable 어노테이션을 사용하면 된다.
@Cacheable
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
...
}
persistence.xml에 shared-cache-mode를 설정해서 애플리케이션 전체에 캐시를 어떻게 적용할지 옵션을 설정해야 한다.
- ALL: 모든 엔티티를 캐시한다.
- NONE: 캐시를 사용하지 않는다.
- ENABLE_SELECTIVE: Cacheable(true)로 설정된 엔티티만 캐시한다.
- DISABLE_SELECTIVE: 모든 엔티티를 캐시하는데 Cacheable(false)로 명시된 엔티티는 캐시하지 않는다.
- UNSPECIFIED: JPA 구현체가 정의한 설정을 따른다.
16.2.3 하이버네이트와 EHCACHE 적용
하이버네이트가 지원하는 캐시는 크게 3가지가 있다.
- 엔티티 캐시: 엔티티 단위로 캐시한다. 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용한다.
- 컬렉션 캐시: 엔티티와 연관된 컬렉션을 캐시한다. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시한다.
- 쿼리 캐시: 쿼리와 파라미터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시한다.
하이버네이트 전용인 @Cache 어노테이션을 사용하면 세밀한 설정이 가능하다.
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
...
}
CacheConcurrencyStrategy 속성
- NONE: 캐시를 설정하지 않는다.
- READ_ONLY: 읽기 전용으로 설정한다. 등록, 삭제는 가능하지만 수정은 불가능하다.
- NONSTRICT_READ_WRITE: 엄격하지 않은 읽고 쓰기 전략이다. 동시에 같은 엔티티를 수정하면 데이터 일관성이 깨질 수 있다.
- READ_WRITE: 읽기 쓰기가 가능하고 READ COMMITTED 정도의 격리 수준을 보장한다.
- TRANSACTIONAL: 컨테이너 관리 환경에서 사용할 수 있다. 설정에 따라 REPEATABLE READ 정도의 격리 수준을 보장받을 수 있다.
✔️ 복습하기
- 트랜잭션의 4가지 특성은?
- 낙관적 락과 비관적 락의 차이점은?
- 1차 캐시와 2차 캐시의 차이점은?

이 글은 『 자바 ORM 표준 JPA 프로그래밍』 책을 학습한 내용을 정리한 것입니다.
'학습일지 > Java' 카테고리의 다른 글
| [스터디10] 14. 고급 주제와 성능 최적화 (0) | 2025.11.28 |
|---|---|
| [스터디10] 13. 컬렉션과 부가 기능 (0) | 2025.11.01 |
| [스터디10] 12. 웹 애플리케이션과 영속성 관리 (0) | 2025.10.22 |
| [스터디10] 10. 객체지향 쿼리 언어 (0) | 2025.09.04 |
| [스터디10] 09. 값 타입 (0) | 2025.08.21 |