JPA 개념 정리
-
개념:
JPA (Java Persistence API)는 자바 애플리케이션에서 객체 관계 매핑(ORM)을 제공하는 표준 사양이다.
JPA는 자바 객체를 관계형 데이터베이스 테이블에 매핑하여 데이터베이스와의 상호작용을 단순화하고, 개발자가 데이터베이스 관련 코드를 작성하지 않고도 데이터베이스와 상호작용할 수 있게 해준다.
JPA는 데이터베이스 독립적인 방식으로 동작하며, 이를 통해 애플리케이션의 이식성을 높입니다
-
작동원리:
EntityManagerFactory(DB당 1개 생성) -> EntityManager(Transaction 과 생명 주기 동일) ->
엔티티 매니저를 통해 객체지향 코드를 DB에 CRUD 하도록 함,
엔티티 객체를 영속성 컨텍스트에 저장하여 관리하는데, 이 때 1차 캐시에 저장됨.
같은 트랜잭션 안에서 같은 데이터는 동일성이 보장되어 조회됨
단 트랜잭션 종료시 삭제됨.
em.getTransaction().begin() -> em.persist() 단계에서 영속화 진행됨(영속성 컨텍스트 1차캐시에 저장됨).
단 쓰기 지연 저장소에 저장되며 실제 반영은 commit 시 일괄 처리됨 (flush).
이를 통해 DB와의 네트워킹 횟수를 감소시키며 부하를 줄일 수 있음
영속성 컨텍스트의 1차 캐시를 사용하여 변경 감지 (dirty check)를 하고 commit 시점에 flush()가 호출 될 경우 1차 캐시에 저장된 스냅샷과 비교하여 데이터의 변경을 감지한다.
만약 변경이 감지되었다면 update 쿼리가 생성되며, 쓰기지연저장소에 저장한다.
영속성 컨텍스트: 논리적인 개념으로, 엔티티 매니저와 DB 사이 중간 계층의 개념이다.
엔티티의 영속화 관리 즉 엔티티를 저장하고 관리하는 저장소 개념이다.
-
PK 생성 전략 (IDENTITY, SEQUENCE, TABLE) -> IDENTITY는 em.persist 단계에서 insert query 가 수행되어 쓰기 지연의 효과를 얻지 못함. 따라서 성능 상 오버헤드가 발생할 수 있음을 인지해야함. 데이터베이스 호환성: MySQL이나 SQL Server와 같이 IDENTITY 컬럼을 기본으로 사용하는 데이터베이스에서는 IDENTITY 전략이 자연스러운 선택입니다.
그러나 그 외에 update 등 변경 감지에 따른 sql 수행은 똑같이 쓰기 지연 저장소에 저장됨.
-
jpql
- query dsl:
- 컴파일 시점에서 문법 오류 발견
- 동적 쿼리 작성 편리
- 자바 코드 자체가 쿼리스럽고 직관적인 형태임
- 유지보수에 용이함
-
fetch join
- 일반 Join의 경우, Join을 수행할 때 연관 관계를 포함하지 않는다.
- Fetch Join의 경우, Join을 수행할 때 연관 관계를 포함한다.
- 일반 Join의 경우, SELECT 절에 지정된 Entity만을 조회한다.
- Fetch Join의 경우, 연관된 Entity를 함께 조회한다.
- Fetch Join은 즉시 로딩으로 동작한다. 여기에도 차이점이 있다.
- 일반적인 즉시 로딩은 연관된 Entity 조회 시, 추가적인 쿼리문이 발행된다. (N + 1 문제 발생)
- Fetch Join의 경우, 하나의 쿼리문을 사용하여 연관된 Entity를 함께 조회한다. (N + 1 문제 없음)
-
@Query 를 통한 JPQL 사용 시 주의사항
- 보통 복잡한 쿼리를 사용할 때 사용하지만, insert, update, delete와 같이 DB에 변경 사항이 생기는 쿼리의 경우 @Modifying을 붙여줘야 한다.
이는 JPA에게 DB 변경 사항이 있음을 명시해줌으로써 JPA가 관리하는 영속성 컨텍스트와 DB 간 싱크가 맞지 않는 상황을 방지하기 위함이다.
- 단, @Modifying을 붙인다면 JPA의 장점 중 하나인 쓰기 지연 저장소, 즉 DB 네트워킹 횟수를 줄일 수 있다는 장점을 포기하게 되므로, 지양하는 것이 좋다고 생각한다.
Transaction 개념 정리
- 단일한 논리적인 작업 단위
- 논리적인 이유로 여러 SQL 문들을 단일 작업으로 묶어서 나눠질 수 없게 만든 것이 transaction 이다.
- 속성
- Atomicity
- Consistency
- 제약 사항을 어기지 않고, 일관성을 유지해야한다.
- Isolation
- 한 트랜잭션은 다른 트랜잭션에 영향을 끼치면 안된다.
- 여러 트랜잭션들이 동시에 실행될 때에도 혼자 실행되는 것처럼 동작하게 만든다.
- concurrency control의 주된 목표가 isolation이다.
- Durability
- 커밋된 트랜잭션은 DB에 영구적으로 저장한다.
-
@Transactional 은 AOP를 통해 트랜잭션 처리 과정을 처리해주도록 한다.
- @Transactional이 붙은 빈을 찾아 해당 빈을 감싸는 프록시 객체가 생성 및 등록되고
- 해당 빈의 메서드가 호출 될 경우 요청을 가로채서 트랜잭션을 관리한다.
- 프록시 객체는 트랜잭션 인터셉터(TransactionInterceptor)를 사용하여 메서드 호출을 가로챈다.
- 트랜잭션 인터셉터는 TransactionInterceptor 클래스에 의해 구현되며, 이 클래스는 트랜잭션을 시작하고 종료하는 로직을 포함한다.
- 트랜잭션 인터셉터는 PlatformTransactionManager 인터페이스를 사용하여 트랜잭션을 시작한다. 이 인터페이스의 구현체가 JpaTransactionManager이다.
- JpaTransactionManager는 EntityManagerFactory를 사용하여 EntityManager를 생성하고, 이를 통해 트랜잭션을 시작한다.
-
JPA 사용 시 spring-boot-starter-data-jpa 를 통해 JPA와 관련된 모든 빈을 자동 설정한다.
- Spring Boot 는 JpaBaseConfiguration 추상 클래스를 통해 JPA 관련 설정을 자동으로 구성한다. 이 클래스는 JPA 관련 여러 빈을 설정하는데, 그 중 하나가 JpaTransactionManager 이다.
- Spring Boot는 자동 설정 과정에서 EntityManagerFactory 빈을 생성한다. 이 빈은 JPA의 EntityManager를 생성하는데 사용된다. EntityManagerFactory 빈은 LocalContainerEntityManagerFactoryBean을 통해 생성된다.
- Spring Boot는 JpaTransactionManager 빈을 자동으로 생성한다. 이 트랜잭션 매니저는 EntityManagerFactory 빈을 사용하여 트랜잭션을 관리한다.
-
Isolation level
- Read uncommitted
- Dirty read, Non-repeatable read, pahntom read 허용
- Read committed
- Non-repeatable read, pahntom read 허용
- Repeatable read
- Serializable
-
위 논문 내용 반박
- rollback 시 정상적인 recovery는 매우 중요하므로 모든 isolation level에서 dirty write을 허용하면 안된다.
- lost update 또한 가능함
- dirty read의 개념이 조금 더 확장적으로 쓰여야함 (abort가 발생하지 않아도 발생 가능함)
- read skew (inconsistent read)
- write skew (inconsistent write)
- phantom read도 확장되어야 한다.
- SNAPSHOT ISOLATION (type of MVCC)
-
2PL protocol (two-phase locking)
serializability를 보장하기 위해 사용되는 프로토콜
tx에서 모든 locking operation이 최초의 unlock operation 보다 먼저 수행되도록 하는 것
- Expanding phase (growing phase): lock을 취득하기만 하고 반환하지는 않는 phase
- Shrinking phase (contracting phase): lock을 반환만 하고 취득하지 않는 phase
- Dead lock 발생 가능
- recoverability를 보장하기 위해 strict 2pl, strong strict 2pl (commit 후 write unlock, read/write unlock) 을 많이 사용함 -> 단 read 간 락이 아닌 경우 즉 write 락이 있는 경우 처리량이 낮다는 단점
-
MVCC (Multiversion Concurrency Control)
- write-write lock 외에 락 조합을 가능하게 해주는 경우
- MVCC는 커밋된 데이터만 읽는다.
- write 시 해당 트랜잭션만 알고있는 공간에 저장함 (커밋 전)
- 그리고 커밋 시 unlock이 진행되는데, 이는 recoverability를 보장하기 위함
- 데이터를 읽을 때 특정 시점 기준으로 가장 최근에 커밋된 데이터를 읽는다. (mysql- consistent read)
- 데이터 변화 이력을 관리한다. (추가적으로 저장 공간을 더 사용함)
- read, write는 서로를 블락하지 않는다. (성능 면에서 동시에 처리할 수 있는 트랜잭션 수 많아짐)
- isolation level repeatable read 로 설정할 경우 postgreSQL 에서는 같은 데이터에 먼저 업데이트한 트랜잭션이 커밋되면 나중 트랜잭션은 롤백된다.
단 이 개념은 postgreSQL에만 있는 개념임
- 트랜잭션 마다 다른 isolation level을 줄 수 있음.
- MySQL은 위와 같은 개념이 없다. 즉 repeatable read 여도 여전히 lost update가 발생한다.
- MySQL은 repeatable read로 설정한 후 Locking read를 적용해줘야한다. select문에 FOR UPDATE를 붙여줌으로써 리드락을 명시해줘야한다.
- MySQL은 locking read 시 가장 최근에 커밋된 데이터를 읽는다.
- FOR UPDATE(write lock, exclusive lock 획득), FOR SHARE(read lock, shared lock 획득)
- MVCC 에서도 write skew 현상은 발생함
- MySQL, repeatable read 의 경우 locking read를 사용하면 방지 가능 -> 최근 커밋된 데이터 읽기 때문에 (정상)
- PostgreSQL은 repeatable read 레벨을 사용할 경우 먼저 업데이트한 트랜잭션이 존재하여 롤백하게됨 (정상)
- Serializable level
- MySQL 의 경우 트랜잭션의 모든 평범한 SELECT 문은 암묵적으로 FOR SHARE 처럼 동작한다. 따라서 MySQL은 Serializable 레벨에서는 MVCC 보다는 락으로 동작한다고 말한다.
- PostgreSQL의 경우 SSI (Serializable snapshot isolation)으로 구현 -> first-committer-winner
+) MVCC
MVCC (Multiversion Concurrency Control) 개념 정리
Multiversion Concurrency Control (MVCC)는 데이터베이스 시스템에서 일관성과 성능을 높이기 위한 동시성 제어 메커니즘입니다. MVCC는 여러 트랜잭션이 동시에 실행될 때 발생하는 충돌을 최소화하며, 각 트랜잭션이 데이터베이스의 스냅샷을 유지하도록 합니다. 주요 개념은 다음과 같습니다:
1. 커밋된 데이터만 읽기: 트랜잭션은 커밋된 데이터만 읽습니다. 이는 일관성을 보장하며, 데이터 읽기 작업이 다른 트랜잭션의 진행에 방해되지 않도록 합니다.
2. 트랜잭션 공간에 데이터 저장: 트랜잭션이 데이터를 수정할 때, 그 변경 사항은 해당 트랜잭션이 알고 있는 공간에 저장됩니다. 커밋 전까지는 다른 트랜잭션에서 이 데이터를 볼 수 없습니다.
3. 커밋과 언락: 트랜잭션이 커밋되면 변경된 데이터가 공개되고, 이를 통해 다른 트랜잭션이 변경된 데이터를 접근할 수 있게 됩니다. 이는 recoverability를 보장하기 위한 과정입니다.
4. 일관된 읽기: 데이터베이스는 특정 시점 기준으로 가장 최근에 커밋된 데이터를 읽습니다. 이는 MySQL의 경우 consistent read라 불립니다.
5. 데이터 변화 이력 관리: MVCC는 데이터의 변화 이력을 관리하여 추가적인 저장 공간이 필요합니다. 이는 트랜잭션이 데이터의 이전 상태를 유지하도록 합니다.
6. 블로킹 없는 읽기와 쓰기: 읽기와 쓰기 작업은 서로를 블락하지 않습니다. 이는 성능 향상에 기여하며, 동시에 처리할 수 있는 트랜잭션 수가 많아집니다.
데이터베이스별 MVCC 동작
PostgreSQL:
- Repeatable Read: 동일 데이터에 먼저 업데이트한 트랜잭션이 커밋되면 나중 트랜잭션은 롤백됩니다. 이는 PostgreSQL만의 개념입니다.
- Serializable Level: SSI (Serializable Snapshot Isolation)로 구현되어 있으며, first-committer-winner 방식을 채택합니다. 먼저 커밋한 트랜잭션이 승리합니다.
MySQL:
- Repeatable Read: 트랜잭션마다 다른 isolation level을 지정할 수 있지만, MySQL의 경우 여전히 lost update가 발생할 수 있습니다.
- Locking Read: repeatable read 상태에서도 locking read를 적용해야 합니다. SELECT ... FOR UPDATE (write lock, exclusive lock) 또는 SELECT ... FOR SHARE (read lock, shared lock)을 사용합니다.
- Serializable Level: 모든 SELECT 문이 암묵적으로 FOR SHARE처럼 동작하며, 이로 인해 Serializable 레벨에서는 MVCC보다는 락을 사용하여 동작한다고 할 수 있습니다.
MVCC와 Write Skew 현상
- Write Skew: MVCC 환경에서도 발생할 수 있습니다.
MySQL: repeatable read에서 locking read를 사용하면 방지할 수 있습니다. 최근 커밋된 데이터를 읽기 때문에 정상이 유지됩니다.
PostgreSQL: repeatable read 레벨에서 먼저 업데이트한 트랜잭션이 존재하면 나중 트랜잭션이 롤백됩니다.
이 개념들은 데이터베이스에서 트랜잭션의 동시성을 관리하고, 데이터 무결성을 유지하는 데 중요한 역할을 합니다. 각각의 데이터베이스 시스템은 MVCC를 활용하여 고유의 동시성 제어 메커니즘을 구현하고 있습니다.