개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › JPA › 영속성 관리

영속성 컨텍스트

JPA는 SQL을 언제 보내나

JPA는 엔티티를 영속성 컨텍스트라는 메모리 공간에 보관하고, SQL을 코드가 실행되는 순간이 아니라 필요한 시점에 모아서 보냅니다.

실행으로 대조 · Hibernate 7.4.5와 H2에서 여섯 장면의 SQL을 대조

이 실습에 나오는 개념

영속성 컨텍스트
엔티티를 보관하고 관리하는 JPA의 논리적 공간이다. 조회하거나 저장한 엔티티는 이곳에 보관되며, JPA는 이곳의 엔티티에 일어난 변화를 추적해 트랜잭션이 끝날 때 DB에 반영한다. 보통 트랜잭션 하나와 생명주기를 같이한다(OSIV가 켜진 웹 요청에서는 요청이 끝날 때까지 유지된다). 1차 캐시, 변경 감지, 쓰기 지연 같은 기능이 이 위에서 동작한다.
1차 캐시
영속성 컨텍스트 안에 엔티티를 식별자 기준으로 보관하는 저장소다. 같은 트랜잭션 안에서 같은 식별자로 다시 조회하면 DB에 쿼리를 보내지 않고 보관된 엔티티를 돌려준다. 그 결과 같은 트랜잭션 안에서 같은 식별자의 엔티티는 항상 같은 객체다. 기본적으로 트랜잭션이 끝나면 사라지므로(OSIV가 켜진 웹 요청에서는 요청이 끝날 때까지 유지된다) 애플리케이션 전체의 캐시와는 다르다.
변경 감지(Dirty Checking)
영속성 컨텍스트가 관리하는 엔티티의 값이 바뀌면, 플러시할 때(보통 커밋 직전) JPA가 처음 읽었던 상태와 비교해 값이 달라진 엔티티에 대한 UPDATE 문을 자동으로 실행하는 기능이다. Hibernate는 기본적으로 바뀐 컬럼만이 아니라 전체 컬럼을 UPDATE 문에 넣으며, 바뀐 컬럼만 넣으려면 @DynamicUpdate를 쓴다. 따라서 조회한 엔티티의 값을 바꾸기만 하면 별도의 저장 메서드를 호출하지 않아도 DB에 반영된다.
쓰기 지연과 flush
쓰기 지연은 INSERT, UPDATE, DELETE 문을 바로 DB에 보내지 않고 모아 두었다가 한꺼번에 보내는 것이다. 단, 기본 키를 IDENTITY 전략(MySQL의 AUTO_INCREMENT 등)으로 생성하는 엔티티는 식별자를 얻기 위해 저장을 요청한 시점에 INSERT가 바로 실행된다. flush는 모아 둔 변경 내용을 DB에 보내는 동작으로, 트랜잭션 커밋 직전이나 JPQL 쿼리 실행 직전에 자동으로 일어난다. flush는 SQL을 DB에 전송할 뿐 트랜잭션을 커밋하는 것은 아니다.
엔티티
데이터베이스 테이블의 행에 대응하는 자바 객체다. @Entity를 붙인 클래스로 정의하며 식별자(@Id)를 가진다. JPA는 엔티티의 상태 변화를 추적해 그에 맞는 SQL을 실행한다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 1번째 줄: SELECT로 회원 1을 읽어 영속성 컨텍스트에 보관합니다. 읽은 시점의 값 "김철수"를 스냅샷으로 남깁니다.
  2. 2번째 줄: 자바 객체의 이름만 바꿨습니다. 이 순간 DB에는 아무 SQL도 가지 않습니다. 영속성 컨텍스트 안에서 현재 값(김영수)과 스냅샷(김철수)이 달라졌습니다.
  3. 3번째 줄: save를 부르지 않고 메서드가 끝납니다.
  4. 메서드 종료, 커밋: 커밋 직전에 flush가 일어납니다. 관리 중인 엔티티의 현재 값을 스냅샷과 비교해 달라진 회원 1을 찾아내고, UPDATE를 만들어 DB로 보낸 뒤 커밋합니다. 이것이 변경 감지입니다.

실행 결과

코드

@Transactional
public void run() {
    Member m = memberRepository.findById(1L).orElseThrow();
    m.setName("김영수");
    // save()를 부르지 않고 메서드가 끝난다
}   // 메서드가 끝나면 커밋

이 모형의 전제

코드가 하나의 영속성 컨텍스트 안에서 실행된다고 보았습니다. 시퀀스 전략은 persist마다 시퀀스를 한 번 조회하는 것으로 단순화했습니다. 실제 기본 설정(allocationSize 50)에서는 id를 묶음으로 받아 두기 때문에 시퀀스 조회가 훨씬 적게 나갑니다. "@Transactional이 없으면" 예에서는 스프링 데이터 리포지토리의 조회 메서드가 자체 트랜잭션으로 실행되고 끝난다고 가정했습니다. 스프링 부트의 open-in-view 기본 설정에서는 엔티티가 관리 상태로 남기도 하지만, 커밋 시점이 없으므로 UPDATE가 나가지 않는 결과는 같습니다. 여섯 장면 모두를 실제 Hibernate 7.4.5와 H2 데이터베이스에서 실행해, 나간 SQL의 수와 쓰기 SQL이 나간 시점, DB에 남은 값이 화면과 일치하는 것을 확인했습니다(시퀀스 전략은 화면의 단순화에 맞춰 allocationSize를 1로 두고 실행).

앞 실습: JPA가 푸는 문제 · 다음 실습: 엔티티의 생명주기

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요