영속성 컨텍스트

1. 영속성 컨텍스트 정의

영속성 컨텍스트란, 엔티티(Entity)를 저장하는 가상의 메모리 공간(1차 캐시)입니다.

Java Persistence API(JPA)에서 엔티티 객체를 영구 저장소(데이터베이스)에 저장하거나 조회할 때, 직접 DB와 바로 통신하는 것이 아니라, 중간에 있는 영속성 컨텍스트를 통해 처리합니다.

 

2. 영속성 컨텍스트의 역할

역할 설명
1차 캐시 엔티티를 메모리에 저장해두고 동일한 엔티티 재조회 시 캐시에서 반환
동일성 보장 같은 트랜잭션 내에서는 같은 엔티티 인스턴스를 보장 (== 비교 가능)
변경 감지 (Dirty Checking) 엔티티 값이 변경되면 트랜잭션 종료 시 자동으로 UPDATE 쿼리 생성
지연 로딩 (Lazy Loading) 연관 엔티티는 실제 사용할 때까지 SQL을 실행하지 않음
쓰기 지연 (Write-behind) INSERT/UPDATE/DELETE 쿼리를 모아서 트랜잭션 커밋 시점에 실행

 

영속성 컨텍스트는 위와 같은 효과를 보여줍니다. 일단은 아 그렇구나하고 넘어가는게 좋습니다.

 

더 읽다보시면 이해가 가실겁니다.

3. 엔티티 생명주기와 영속성 컨텍스트

4. 직접 코드로 알아보자

Entity 코드

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    // 생성자, getter/setter 생략
}

영속 상태

public void 영속상태_예시(EntityManager em) {
    // 1. 비영속 상태: 객체는 생성되었지만 JPA와는 아무 관련 없음
    Member member = new Member();
    member.setName("홍길동");

    // 2. 영속 상태: persist() 호출 후 영속성 컨텍스트에 저장됨
    em.persist(member);

    // 여기까지는 DB에 INSERT 쿼리가 실행되지 않음 (쓰기 지연)
    // 트랜잭션 커밋 시점에 DB에 반영됨

    // 3. 1차 캐시에 존재하므로 같은 ID로 다시 조회하면 DB를 조회하지 않음
    Member findMember = em.find(Member.class, member.getId());
    System.out.println(member == findMember); // true (같은 인스턴스)
}

영속 상태는 JPA의 관리 하에 들어간 상태를 뜻합니다. 특히 em.persist( )를 사용하면 Entity Manager를 활용해서 1차 캐시에 저장할 수 있습니다.

 

하지만 member 객체를 추가하기 위해 바로 DB에 직접 접근하지않습니다. 그 이유는 persist 코드를 사용하자마자 쿼리를 날려 DB에 접근한다면 최적화할 여지가 없어지기 때문에 메서드가 종료되면 쿼리를 날리게 됩니다.

 

쉽게 생각해보자면 비즈니스 로직의 성능 최적화 부분을 생각해보면 됩니다.

 

DB에 접근하기 위해 쿼리를 날리는 행위는 성능에 많은 영향을 미칩니다. 그렇다면 성능을 최적화하려고 한다면 어떻게 해야할까요?

커넥션 풀, 쿼리 발생으로 인한 지연 문제를 최소화를 목표로 비즈니스 로직을 운영할 수 있어야합니다.

 

위의 코드를 본다면 쿼리를 바로 날리지않고 영속성 컨텍스트에서 관리하는 쓰기-지연을 사용해서 쿼리의 발생을 최적화하고 있습니다.

 

SELECT 문을 통해서 DB에 접근하는 것이 아니라 영속성 컨텍스트를 활용해서 값을 찾을 수 있다는 것은 쿼리 발생을 최소화하는 것을 뜻합니다.

    // 3. 1차 캐시에 존재하므로 같은 ID로 다시 조회하면 DB를 조회하지 않음
    Member findMember = em.find(Member.class, member.getId());
    System.out.println(member == findMember); // true (같은 인스턴스)
}

