공부해봅시당

[JPA] 영속성 컨텍스트 본문

STUDY/Spring

[JPA] 영속성 컨텍스트

tngus 2024. 5. 8. 16:38

JPA 영속성 컨텍스트(Persistence Context)

JPA 영속성 컨텍스트란 엔터티를 영구 저장하는 환경이라는 뜻으로, 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다.

EntityManager에 엔티티를 저장하거나 조회하면 EntityManager는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

 

Entity를 영속성 컨텍스트에 저장하는 코드로, 해당 코드는 DB에 저장이 안 된 상태이다. (트랜젝션이 끝나야 DB에 반영함)

entityManager.persist(entity);

 

특징

- 영속성 컨텍스트는 논리적인 개념

- 영속성 컨텍스트는 엔터티 매니저를 생성할 때 하나 만들어짐, 엔터티 매니저를 통해 영속성 컨텍스트에 접근하고 관리할 수 있음

- 스프링에서 EntityManager를 주입받아서 쓰면, 같은 트랜잭션 범위에 있는 EntityManager는 동일 영속성 컨텍스트에 접근함

 

영속성 컨텍스트를 사용하는 이유

- 1차 캐시(조회 성능 높여줌)

- 동일성 보장

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

 

1) 1차 캐시

영속성 컨텍스트 내부에는 캐시가 있는데 이를 1차 캐시라고 부름.

캐시는 Map의 형태로 만들어지며 key는 id값, value는 해당 entity 값이 들어 있음

Brower brower = em.find(Brower.class, "Google");

- 1차 캐시에서 엔터티를 첫 번째로 찾음. 해당 엔터티가 있으면 바로 반환해줌

- 1차 캐시에 없을 경우 데이터베이스에서 조회. 조회한 데이터로 엔터디를 생성해 1차 캐시에 저장. 이후 엔터티를 반환

 

2) 동일성 보장

영속성 컨텍스트는 엔터티의 동일성을 보장함

 

- 동일성 비교: 실제 인스턴스가 같고, ==을 사용해 비교

- 동등성 비교: 실제 인스턴스는 다를 수 있지만 인스턴스가 가지고 있는 값이 같음. equals() 메서드로 비교.

 

동일성 보장 vs 싱글톤 관리
JPA의 영속성 컨텍스트가 엔터티의 동일성을 보장한다는 것은, 특성 영속성 컨텍스트 내에서 동일한 데이터베이스 행을 나타내는 엔터티에 대해 항상 같은 자바 인스턴스를 반환한다는 의미. 

영속성 컨텍스트가 엔터티의 동일성을 보장하는 방식은 다음과 같음
- 한 영속성 컨텍스트 내에서: 같은 데이터베이스 행에 대응하는 엔터티를 요청하면, JPA는 처음에 엔터티를 로드하고 영속성 컨텍스트 내에 저장함. 그 후 같은 엔터티를 다시 요청할 경우, 영속성 컨텍스트는 이미 캐시된 같은 인스턴스를 반환하여, 어플리케이션 전반에서 해당 엔터티에 대한 동일성을 유지함.
- 다른 영속성 컨텍스트에서: 같은 데이터베이스 행을 나타내는 엔터티가 다른 영속성 컨텍스트에서 요청되면, 각 컨텍스트는 별도의 엔터티 인스턴스를 생성하고 관리함. 따라서 서로 다른 영속성 컨텍스트 사이에서는 동일성이 보장되지 않음

싱글톤 패턴은 애플리케이션 전체에서 단 하나의 인스턴스만을 생성하고 공유하는 것을 의미. 반면, JPA에서는 각 영속성 컨텍스트가 별도로 엔터티 인스턴스들을 관리하므로, 싱글톤과는 다른 개념. 영속성 컨텍스트의 역할은 주로 엔터티를 관리하고 해당 컨텍스트 내에서 엔터티의 일관성과 동일성을 보장하는 것.

 

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

entityManager.flush();

entity값을 변경하면 DB에 바로 업데이트하지 않음

트랜젝션 내부에서 영속 상태의 entity의 값을 변경하면 insert SQL Query들은 DB에 바로 보내지 않고 쿼리 저장소에 쿼리문들을 생성해서 쌓아둠.

이후 entityManager의 flush()나 트랜젝션의 commit을 통해 보내지게 됨.

<쓰기 지연>
데이터베이스와의 통신 효율성을 높으기 위해 도입된 개념
엔터티의 변경 사항을 바로 데이터베이스에 반영하지 않고, 일정량 모아서 한 번에 데이터베이스로 전송하는 방식
이를 통해 네트워크 사용과 데이터베이스의 부하를 줄일 수 있음

