본문 바로가기

Spring

JPA 복기하기 2 : 영속성 컨텍스트와 엔티티 생명주기

영속성 컨텍스트를 알아야 하는 이유

  • jpa의 핵심인 영속성 컨텍스트에 대한 이해가 부족하면 sql을 직접 사용해서 개발하는 것보다 못한 상황이 벌어진다!
  • 뭔지도 모르면서 냅다 기술만 갖다 쓰는 최악의 경우가 된다는 것!!!

즉, jpa의 핵심 == 영속성 컨텍스트

 

ORM

자바의 객체와 데이터베이스의 데이터를 매핑해주는 방법

 

JPA

자바에서 orm 표준으로 사용하는 인터페이스

 

Hibernate

orm 프레임워크이자, jpa 구현체, 내부적으로는 jdbc api를 사용함

 

 

JPA의 핵심요소

1. Persistance Unit : 데이터베이스 연결을 위한 설정, jdbc 드라이버, url, 사용자 이름, 비밀번호 등, 스프링에서는 properties나 yml로 설정할 수도 있음

2. Entity Manager Factory : Entity Manager를 생성

3. Persistence Context : Entity를 관리하는 공간

4. Entity Manager : Entity를 관리함

5. Entity : DB에 매핑될 객체

 

 

Flush와 Commit의 차이

용어 의미
Flush em.flush() → 변경된 엔티티들을 SQL로 변환해 DB에 전송 (but 아직 확정 X)
Commit tx.commit() → 보낸 SQL들을 실제 DB에 반영 확정
Rollback tx.rollback() → 보낸 SQL도 모두 무효 처리

 

 

Entity Manager

  • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하고 관리함
  • 엔티티 매니저를 생성하면 그 안에 영속성 컨텍스트가 있음
  • 엔티티 매니저 팩토리를 통해 요청이 올 때마다 엔티티 매니저 생성
  • 엔티티 매니저는 내부적으로 connection을 사용하여 db 접근
  • 엔티티 저장 : EntityManager.persist(entity)
    • 데이터를 영속성 컨텍스트에 저장
    • 엔티티를 영속화한다는 의미
    • DB에 엔티티를 저장한다는 의미가 아님
  • 스프링에서 엔티티 매니저는 여러개고, 영속성 컨텍스트는 1개만 존재함
    • 엔티티 매니저:영속성 컨텍스트 = N : 1

 

Entity 생명주기

엔티티는 엔티티 매니저를 통해서 영속성 컨텍스트에 보관, 관리, 제거됨

  • 비영속(new)
    • 객체 생성
    • 엔티티가 영속성 컨텍스트에 없는 상태 (= 영속성 컨텍스트와 관계 x)
Member member = new Member(); // 엔티티(객체) 생성
  • 영속(managed)
    • 엔티티가 영속성 컨텍스트에 저장
    • 영속성 컨텍스트가 엔티티를 관리
em.persist(member);
  • 준영속(detached)
    • 엔티티가 영속성 컨텍스트에 저장되었다가 분리된 상태
      • 엔티티가 영속 상태에서 준영속 상태로 간 상태
    • 영속성 컨텍스트가 엔티티 관리하지 않음
      • 영속성 컨텍스트가 제공하는 기능 사용하지 못함
    • 준영속 상태로 만드는 법
      • em.detach(entity);
        • 특정 엔티티만 준영속 상태로 변환
      • em.clear()
        • 영속성 컨텍스트 초기화(다 지움)
      • em.close()
        • 영속성 컨텍스트 종료
  • 삭제(removed)
    • 영속성 컨텍스트에 있는 엔티티를 삭제
    • 이때 DB에서도 삭제
      • 트랜잭션이 끝나거나 EntityManger를 플러시하여 삭제 쿼리가 나가야 DB에서 삭제됨

 

영속성 컨텍스트 (Persistence Context)

  • 애플리케이션과 DB사이 객체를 보관하는 가상의 DB
  • 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 보관, 관리함

 

엔티티를 영속성 컨텍스트에 보관하여 얻는 장점

  1. 1차 캐시
  2. 동일성 보장
  3. 트랜잭션을 지원하는 쓰기지연
  4. 변경감지
  5. 지연로딩

 