그렇기에 1차 캐시의 기능을 할 수 있는 영속성 컨텍스트는 쿼리 발생을 최소화 해줄 수 있습니다.

 

아래 코드는 일반적인 비즈니스 로직입니다.

public void 영속상태_예시(EntityManager em) {
    EntityTransaction tx = em.getTransaction(); // 트랜잭션 시작
    tx.begin();
    
    // 1. 비영속 상태: 객체는 생성되었지만 JPA와는 아무 관련 없음
    Member member = new Member();
    member.setName("홍길동");
    
    // 2. 영속 상태: persist() 호출 후 영속성 컨텍스트에 저장됨
    em.persist(member)	
    
    // 3. 중간에 필드 값 변경(Dirty Check에 따른 변경된 객체의 값이 영속성 컨텍스트에 저장됩니다.)
    member.setName("이순신");
    
		// 4. 커밋에 따른 쓰기 지연된 쿼리문 발생
		tx.commit();
}

 

다시 한번 코드를 봅시다. em.persist(member)를 활용해서 영속성 컨텍스트에 name이 “홍길동”인 member 객체를 저장합니다.

그리고 아래의 쿼리문을 쓰기-지연을 발생시켜 커밋 명령어 이후에 쿼리를 발생시킵니다.

INSERT INTO member (name) VALUES ('홍길동')

 

그러나 영속성 컨텍스트에 저장된 이후로 name이 “홍길동”인 member객체의 name이 “이순신”으로 바뀌었습니다.

이때는 JPA의 더티 체킹(Dirty Checking)으로 변경된 name을 감지합니다. 따라서 영속성 컨텍스트의 member의 객체의 name도 새로운 값인 “이순신”으로 캐싱됩니다.

 

또한 커밋 이후에 발생하는 쿼리문 집합에는 기존의 member의 name을 변경하는 쿼리문이 추가됩니다.

UPDATE member SET name = '이순신' WHERE id = ?

 

그렇다면 아래 코드는 어떻까요?

public void 영속상태_예시(EntityManager em) {
    EntityTransaction tx = em.getTransaction(); // 트랜잭션 시작
    tx.begin();
    
    // 1. 비영속 상태: 객체는 생성되었지만 JPA와는 아무 관련 없음
    Member member = new Member();
    member.setName("홍길동");
    
    // 2. 중간에 필드 값 변경
    member.setName("이순신");
    
		// 3. 영속 상태: persist() 호출 후 영속성 컨텍스트에 저장됨
    em.persist(member)
    
	  tx.commit();
}

값 변경 이전에 em.persist( )가 없다면 결과적으로 발생하는건 member가 영속성 컨텍스트에 등록되고 나서의 값 뿐입니다.

 

INSERT INTO member (name) VALUES ("이순신")

그렇기에 모든 비즈니스 로직에 불필요한 쿼리를 날리는 것은 성능을 최적화할 수 있는 부분이 없겠죠.

 

그래서 영속성 컨텍스트 관리는 값의 변경이 모두 종료되면 호출해주는 것이 올바릅니다.

근데 우리가 스프링에서 사용하는 코드는 사뭇 다릅니다.

우리는 스프링 부트 프로젝트에서는 비즈니스 로직에서 respository.save( )를 사용하곤합니다.

 

이제부터 em.persist와 repository.save()의 차이를 알아보겠습니다.

 

em.persist() vs repository.save() 차이

항목 em.persist(entity) repository.save(entity)
제공 주체 JPA (EntityManager) Spring Data JPA
기본 동작 항상 새 엔티티 등록 (INSERT만) 새 엔티티면 persist, 기존 엔티티면 merge(UPDATE)
ID 기준 동작 분기 무조건 persist → ID 중복 시 예외 발생 ID가 null이면 persist, 존재하면 merge
merge 포함 여부 직접 호출해야 함 (em.merge()) 내부적으로 merge 포함
쿼리 발생 시점 트랜잭션 커밋 시 (쓰기 지연) 동일
대표 사용 상황 JPA 순수 사용 시 Spring Data JPA에서 기본 사용
필요한 객체 EntityManager JpaRepository 구현체

 

