영속성 컨텍스트는 JPA에서 중요한 핵심 개념이라고 많이 들었는데 영속성 컨텍스트는 정확히 뭘까?
영속성 컨텍스트
영속성 컨텍스트는 '엔티티를 영구 저장하는 환경'으로, JPA가 관리하는 엔티티 객체의 집합이다.
엔티티 매니저를 통해 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
영속성 컨텍스트는 물리적으로 JVM 메모리(Heap 영역)에 할당되지만 개발자가 직접 접근할 수 없고, EntityManager를 통해서 간접적으로 사용하는 논리적인 개념이다.
영속성 컨텍스트는 Map 형태의 자료구조를 가지며 [ 키: @Id, 값: 엔티티 인스턴스 ] 형태를 띈다.
`Map<@Id, Entity Instance>`
영속성 컨텍스트의 동작을 이해하기 위해선 엔티티 생명주기를 먼저 알아야 한다.
엔티티의 생명주기
비영속(new/transient)
영속성 컨텍스트와 전혀 관련이 없는 새로운 상태
아직 JPA가 해당 엔티티를 관리하지 않는다.
특징:
- 영속성 컨텍스트가 제공하는 어떤 기능도 사용할 수 없음
- 데이터베이스와 전혀 관계가 없음
- @Id값의 존재 여부는 상관없음
// 단순히 객체만 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1"); // Id를 설정하지 않았지만 비영속 상태에선 문제 없음
member.setUsername("회원1");
영속(managed)
영속성 컨텍스트에 의해 관리되는 상태
엔티티가 영속성 컨텍스트에 저장되어 JPA가 해당 엔티티를 인식하고 관리한다.
특징:
- 1차 캐시에 저장
- Dirty checking 사용 가능
- Lazy loading 사용 가능
- 트랜잭션 커밋 시점에 변경사항이 데이터베이스에 반영
- 동일한 트랜잭션 내에서 엔티티의 동일성 보장
영속 상태로 만드는 방법
1. `persist()`
// 객체를 영속성 컨텍스트에 저장 (영속)
em.persist(member);
2. `find()`
// 데이터베이스에서 조회한 엔티티는 자동으로 영속 상태가 됨
Member findMember = em.find(Member.class, "member1");
3. `JPQL`
// JPQL로 조회한 결과도 영속 상태가 됨
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
4. `merge()`
// 준영속 엔티티를 영속 상태로 변경
Member mergedMember = em.merge(detachedMember);
준영속(detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
영속성 컨텍스트가 관리하던 엔티티가 영속성 컨텍스트에서 분리된다.
특징:
- 식별자 값을 가지고 있음 ➡️ 한번 영속 상태였기 때문
- 영속성 컨텍스트가 제공하는 기능을 사용할 수 없음
- Dirty checking이 동작하지 않아 수정해도 데이터베이스에 반영되지 않음
- Lazy loading 시 LazyInitializationException 발생
준영속 상태로 만드는 방법
1. `detach()`
// 특정 엔티티만 준영속 상태로 전환
em.detach(member);
2. `clear()`
// 영속성 컨텍스트를 완전히 초기화 (모든 엔티티가 준영속 상태가 됨)
em.clear();
3. `close()`
// 영속성 컨텍스트를 종료 (모든 엔티티가 준영속 상태가 됨)
em.close();
삭제(removed)
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하기로 한 상태
실제 삭제는 트랜잭션 커밋 시점에 이뤄진다.
특징:
- 영속성 컨텍스트에서는 즉시 제거되지만, 데이터베이스에는 트랜잭션 커밋 시점에 삭제됨
- 삭제된 엔티티는 재사용하지 않는 것이 좋음
- 참조가 남아있을 수 있으므로 null 처리하는 것이 안전
영속성 컨텍스트에서 엔티티 삭제하는 방법
`remove()`
// 엔티티를 삭제 상태로 전환
em.remove(member);
// 트랜잭션 커밋 시점에 DELETE SQL 실행
em.getTransaction().commit();
영속성 컨텍스트가 제공하는 기능
영속성 컨텍스트는 다음과 같은 기능을 제공한다.
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연(Transactional Write-Behind)
- Dirty checking
- Lazy loading
- Eager Loading
- Flush
- 트랜잭션 롤백기능
1차 캐시
1차 캐시는 영속성 컨텍스트 내부의 메모리 공간으로 `Map<@Id, Entity Instance>` 형태로 엔티티를 저장하고 관리한다. 1차 캐시는 EntityManager와 동일한 생명주기를 가져 EntityManager가 닫히면 1차 캐시도 소멸한다.
장점:
- 데이터베이스 접근 횟수 감소로 성능 향상
- 동일 트랜잭션 내 반복 조회 시 데이터베이스 접근 없이 메모리에서 조회
동작 방식:
- 영속 엔티티를 1차 캐시에서 찾음
- 1차 캐시에 없으면 데이터베이스에서 조회 후 1차캐시에 저장
동일성 보장
동일한 트랜잭션 내에서 동일한 엔티티 인스턴스를 보장한다.
동일성 비교: 서로 인스턴스가 같다. `==`를 통해 비교 가능
동등성 비교: 인스턴스는 서로 다르지만 인스턴스의 필드 값이 같다. `equals()`를 오버라이딩해 비교 가능
Member a = em.find(Member.class, "id1");
Member b = em.find(Member.class, "id1");
System.out.println(a == b); // true 반환
트랜잭션을 지원하는 쓰기 지연(Transactional Write-Behind)
SQL을 바로 실행하지 않고 모아서 트랜잭션 커밋 시점에 한꺼번에 실행한다.
장점:
- 데이터베이스 접근 횟수 최소화로 성능 향상
- 배치 처리 가능(SQL 일괄 처리)
동작 방식:
- `persist()` 호출 시 쓰기 지연 SQL 저장소에 INSERT SQL 저장
- 트랜잭션 커밋 시점에 저장된 SQL 일괄 실행
Dirty checking
JPA가 엔티티의 변경 사항을 자동으로 감지해 데이터베이스에 반영한다. 이를 통해 SQL의 UPDATE문 없이도 엔티티를 수정할 수 있다.
동작 방식:
- 스냅샷 생성
영속성 컨텍스트는 엔티티를 처음 영속 상태로 만들 때 엔티티의 원본을 스냅샷으로 저장 - 변경 감지 수행
트랜잭션이 커밋되면 `flush()`메서드를 호출
영속성 컨텍스트는 관리하는 모든 영속 엔티티를 스냅샷과 비교해 엔티티의 현재 상태와 스냅샷의 값이 다르면 변경을 감지 - UPDATE 생성 및 실행
변경된 엔티티에 대해 자동으로 UPDATE 쿼리문을 생성
생성된 쿼리문은 쓰기 지연 SQL 저장소에 등록되고 트랜잭션 커밋 시점에 이 SQL이 데이터베이스로 전송되어 실행
// 트랜잭션 시작
EntityTransaction tx = em.getTransaction();
tx.begin();
// 엔티티 조회 (이 시점에 스냅샷 생성)
Member member = em.find(Member.class, "id1");
// 엔티티 수정 (단순히 객체의 값만 변경)
member.setName("새로운 이름");
member.setAge(25);
// 별도의 update() 메서드 호출이 필요 없음!
// 트랜잭션 커밋
tx.commit();
// 커밋 시점에 변경 감지가 동작하여 UPDATE SQL 실행
Lazy loading
JPA에서 연관된 엔티티의 데이터를 실제로 사용하는 시점까지 데이터베이스 조회를 지연시키는 기능이다.
JPA 표준에서는 `@OneToMany`, `@ManyToMany`의 기본이 Lazy loading이다.
장점:
- 필요한 데이터만 로딩하므로 초기 로딩 시간 단축
- 불필요한 데이터베이스 조회 방지
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 설정
@JoinColumn(name = "TEAM_ID")
private Team team;
}
영속성 컨텍스트가 닫힌 후 Lazy loading을 시도할 때 `LazyInitializationException`이 발생한다.
// 트랜잭션 내에서 Member 조회
Member member = em.find(Member.class, memberId);
em.close(); // 영속성 컨텍스트 종료
// 영속성 컨텍스트가 종료된 후 지연 로딩 시도
// LazyInitializationException 발생!
String teamName = member.getTeam().getName();
이 때, fetch join을 사용해 필요한 연관 엔티티를 명시적으로 함께 조회해 해결할 수 있다.
Eager loading
JPA에서 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 기능이다. Lazy loading과 반대로, 엔티티를 로드하는 시점에 모든 연관 엔티티를 함께 조회한다.
JPA 표준에서는 `@ManyToOne`, `@OneToOne`의 기본이 Eager loading이다.
장점:
- 추가 쿼리 없이 연관 엔티티에 바로 접근할 수 있음
- `LazyInitializationException` 걱정 없이 엔티티를 사용할 수 있음
- 엔티티들을 한 번에 조회하여 쿼리 횟수를 줄일 수 있음
단점
- 불필요한 데이터 로딩이 있기 때문에 연관 엔티티가 많은 경우 성능 저하가 심해질 수 있음
- 여러 연관관계가 Eager loading으로 설정된 경우 복잡한 조인 쿼리를 생성할 수 있음
N+1 문제
Lazy loading, Eager loading 모두 N+1문제에 자유롭지 않다. N+1문제에 대해선 뒤에서 자세하게 다룰 예정이다.
둘 다 N+1에서 자유롭지 않음에도 불구하고 실무에서는 Lazy loading을 사용하는 이유는 두 전략의 실질적 차이에 있다.
Lazy loading의 경우 필요한 경우에만 연관 엔티티를 로딩하고, Eager loading은 불필요한 경우에도 연관 엔티티를 항상 로딩한다. 따라서, Lazy loading은 N+1 문제가 어디에서 발생할 지 예측이 가능하고 최적화할 수 있지만 Eager loading은 모든 케이스에 최적화가 필요하고 예상치 못한 곳에서 N+1 문제가 발생할 수 있다.
따라서, Lazy loading이 N+1 문제를 더 잘 제어할 수 있기 때문에 아래와 같이 글을 작성했다.
💡언제 Lazy loading을 쓰고, 언제 Eager loading을 사용할까?
대부분의 연관관계는 Lazy loading으로 설정하고, 필요한 경우에만 fetch join 등의 기법을 사용해 함께 조회하자.
Eager loading은 N+1문제와 불필요한 데이터 로딩으로 인해 성능 저하를 초래할 수 있어 가급적 자제하자.
Flush
Flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업이다. 영속성 컨텍스트에 저장된 엔티티의 변경사항을 데이터베이스에 반영하는 과정이라고 생각할 수 있다. ➡️ 쓰기 지연 SQL 저장소의 쿼리들을 DB에 반영하는 작업, 영속성 컨텍스트를 비우는게 아님!
동작 방식:
- Dirty checking을 통해 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해 변경된 엔티티를 찾음
- 변경된 엔티티가 있으면 해당 엔티티에 대해 UPDATE 쿼리문을 생성해 SQL 저장소에 저장
- 쓰기 지연 SQL 저장소의 쿼리들을 데이터베이스에 전송
플러시가 발생하는 시점
명시적 호출
`EntityManger.flush()`메서드를 직접 호출해 강제 플러시를 수행할 수 있다.
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = new Member(1L, "회원1");
em.persist(member);
// 강제로 플러시 호출
em.flush();
// 이 시점에 데이터베이스에 SQL이 전송됨
// SELECT * FROM MEMBER WHERE ID = 1; 실행해도 데이터 확인 가능
tx.commit();
트랜잭션 커밋 시 자동 호출
트랜잭션을 커밋하면 자동으로 플러시가 호출된다. 트랜잭션 커밋 전에 변경 내용을 데이터베이스에 반영해야하기 때문이다.
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = new Member(2L, "회원2");
em.persist(member);
// 트랜잭션 커밋 시 자동으로 플러시 호출
tx.commit();
JPQL 쿼리 실행 시 자동 호출
JPQL 쿼리를 실행하기 전에 자동으로 플러시가 호출된다. JPQL이 데이터베이스에서 엔티티를 조회하기 때문에, 영속성 컨텍스트의 변경 사항을 먼저 데이터베이스에 반영해야 일관성 있는 결과를 얻을 수 있다.
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = new Member(3L, "회원3");
em.persist(member);
// JPQL 실행 전 자동으로 플러시 호출
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
// member 엔티티가 결과에 포함됨
tx.commit();
트랜잭션 롤백 기능
영속성 컨텍스트는 트랜잭션 롤백 기능을 제공한다.
동작 방식:
- 영속성 컨텍스트에 캐시된 엔티티들의 변경사항은 데이터베이스에 반영되지 않음
- 트랜잭션 내에서 수행된 모든 데이터 변경 작업(insert, update, delete)이 취소
- 영속성 컨텍스트는 트랜잭션이 시작되기 전의 상태로 엔티티를 복원
영속성 컨텍스트는 엔티티의 원본 상태인 스냅샷을 가지고 있기 때문에, 롤백이 발생하면 영속성 컨텍스트의 변경사항만 폐기하면 된다. 이를 통해 데이터 일관성을 유지하고 트랜잭션의 ACID를 보장한다.
'Spring > Spring Data JPA' 카테고리의 다른 글
| [Spring Data JPA] ID에 관하여 (2) | 2025.03.13 |
|---|---|
| [Spring Data JPA] JPA의 N + 1 문제 (0) | 2025.03.07 |
| [Spring Data JPA] 엔티티 매니저(Entity Manager) (0) | 2025.03.05 |
| [Spring Data JPA] JPA의 ddl-auto 옵션 (0) | 2025.02.28 |
| [Spring Data JPA] 새로운 Entity인지 판단하기 (0) | 2025.02.27 |