JPA, Transaction 개념 정리

김형준·2024년 6월 25일

JPA 개념 정리

  1. 개념:
    JPA (Java Persistence API)는 자바 애플리케이션에서 객체 관계 매핑(ORM)을 제공하는 표준 사양이다.
    JPA는 자바 객체를 관계형 데이터베이스 테이블에 매핑하여 데이터베이스와의 상호작용을 단순화하고, 개발자가 데이터베이스 관련 코드를 작성하지 않고도 데이터베이스와 상호작용할 수 있게 해준다.
    JPA는 데이터베이스 독립적인 방식으로 동작하며, 이를 통해 애플리케이션의 이식성을 높입니다

  2. 작동원리:
    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 사이 중간 계층의 개념이다.
    엔티티의 영속화 관리 즉 엔티티를 저장하고 관리하는 저장소 개념이다.

  3. PK 생성 전략 (IDENTITY, SEQUENCE, TABLE) -> IDENTITY는 em.persist 단계에서 insert query 가 수행되어 쓰기 지연의 효과를 얻지 못함. 따라서 성능 상 오버헤드가 발생할 수 있음을 인지해야함. 데이터베이스 호환성: MySQL이나 SQL Server와 같이 IDENTITY 컬럼을 기본으로 사용하는 데이터베이스에서는 IDENTITY 전략이 자연스러운 선택입니다.
그러나 그 외에 update 등 변경 감지에 따른 sql 수행은 똑같이 쓰기 지연 저장소에 저장됨.

  4. jpql

    1. query dsl:
      1. 컴파일 시점에서 문법 오류 발견
      2. 동적 쿼리 작성 편리
      3. 자바 코드 자체가 쿼리스럽고 직관적인 형태임
      4. 유지보수에 용이함
  5. fetch join

    1. 일반 Join의 경우, Join을 수행할 때 연관 관계를 포함하지 않는다.
    2. Fetch Join의 경우, Join을 수행할 때 연관 관계를 포함한다. 
    3. 일반 Join의 경우, SELECT 절에 지정된 Entity만을 조회한다.
    4. Fetch Join의 경우, 연관된 Entity를 함께 조회한다. 
    5. Fetch Join은 즉시 로딩으로 동작한다. 여기에도 차이점이 있다.
    6. 일반적인 즉시 로딩은 연관된 Entity 조회 시, 추가적인 쿼리문이 발행된다. (N + 1 문제 발생) 
    7. Fetch Join의 경우, 하나의 쿼리문을 사용하여 연관된 Entity를 함께 조회한다. (N + 1 문제 없음)
  6. @Query 를 통한 JPQL 사용 시 주의사항

    1. 보통 복잡한 쿼리를 사용할 때 사용하지만, insert, update, delete와 같이 DB에 변경 사항이 생기는 쿼리의 경우 @Modifying을 붙여줘야 한다.
이는 JPA에게 DB 변경 사항이 있음을 명시해줌으로써 JPA가 관리하는 영속성 컨텍스트와 DB 간 싱크가 맞지 않는 상황을 방지하기 위함이다.
    2. 단, @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
      • pahntom 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를 활용하여 고유의 동시성 제어 메커니즘을 구현하고 있습니다.
profile
BackEnd Developer

0개의 댓글