쓰기 지연 작동 방식
1. 변경 감지: JPA는 영속 상태의 엔터티가 변경되는 것을 감지함. 이는 주로 엔터티 매니저에 의해 관리되는 영속성 컨텍스트 내에서 일어남
2. SQL 명령어 캐싱: 감지된 변경사항에 대한 SQL 명령어(INSERT, UPDATE, DELETE 등)를 생성하지만, 바로 데이터베이스로 보내지 않고 캐시(일종의 임시 저장소)에 저장함
3. 트랜잭션 커밋 시점에 실행: 애플리케이션이 트랜잭션을 커밋하면, 이때 캐시에 모인 SQL 명령어들이 데이터베이스로 전송되어 실행됨. 이 과정에서 실제 데이터베이스의 데이터가 변경됨

쓰기 지연은 특히 여러 개의 데이터 변경이 동시에 일어날 때 유용하고, 이를 통해 애플리케이션의 성능을 향상시킬 수 있음. 또한, 트랜젝션이 롤백되는 경우, 캐시에 저장된 SQL 명령어들은 실행되지 않아 데이터베이스의 일관성을 유지하는 데 도움을 줌.

 

 

Example

update에 관련된 예제 코드

@Transactional
public Long update(Long id, PostsUpdateRequestDto requestDto) {
    Posts posts = postsRepository.findBy(id).orElseTrow(() -> 
    	new IllegalArgumentException("해당 게시글이 없습니다. id ::" + id));
        
    posts.update(requestDto.getTitle(), requestDto.getContent());
    
    return id;
}

 

JPA의 엔터티 매니저가 활성화된 상태로(Spring Data JPA를 쓰고 있다면 기본 옵션) 트랜젝션 안에서 데이터베이스 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태.

 

이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경문을 반영함. 즉, Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없게 되는 것. 이 개념을 dirty checking이라고 함.

 

Dirty Checking이란

더티 체킹(Dirty Checking)이라는 이름을 먼저 살펴보자.

Dirty는 변경이 감지된 상태를 칭한다. 그래서 데이터의 원본 상태가 수정된 것을 의미한다.

이러한 변경된 상태를 체크한다고 해서 Dirty Checking이라고 부른다.

이 기능은 데이터의 원본과 현재상태를 비교해 변경된 부분만을 데이터베이스에 업데이트함.

 

1. 트랜잭션을 커밋하면 entityManager의 내부에서 먼저 flush()가 호출됨

2. 엔터티와 스냅샷을 비교해 변경된 엔터티를 찾음

3. 변경된 엔터티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 저장

4. 쓰기 지연 저장소의 SQL을 flush()

5. 데이터베이스 트랜잭션을 커밋

 

엔터티의 생명주기

비영속

엔터티 객체를 생성했지만 아직 영속성 컨텍스트에 저장하지 않은 상태

Brower brower = new Brower();

 

영속

엔터티 매니저를 통해 엔터티를 영속성 컨텍스트에 저장한 상태

영속성 컨텍스트에 의해 관리된다는 뜻

em.persist(brower);

 

준영속

영속성 컨텍스트가 관리하던 영속 상태의 엔터티를 더이상 관리하지 않는 상태.

특정 엔터티를 준영속 상태로 만드려면 em.datch()를 호출하면 됨

* 준영속 상태에서는 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않음. 단, 식별자 값은 가지고 있음

/**
 * 엔터디를 준영속 상태로 만듦
 * 준영속 상태의 엔터티는 영속성 컨텍스트의 관리를 받지 않음
 * 영속성 컨텍스트의 변경 감지(dirty checking)이나 지연 로딩(lazy loading) 등의 기능도 사용할 수 없음
 */
em.detach(brower);

/**
 * 영속성 컨텍스트 내의 모든 엔터티를 준영속 상태로 만듦
 * 영속성 컨텍스트를 완전히 비움
 * 이 명령 후에는 영속성 컨텍스트가 관리하면 모든 엔터티가 준영속 상태가 됨
 */
em.clear();

/**
 * 영속성 컨텍스트를 종료하고, 연결된 데이터베이스 리소스를 해제함
 * 영속성 컨텍스트가 닫히면, 이 컨텍스트가 관리하던 엔터티들은 자동으로 준영속 상태가 됨
 * 이미 준영속 상태인 엔터티는 상태에 변화가 없음
 */
em.close();

 

 

삭제

엔터티를 영속성 컨텍스트와 데이터베이스에서 삭제함

em.remove(brower);

 

준영속 상태에 대해 더 알아보자

