일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 성능
- nginx설치
- APNS
- php
- 자바스크립트
- 웹사이트성능
- nginx
- 카프카 트랜잭션
- 푸시
- 웹사이트 성능
- nginx설정
- 도메인 주도 개발
- graphql
- gcm 푸시 번역
- 웹사이트최적화기법
- GCM 번역
- notification
- 페이스북 번역
- 푸시 번역
- GCM
- Design Pattern
- ddd
- kafka
- Push
- Java
- 디자인패턴
- 카프카
- git
- JPA
- Today
- Total
간단한 개발관련 내용
16장 트랜잭션과 락, 2차 캐시 본문
16.1 트랜잭션과 락
16.1.1 트랜잭션과 격리 수준
- 트랜잭션은 ACID를 보장해야한다.
- 원자성 (작업에 대한 것) - Atomicity
- 트랜잭션 내의 작업은 모두 성공하거나 실패해야한다
- 일관성 (데이터에 대한 것) - Consistency
- 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다
- 격리성 (트랜잭션에 대한 것) - Isolation
- 트랜잭션의 분리 및 보장
- 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 한다.
- 지속성 (데이터에 대한 것)- Durability
- 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야한다
- 원자성 (작업에 대한 것) - Atomicity
- 트랜잭션은 원자성, 일관성, 지속성을 보장해야 하는데 그러면 트랜잭션을 정말 순차적으로 처리야하며 이렇게 되면 성능이 낮아질 수 있다. 그래서 격리수준을 정하여 서비스에 맞게 설정해서 사용한다.
- READ-UNCOMMITED(커밋되지 않은 읽기)
- DIRTY-READ : 아직 커밋되지 않은 T1의 데이터를 T2가 읽고 수정해 버린다.
- READ-COMMITED(커밋된 읽기)
- NON-REPEATABLE-READ: 반복가능한 읽기가 안됌, 아직 커밋되지 않은 T1의 데이터를 T2가 읽고 수정해 버리면 T1은 수정된 데이터를 읽을 수는 있지만 데이터는 변경되었다.
- 한 트랜잭션 안에서 반복해서 같은 데이터를 읽을 수 없다
- REPEATABLE-READ(반복가능한 읽기)
- 한 번 조회한 데이터를 반복해서 읽어도 같은 데이터가 조회된다. (수정은 안되게 함)
- 하지만 PHANTOM-READ 가 발생한다
- 추가되는 데이터는 허용
- SERIALIZABLE(직렬화 가능)
- 가장 엄격하고 PHANTOM-READ 가 발생하지 않지만 처리수준이 떨어진다
- READ-UNCOMMITED(커밋되지 않은 읽기)
16.1.2 낙관적 락과 비관적 락 기초
<aside> 💡
JPA의 영속성 컨텍스트(1차캐시)를 적절히 활용하면 데이터베이스 트랜잭션이 READ-COMMITED 격리수준이어도 애플리케이션 레벨에서 REPEATABLE-READ가 가능하다. 물론 엔터티가 아닌 스칼라 값을 직접 조회하면 영속성 컨텍스트의 관리를 받지 못하므로 반복가능한 읽기를 할 수 없다.
</aside>
(그럼 이럴 경우가 많으니, REPEATABLE-READ로 써야하지 않나? 라는 생각이 든다)
- 여하튼 READ-COMMITED 치고, 둘 다 쓰기 작업의 충돌을 방지하기 위해서 필요하다
- 낙관적 락
- 트랜잭션은 대부분 충돌이 발생하지 않는다고 낙관적으로 가정
- JPA가 제공하는 버전관리 기능을 사용, 애플리케이션이 제공하는 락
- 비관적 락
- 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 법
- 데이터베이스가 제공하는 락, select … for update 구문
<aside> 💡
비관적 락을 사용하는 JPA나 QueryDSL 코드에서 LockModeType.PESSIMISTIC_WRITE를 적용하면, **내부적으로는 SELECT FOR UPDATE**와 같은 SQL 구문을 데이터베이스에 전달합니다. 직접적으로 SQL에서 SELECT FOR UPDATE가 보이지 않더라도, SQL 로그에서 이 구문을 확인할 수 있습니다.
- JPA에서 비관적 락을 설정할 때 LockModeType.PESSIMISTIC_WRITE를 사용하면, **자동으로 SELECT FOR UPDATE*가 포함된 쿼리가 실행됩니다.
- SQL 로그를 확인하면 실제로 데이터베이스에서 실행되는 쿼리에 **FOR UPDATE*가 포함된 것을 볼 수 있습니다. </aside>
- 두 번의 갱신 분실 문제(Second lost updates problem)
- 마지막 커밋만 인정하기(기본)
- 최초 커밋만 인정하기
- 충돌하는 갱신 내용 병합하기
16.1.3 @Version
- @Version 적용 가능 타입
- Long
- Integer
- Short
- Timestamp
- 엔터티에 버전관리용 필드를 추가
- 버전은 엔터티 값을 변경하면 증가한다
- 버전관리 필드는 JPA가 직접 관리하므로 개발자가 임의로 수정하면 안 된다.
16.1.4 JPA 락 사용
<aside> 💡
JPA를 사용할때 추천하는 전략은 READ-COMMITTED 트랜잭션 격리수준 + 낙관적 버전 관리다
두 번의 갱신 내역 분실 문제 예방
</aside>
16.1.5 JPA 낙관적 락
- Version 을 사용
- NONE
- 두 번의 갱신 분실 문제를 예방
- OPTIMISTIC
- DIRTY-READ와 NON-REPEATABLE-READ 를 방지
- 이래서 READ-COMMITED 만 해도 된다고 한건가
- DIRTY-READ와 NON-REPEATABLE-READ 를 방지
- OPTIMISTIC_FORCE_INCREMENT
- 버전 정보 강제 증가
- Aggregate Root에 사용할 수 있다
16.1.6 JPA 비관적 락
- 데이터베이스 트랜잭션 락 메커니즘에 따름
- PESSIMISTIC_WRITE 모드를 사용하면 쿼리에 SELECT … FOR UPDATE 가 붙는다
- 엔터티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다
- 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다
- Modes
- PESSIMISTIC_WRITE
- PESSIMISTIC_READ
- PESSIMISTIC_FORCE_INCREMENT
- 유일하게 버전정보 사용
16.1.7 비관적 락과 타임아웃
- 비관적 락을 사용하면 획득할 때까지 트랜잭션이 대기한다. 무한정 기다릴 수는 없으므로 타임아웃 시간을 줄 수 있다.
16.2 2차 캐시
- JPA가 제공하는 애플리케이션 범위의 캐시
16.2.1 1차 캐시와 2차 캐시
- 1차 캐시 : 영속성 컨텍스트의 캐시
- 1차 캐시는 같은 엔터티가 있으면 해당 엔터티를 그대로 반환한다. 따라서 1차 캐시는 객체 동일성(==)을 보장한다.
- 1차 캐시는 기본적으로 영속성 컨텍스트 범위의 캐시다.
- 컨테이너 환경에서는 트랜잭션 범위의 캐시, OSIV를 적용하면 요청 범위의 캐시다
- 2차 캐시
- 하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 공유 캐시 또는 2차캐시라 한다. Shared-Cache
- 애플리케이션 범위의 캐시이고 애플리케이션이 종료할 때까지 유지된다
- 특징
- 영속성 유닛 범위의 캐시다
- 조회한 객체를 그대로 반환하는게 아니라 복사본을 만들어서 반환한다
- 데이터베이스 기본 키를 기준으로 캐시하지만 영속성컨텍스트가 다르면 객체 동일성(==)을 보장하지 않는다
16.2.2 JPA 2차 캐시 기능
- 캐시 모드 설정
- @Cacheable
- hibernate의 shared-cache-mode 설정
16.2.3 하이버네이트와 EHCACHE 적용
- 엔터티 캐시
- 컬렉션 캐시
- 쿼리 캐시
16.3 정리
- 트랜잭션 격리 수준은 4단계가 있다. 격리 수준이 낮을 수록 동시성은 증가하지만 격리 수준에 따른 다양한 문제가 발생한다.
- 영속성 컨텍스트는 데이터베이스 트랜잭션이 READ-COMMITED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기 REPEATABLE-READ를 제공한다
- JPA는 낙관적 락과 비관적 락을 지원한다. 낙관적 락은 애플리케이션이 지원하는 락이고 비관적 락은 데이터베이스 트랜잭션 락 매커니즘에 의존한다.
- 2차 캐시를 사용하면 애플리케이션의 조회 성능을 극적으로 끌어올릴 수 있다
JPA 2차 캐시 스프링부트 적용
Spring Boot에서 Hibernate의 Shared Cache Mode 설정을 통해 2차 캐시(Second-Level Cache)의 동작 방식을 조정할 수 있습니다. Hibernate는 다양한 캐시 모드를 제공하며, 이를 설정하여 엔티티나 컬렉션을 어떻게 캐싱할지 결정할 수 있습니다.
Shared Cache Mode란?
Shared Cache Mode는 Hibernate가 2차 캐시에서 엔티티, 컬렉션, 쿼리 결과 등을 캐싱할 때 사용할 전략을 결정합니다. 기본적으로 제공되는 캐시 모드는 다음과 같습니다:
- ALL: 모든 엔티티 및 컬렉션이 캐싱됩니다.
- NONE: 캐시가 전혀 사용되지 않습니다.
- ENABLE_SELECTIVE: 캐싱이 명시적으로 허용된 엔티티나 컬렉션만 캐싱됩니다.
- DISABLE_SELECTIVE: 캐싱이 명시적으로 금지되지 않은 엔티티나 컬렉션은 모두 캐싱됩니다.
- UNSPECIFIED: Hibernate가 기본 설정을 따릅니다.
Spring Boot에서 Shared Cache Mode 설정
Spring Boot에서는 application.properties 또는 application.yml 파일을 통해 Hibernate의 Shared Cache Mode를 설정할 수 있습니다.
application.properties 예시
# Hibernate 2차 캐시 설정
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.use_query_cache=true
# Shared Cache Mode 설정
spring.jpa.properties.hibernate.cache.shared_cache_mode=ALL
application.yml 예시
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
use_query_cache: true
shared_cache_mode: ALL
위 설정에서:
- hibernate.cache.use_second_level_cache: Hibernate 2차 캐시를 활성화하는 설정입니다. true로 설정하면 Hibernate가 2차 캐시를 사용하게 됩니다.
- hibernate.cache.use_query_cache: 쿼리 결과 캐시를 활성화하는 설정입니다. 특정 쿼리 결과를 캐싱하여 성능을 향상시킬 수 있습니다.
- hibernate.cache.shared_cache_mode: Shared Cache Mode를 설정합니다. 위 예시에서는 모든 엔티티 및 컬렉션을 캐시하는 ALL 모드로 설정되었습니다.
Shared Cache Mode 옵션
- ALL: 모든 엔티티와 컬렉션이 캐싱됩니다.
- NONE: 캐시가 전혀 사용되지 않습니다.
- ENABLE_SELECTIVE: 캐싱이 명시적으로 설정된 엔티티만 캐싱됩니다.
- DISABLE_SELECTIVE: 캐싱이 명시적으로 금지된 엔티티를 제외한 모든 엔티티가 캐싱됩니다.
- UNSPECIFIED: 캐시 설정이 지정되지 않고 기본 설정을 따릅니다.
예시: 특정 엔티티에 캐시 적용하기
Shared Cache Mode를 ENABLE_SELECTIVE로 설정하고, 특정 엔티티에 2차 캐시를 적용하려면 다음과 같이 엔티티에 @Cacheable을 명시적으로 설정합니다.
엔티티에 캐시 적용
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
@Id
private Long id;
private String name;
private BigDecimal price;
// Getters and setters
}
- @Cacheable: 이 엔티티가 캐시될 수 있음을 명시합니다.
- @Cache: Hibernate의 캐시 설정을 정의합니다. CacheConcurrencyStrategy는 캐시의 동시성 전략을 지정하는데, READ_WRITE, NONSTRICT_READ_WRITE, READ_ONLY, TRANSACTIONAL 등의 옵션이 있습니다.
주의사항
- 캐시 프로바이더: Spring Boot에서 Hibernate 2차 캐시를 활성화하려면 캐시 프로바이더가 필요합니다. 예를 들어, Ehcache, Hazelcast, Infinispan 등의 라이브러리를 사용하여 캐시를 구성할 수 있습니다. 캐시 프로바이더를 설정하지 않으면 2차 캐시 기능이 제대로 동작하지 않습니다.
- 캐시 설정의 성능 고려: 모든 엔티티나 쿼리를 캐싱하면 성능이 향상될 수 있지만, 캐시 크기가 커질수록 메모리 소비가 증가할 수 있습니다. 따라서 필요한 엔티티나 쿼리에만 캐시를 설정하는 것이 중요합니다.
Ehcache를 이용한 예시 설정
만약 Ehcache를 사용하여 2차 캐시를 설정하려면, 다음과 같이 캐시 프로바이더를 설정하고 ehcache.xml을 추가해야 합니다.
application.properties 예시
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
spring.cache.ehcache.config=classpath:ehcache.xml
ehcache.xml 예시
<ehcache>
<cache name="com.example.Product"
maxEntriesLocalHeap="1000"
timeToLiveSeconds="3600" />
</ehcache>
위 예시에서는 Product 엔티티를 캐시하도록 설정한 예시입니다.
결론
- Spring Boot에서 Shared Cache Mode는 application.properties 또는 application.yml을 통해 설정할 수 있으며, Hibernate의 hibernate.cache.shared_cache_mode를 사용해 조정됩니다.
- 각 엔티티에 @Cacheable 및 @Cache를 사용하여 캐싱 설정을 제어할 수 있습니다.
- Ehcache 등의 캐시 프로바이더를 함께 사용하여 2차 캐시 기능을 구현할 수 있으며, 성능 향상을 위해 필요한 엔티티에만 캐시를 적용하는 것이 중요합니다.