em.persist( )는 무조건 엔티티를 등록하는데 사용됩니다.

 

그러나 repository.save( )는 새 엔티티를 em.persist()로 추가할 뿐만 아니라, 이미 존재하는 엔티티라면 UPDATE를 발생시킵니다.

예시 코드

// Spring Data JPA 환경 예시
@Transactional
public void 영속상태_예시() {
    // 1. 비영속 상태: 객체는 생성되었지만 JPA와는 아무 관련 없음
    Member member = new Member();
    member.setName("홍길동");
    
    // 2. 중간에 필드 값 변경
    member.setName("이순신");
    
    // 3. 영속 상태: persist() 호출 후 영속성 컨텍스트에 저장됨
    memberRepository.save(member)
}

 

그림으로 이해해보자

위 그림은 실제로 em.persist(member)가 발생하면 작동하는 구조를 보여줍니다.

영속성 컨텍스트에 1차 캐싱을 진행한 뒤 쓰기 지연 저장소에 커밋 이후에 동작할 쿼리문을 적재합니다.

이후 Dirty Checking에 의해 JPA의 감시 하에 객체의 변경을 감지하고 이에 맞는 쿼리문을 추가 적재합니다.

5. 준영속 상태 (Detached)

준영속 상태(Detached)란,

엔티티가 한때는 영속성 컨텍스트에 의해 관리되었지만,

지금은 영속성 컨텍스트에서 분리되어 더 이상 관리되지 않는 상태를 의미합니다.

쉽게 말해, JPA가 더 이상 감시하지 않는 객체입니다.

따라서 영속성 컨텍스트에서 벗어나야 하는 객체를 뜻합니다.

영속 → 준영속 상태 변화 예시

 

상태 변화  설명
em.persist(entity) 비영속 → 영속
em.detach(entity) 영속 → 준영속
em.clear() 모든 영속 상태 → 준영속
em.close() 모든 영속 상태 → 준영속

 

예시 코드

Member member = em.find(Member.class, 1L); // 영속 상태

em.detach(member); // 이제 JPA는 더 이상 감시하지 않음 (준영속 상태)

member.setName("이순신"); // 변경해도 반영되지 않음

em.flush(); // 아무 쿼리도 나가지 않음

 

준영속 상태의 특징

  • 변경 감지(DIRTY CHECKING)가 작동하지 않음
  • 트랜잭션 커밋 시 아무 영향 없음
  • SELECT/UPDATE/DELETE 쿼리 발생하지 않음
  • DB 반영하려면 merge()로 다시 영속 상태로 만들어야 함

 

다시 영속 상태로 되돌리기

Member detached = ... // 준영속 상태 객체
Member merged = em.merge(detached); // 새 영속 상태 객체 반환

주의: merge()는 기존 영속 상태와는 다른 새로운 객체를 반환하므로,

이후에는 merged 객체를 써야 해요. detached는 여전히 관리되지 않습니다.

 

준영속 상태는 언제 쓰이나?

  • 영속성 컨텍스트 초기화할 때 (em.clear())
  • 비즈니스 로직에서 JPA 관리를 잠시 끊고 싶을 때
  • 성능 최적화 또는 사용자 입력 값으로부터 생성된 객체를 검증 후 반영할 때

 

6. 주요 API 동작과의 관계

메서드 동작
persist(entity) 엔티티를 영속 상태로 등록 (1차 캐시에 저장, DB에는 아직 INSERT되지 않음)
find(entityClass, id) 1차 캐시 → DB 순서로 조회
remove(entity) 엔티티를 삭제 예약 (트랜잭션 커밋 시 DELETE 실행)
detach(entity) 해당 엔티티만 준영속 상태로 전환
clear() 영속성 컨텍스트 초기화 (모든 엔티티를 준영속 상태로 만듦)
flush() 영속성 컨텍스트의 변경 내용을 DB에 즉시 반영

 

개발자 성현