1차 캐시

  • 영속성 컨텍스트에 1차 캐시 존재
  • 캐시는 map 형태로 key-value 저장
    • key : DB의 PK(기본 키)
    • value : 객체

 

1차 캐시 동작 예시

  • member1이 PK가 member1인 엔티티 생성 후 영속화
  • 이 다음 PK가 member1인 엔티티 조회하면?
    • em.find(Member.class, "member1");
    • JPA는 영속성 컨텍스트에 1차 캐시 확인
    • key = member1 있으면 캐시에서 값을 가져옴

 

만약 1차 캐시에 찾는 엔티티가 없으면?

  • em.find(Member.class, "member2");
  • JPA는 영속성 컨텍스트에 1차 캐시 확인
  • 1차 캐시에 없다면 DB 조회
  • DB에 데이터 있으면 가져와 1차 캐시에 저장 후 데이터 반환

 

 

동일성 보장

같은 트랜잭션 안에서 같은 객체는 == 비교 보장

// 같은 PK 값 데이터 찾음
// 같은 데이터이지만 저장된 객체는 member1과 member2로 다름
Member member1 = em.find(Member.class, 1L);
Member member2 = em.find(Member.class, 1L);

// 이 때 == 비교를 하면?
System.out.println("result: "+(member1 == member2));

// 결과
result: true

 

쓰기 지연 (Write-behind / Write Delay)

  • 쓰기 작업을 즉시 DB에 반영하지 않고, 나중에 한 번에 처리하는 전략
  • JPA에서 영속성 컨텍스트에 저장만 해놓고, 실제 DB쿼리는 지연해서 나중에 실행함
  • 예: em.persist(entity)를 해도 바로 insert 쿼리 안 나감
  • 트랜잭션 커밋 시(tx.commit()), 플러시(em.flush()), JPQL 실행 등을 해야 쿼리가 나감
em.persist(member); // INSERT 안 나감
em.persist(team);   // INSERT 안 나감
em.flush();         // INSERT 2개 나감

 

트랜잭션을 지원하는 쓰기 지연

  • 쓰기 지연을 트랜잭션 커밋 시점에 한꺼번에 실행하는 것
  • 트랜잭션의 원자성을 보장하기 위해 DB에 쓰지 연산을 최종적으로 커밋 직전에 flush
  • 한 트랜잭션 안에서 DB에 보낼 쿼리문을 모았다가 한번에 보냄
    • 쿼리문 모아두는 곳 : 영속성 컨텍스트 안에 있는 쓰기 지연 SQL 저장소
    • 쿼리문 보내는 시점
      • 트랜잭션 끝나기 전 - transcation.commit()
      • EntityManger 플러시 - em.flush()
  • 네트워크 부하를 줄이기 위한 기능
  • em.persist(entity)를 하면?
    • 영속성 컨텍스트 1차 캐시에 저장
    • JPA가 엔티티 분석하여 INSERT 쿼리 생성하여 쓰기 지연 SQL 저장소에 쌓아둠

 

쓰기 지연 SQL 저장소에 쿼리문 쌓는 과정

  • em.persist(memberA);
    • memberA 객체 생성 후 영속화
    • 1차 캐시에 저장 및 INSERT 쿼리 쌓아둠
  • em.persist(memberB)
    • memberB 객체 생성 후 영속화

 

트랜잭션 커밋이 일어나면?

  • transcation.commit() - 트랜잭션 커밋
  • 커밋 시점에 EntityManager에서 flush() 호출
    • flush: 영속성 컨텍스트의 내용을 DB에 반영, 모아두었던 SQL이 DB에 날라감
  • 실제 데이터 저장

 

변경감지(Dirty checking)

  • 영속성 컨텍스트에서 보관하는 데이터에 변경이 일어났는지 확인
  • 데이터 변경이 일어났다면 JPA가 알아서 UPDATE 쿼리문 날려줌
  • 엔티티 삭제도 동일함

작동 순서