여기서 준영속 상태에 대해 더 자세히 알아보도록 하자

준영속 상태는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라고 한다.

따라서, 준영속 상태의 엔터티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.

 

준영속 상태로 만드는 법

영속 상태의 엔터티를 준영속 상태로 만드는 방법엔 크게 3가지가 있다.

1. em.detach(entity): 특정 엔터티만 준영속 상태로 전환

2. em.clear(): 영속성 컨텍스트를 완전히 초기화

3. em.close(): 영속성 컨텍스트를 종료

 

순서대로 한 번 알아보자

 

1) 엔터티를 준영속 상태로 전환: detach()

em.detach() 메서드는 특정 엔터티를 준영속 상태로 만든다.

 

public void testDetached() {
    ...
    
    // 회원 엔터티 생성, 비영속 상태
    Member member = new Member();
    member.setId("memberA");
    member.setUsername("회원A");
    
    // 회원 엔터티 영속 상태
    em.persist(member);
    
    // 회원 엔터티를 영속성 컨텍스트에서 분리, 준영속 상태
    em.detach(member);
    
    transaction.commit(); // 트랜젝션 커밋
}

 

먼저 회원 엔터티를 생성하고 영속화한 다음, detach()를 호출했다.

이 메서드를 호출하는 순간 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔터티를 관리하기 위한 모든 정보가 제거된다.

그림으로 살펴보면 다음과 같다.

detach 실행 전
detach 실행 후

위 두번째 그림에서 보는 것처럼 영속성 컨텍스트에서 memberA에 대한 모든 정보를 삭제했다.

이렇게 영속 상태였다가 더는 영속성 컨텍스트가 관리하지 않는 상태를 준영속 상태라고 한다. 이미 준영속 상태이므로 영속성 컨텍스트가 지원하는 어떤 기능도 동작하지 않는다. 심지어 쓰기 지연 SQL 저장소의 INSERT SQL도 제거되어 데이터베이스에 저장되지도 않는다.

 

정리하면, 준영속 상태는 영속성 컨텍스트로부터 분리된 상태이다.

 

2) 영속성 컨텍스트 초기화: clear()

em.clear() 메서드는 영속성 컨텍스트를 초기화해서 해당 영속성 컨텍스트의 모든 엔터티를 준영속 상태로 만든다.

 

- 영속성 컨텍스트 초기화

// 엔터티 조회, 영속 상태
Member member = em.find(Member.class, "memberA");

// 영속성 컨텍스트 초기화
em.clear();

// 준영속 상태
member.setUsername("changedName");

초기화 전
초기화 후

 

위의 두 그림을 비교해보면, 영속성 컨텍스트에 있는 모든 것이 초기화되어 버렸다. 영속성 컨텍스트를 제거하고 새로 만든 것과 같다.

이제 memberA, memberB는 영속성 컨텍스트가 관리하지 않으므로 준영속 상태이다.

member.serUsername("changedName");

그리고 준영속 상태에서는 영속성 컨텍스트가 지원하는 변경 감지도 동작하지 않는다. 따라서 위의 코드로 회원의 이름을 변경해도 데이터베이스에 반영되지 않는다.

 

3) 영속성 컨텍스트 종료: close()

영속성 컨텍스트를 종료하면 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔터티가 모두 준영속 상태가 된다.

 

- 영속성 컨텍스트 닫기

public void closeEntityManager() {
	EntityManagerFactory emf = Persistence.createEntityFactory("jpa_test");
    
    EntityManager em = emf.createEntityManager();
    EntityTransaction transaction = em.getTreansaction();
    
    transaction.begin(); // [트랜젝션] - 시작
    
    Member memberA = em.find(Member.class, "memberA");
    Member memberB = em.find(Member.class, "memberB");
    
    transaction.commit(); // [트랜잭션] - 커밋
    
    em.close(); // 영속성 컨텍스트 닫기(종료)
}

영속성 컨텍스트 제거 전
영속성 컨텍스트 제거 후

 

영속성 컨텍스트가 종료되어 더는 memberA, memberB가 관리되지 않는다.

- 영속 상태의 엔터티는 주로 영속성 컨텍스트가 종료되면서 준영속 상태가 된댜. 개발자가 직접 준영속 상태로 만드는 일은 드물다.

 

준영속 상태의 특징

그럼 준영속 상태인 회원 엔터티는 어떻게 될까?

- 거의 비영속 상태에 가깝다.

영속성 컨텍스트가 관리하지 않아 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등을 포함한 지원하는 어떤 기능도 동작하지 않는다.