트랜잭션 커밋이 발생하면

  1. flush() 호출
  2. 1차 캐시에서 entity와 스냅샷 비교
    • 스냅샷 : 최초로 영속성 컨텍스트에 들어온 객체 상태
    • entity : 실제 값
  3. entity와 스냅샷이 다르다면 쓰기 지연 SQL 저장소에 update 쿼리문 추가
  4. DB에 update 쿼리문 반영 후 commit

Member member = em.find(Member.class, 1L); // name: 홍길동, age = 20
member.setAge(21); // 데이터 변경
Hibernate: 
    /* update
        jpabasic.hellojpa.Member */ 
        update
            Member 
        set
            name=? 
        where
            id=?

 

지연 로딩

  • 엔티티를 DB에서 가져올 때 연관관계를 가진 다른 객체를 가지고 있다면?
    • 즉시 로딩 : 엔티티를 조회할 때 연관된 객체도 함께 DB에서 조회
    • 지연 로딩 : 연관된 객체는 조회 x, 나중에 필요하면 그 때 DB에서 가져옴
  • 각 연관 관계마다 즉시 로딩, 지연 로딩 설정 가능

 

작동 방식 정리

어떤 자바 객체가 방금 생성됨. 비영속 상태. 영속성 컨텍스트랑 Entity 매니저랑 아무런 관련이 없다. jpa는 아무런 작업을 하지 않음.

 

어떤 엔티티를 저장하거나 조회한다. 영속 상태. jpa가 엔티티의 변경사항을 추적한다. insert나 update문을 실행함

 

어떤 엔티티를 삭제함. 삭제상태. jpa는 delete문을 실행함

 

트랜젝션을 커밋한다. (@transactional 이 붙은 함수가 종료되면 ) 지연 저장소에 있는 쿼리문을 DB로 보내고 영속성 컨텍스트가 닫히고, 영속 객체가 준영속 상태가 됨. jpa는 더 이상 변경을 추적하지 않는다. 추가적인 sql을 실행하지도 않는다.

 

요청 1개마다 → 트랜잭션 1개 → EntityManager 1개

  • 1차 캐시가 있는 EntityManager는 트랜잭션 단위로 만들고 사라진다. 즉, 1차 캐시가 살아있는 시간은 매우 짧아 성능에 큰 효과는 없다.
  • 트랜잭션마다 각자 EntityManger를 사용한다. 즉, 각자 다른 영속성 컨텍스트와 1차 캐시를 가진다.
 
POST /members ← 새로운 요청

 

동작 순서

  1. Spring이 요청을 받음
  2. @Transactional 걸린 메서드 호출됨
    트랜잭션 시작
  3. Spring이 EntityManager를 하나 생성
    → PersistenceContext도 함께 생성
  4. 서비스/레포지토리에서 작업 중인 엔티티들은 영속 상태
  5. 메서드가 끝나면
    → flush()
    → commit()
    EntityManager 닫힘
  6. → 모든 엔티티는 준영속(detached)

 

다음 요청이 오면?

  1. 새로운 트랜잭션 시작
  2. 새로운 EntityManager 생성
  3. → 새로운 영속성 컨텍스트 시작
  4. 이전 요청에서 쓰던 엔티티 객체들은 이미 준영속 상태이기 때문에
    → 다시 쓰려면 merge() 등으로 재등록해야 함

 

트랜잭션 없는 요청일 때

@Transactional 없이 find() 가능 (읽기 연산은 DB와 바로 연결됨)
@Transactional 없이 persist()  예외 발생 또는 반영 안 됨
@Transactional(readOnly = true)  조회만 가능, 쓰기 금지 (안전모드)

 

근데  Spring Data JPA를 쓸 때는 @Tranjactional을 안 붙여도 쓰기 작업이 됐음!! 뭐지?!

Spring Data JPA는 내부적으로 save(), delete() 같은 메서드는 자동으로 @Transactional이 적용된 상태로 실행됨

@Transactional
public <S extends T> S save(S entity) {
    ...
}

 

flush/commit이 나가는 시점

  • @Transactional이 끝나는 시점 → 스프링이 트랜잭션 커밋 처리
  • 커밋 직전에 EntityManager.flush()가 자동으로 호출됨
  • 쓰기 지연 SQL들 → DB로 전송됨
  • DB에 확정 반영