- 식별자 값을 가지고 있다.

비영속 상태는 식별자 값이 없을 수도 있지만 준영속 상태는 이미 한 번 영속 상태였기 때문에 반드시 식별자 값을 가지고 있다.

- 지연 로딩을 할 수 없다.

지연 로딩은, 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다. 하지만 준영속 상태는 영속성 컨텍스트가 관리하지 않기 때문에 지연 로딩 시 문제가 발생한다. 이에 대한 자세한 내용은 뒤에서 알아보자.

 

병합: merge()

준영속 상태의 엔터티를 다시 영속 상태로 변경하기 위해서는 병합을 사용하면 된다.

merge() 메서드는 준영속 상태의 엔터티를 받아서 그 정보로 새로운 영속 상태의 엔터티를 반환한다.

 

- merge() 사용 예시

Member mergeMember = em.merge(member);

 

준영속 병합

준영속 상태의 엔터티를 영속 상태로 변경해보자

 

- 준영속 병합 예제

public class ExamMergeMain {
	static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa_test");
    
    public static void main(String args[]) {
    	Member member = createMember("memberA", "회원1"); // - (1)
        
        member.serUsername("회원명 변경"); // - (2)
        
        mergeMember(member); // - (3)

    }
    
    static Member createMember(String id, String username) {
    	// 영속성 컨텍스트1 시작
        EntityManager em1 = emf.createEntityManager();
        EntityTransaction tx1 = em1.getTransaction();
        tx1.begin();
        
        Member member = new Member();
        member.setId(id);
        member.setUsername(username);
        
        em1.persist(member);
        tx1.commit();
        
        em1.close(); // 영속성 컨텍스트 종료, member 엔터티는 준영속 상태가 됨
        
        // 영속성 컨텍스트1 종료
        
        return member;
    }
    
    static void mergeMember(Member member) {
    	// 영속성 컨텍스트2 시작
        EntityManager em2 = emf.createEntityManager();
        EntityTransaction tx2 = em2.getTransaction();
        
        tx2.begin();
        Member mergeMember = em2.merge(member);
        tx2.commit();
        
        // 준영속 상태
        System.out.println("member = " + member.getUsername());
        
        // 영속 상태
        System.out.println("mergeMember = " + mergeMember.getUsername());
        
        System.out.println("em2 contains member = " + em2.contains(member));
        
        System.out.println("em2 contains mergeMember = " + em2.contains(mergeMember));
        
        em2.close();
        
        // 영속성 컨텍스트2 종료
    }
}

 

위 코드의 출력 결과는 아래와 같다.

member = 회원명변경
mergeMember = 회원명변경
em2 contains member = false
em2 contains mergeMember = true

 

위 코드의 main 함수 부분을 한 번 살펴보자

 

1. member 엔터티는 createMember() 메서드의 영속성 컨텍스트1에 영속 상태였다가 종료되면서 준영속 상태가 되었다. 따라서 createMember() 메서드는 준영속 상태의 member 엔터티를 반환한다.

2. member.setUsername() 메서드로 회원 이름을 변경했지만, 준영속 상태인 member 엔터티를 관리하는 영속성 컨텍스트가 존재하지 않기 때문에 수정 사항을 데이터베이스에 반영할 수 없다.

3. 준영속 상태의 엔터티를 수정하기 위해서는 다시 영속 상태로 변경해야 한다. 이때 merge()(병합)을 사용한다. 코드에서 준영속 상태의 member 엔터티를 영속성 컨텍스트2가 관리하는 영속 상태로 변경했다. 영속 상태이므로 트랜잭션을 커밋할 때 수정했던 회원명이 데이터베이스에 반영된다. (정확하게는 mergeMember라는 새로운 영속 상태의 엔터티가 반환됨)

 

 

 


참고

https://binco.tistory.com/entry/JPA-%EC%98%81%EC%86%8D%EC%84%B1%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%A0%95%EC%9D%98-%ED%95%B5%EC%8B%AC%EC%9A%94%EC%95%BD

 

JPA 영속성 컨텍스트 정의 및 핵심요약

스프링 부트와 JPA를 공부하다가 update 기능에서 데이터베이스에 쿼리를 날리지 않아도 기능이 원활하게 수행되는 코드를 보게 되었습니다. 찾다 보니 JPA의 영속성 컨텍스트와 관련이 있는 걸 알

binco.tistory.com

https://trillium.tistory.com/m/139

 

[JPA] 준영속

이번 포스팅에서는 영속 → 준영속의 상태 변화를 알아보자. 📂 준영속 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라고 한다. 따라서, 준

trillium.tistory.com