티스토리 뷰
[JPA] 영속성 컨텍스트(persistence context) 개요 및 특징(feat. Dirty Checking, flush())
on1ystar 2025. 2. 7. 04:49Goals
- 영속성 컨텍스트란?
- 엔티티 생명 주기
- 영속성 컨텐스트의 이점
영속성 컨텍스트란?
JPA를 이해하는데 가장 중요한 용어는 영속성 컨텍스트(persistence context)다. 우리말로 해석해 보면 ‘엔티티를 영구 저장하는 환경’이라는 뜻이다. JPA를 사용해 엔티티(객체)를 데이터베이스에 저장하기 전에 항상 이 영속성 컨텍스트라는 곳에 먼저 저장해야 한다. 다만, 영속성 컨텍스트에 엔티티를 저장한다고 해서 데이터베이스에 저장되는 것은 아니다. 그럼에도 엔티티를 굳이 영속성 컨텍스트에 먼저 저장하는 이유는 다양한 이점이 있는데, 이는 뒤에서 설명하겠다.
엔티티 매니저(EntityManager
)로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리할 수 있게 된다. 일반적으로 엔티티 매니저는 사용자의 요청 당 1개가 생성되며, 각 엔티티 매니저마다 영속성 컨텍스트 1개가 만들어진다. 때문에 영속성 컨텍스트에 저장된 엔티티에 접근하기 위해서는 반드시 엔티티 매니저를 통해야 한다.
💡 참고로 스프링과 함께 사용하면 여러 개의 엔티티 매니저가 하나의 영속성 컨텍스트에 매핑된다.
예를 들어 다음과 같은 메서드들을 호출하면 엔티티를 영속성 컨텍스트에 저장할 수 있다.
// em == EntityManager의 참조 변수
em.persist(entity); // entity를 영속성 컨텍스트에 저장(영속 상태로 만듬)
em.find(Entity.class, pk); // entity를 DB에서 로드(영속 상태로 만듬)
이렇게 영속성 컨텍스트 안에서 관리되는 엔티티들을 영속(managed) 상태라고 한다.
엔티티 생명 주기
엔티티에는 총 4가지 상태가 존재한다.

- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 영속(managed) : 영속성 컨텍스트에 관리되는 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
비영속(new/transient)
엔티티 객체를 단순히 생성한 상태다.
// 엔티티 객체 생성. 예제를 단순화하기 위해 setter 사용
Member member = new Member();
member.setId(1L);
member.setName("회원1");
위 member
객체는 새로 생생된 일반적인 객체일 뿐, 아직 영속성 컨텍스트나 데이터베이스와 전혀 관련이 없는 상태다.
영속(managed)
엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장하거나, DB로부터 조회했을 때 영속성 컨텍스트가 엔티티를 관리하게 된다. 즉, 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 한다.
// em == EntityManager의 참조 변수
em.persist(member); // entity를 영속성 컨텍스트에 저장(영속 상태로 만듬)
em.find(Member.class, id); // entity를 DB에서 로드(영속 상태로 만듬)
이때 영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다.
- 식별자 값 :
@Id
애노테이션이 붙은 필드로 테이블의 기본키와 매핑한
만약 식별자 값이 없으면 예외가 발생한다.
준영속(detached)
영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않게 됐을 때 준영속 상태가 된다. 쉽게 말해 영속성 컨텍스트에서 떼어내는 것이다.
em.detach(member); // 회원 엔티티를 영속성 컨텍스트에서 분리
em.clear(); // 영속성 컨텍스트 초기화
em.close(); // 엔티티 매니저(영속성 컨텍스트) 종료
삭제(removed)
엔티티를 영속성 컨텍스트와 데이터베이스에서(flush()
호출 시) 삭제한다.
em.remove(member)
영속성 컨텍스트의 이점
엔티티들을 데이터베이스에 바로 반영하지 않고 굳이 영속성 컨텍스트라는 공간에서 관리하는 이유가 있다.
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지(Dirty Checking)
- 지연 로딩
1차 캐시
영속성 컨텍스트는 내부에 엔티티를 저장할 수 있는 캐시를 가지고 있는데 이를 1차 캐시라고 한다. 엔티티를 처음 영속성 컨텍스트에 저장하면(영속 상태로 만들면) 1차 캐시에 엔티티가 저장된다.
// 엔티티 객체 생성
Member member = new Member();
member.setId("member1");
member.setName("회원1");
em.persist(member);

그림처럼 키는 식별자 값(@Id
)이고 값은 엔티티 인스턴스다. 만약 1차 캐시에 조회하려는 엔티티가 있다면, DB를 거치지 않고 메모리 상에서 바로 조회할 수 있다.
다음 코드를 실행해 보자.
Member member1 = em.find(Member.class, "member1");
그럼 먼저 1차 캐시에서 Member
클래스 중 @Id
값이 member1
인 엔티티가 있는지 찾는다. 위 1차 캐시에 해당하는 엔티티가 있으므로 굳이 DB까지 거치지 않고 메모리에서 조회할 수 있다.
만약 1차 캐시에 없는 엔티티를 조회하면 어떻게 될까?
Member member2 = em.find(Member.class, "member2");

@Id
값이member2
인Member
엔티티가 1차 캐시에 있는지 조회한다.- 1차 캐시에 없으므로 데이터베이스에서 조회한다.
- 조회한 데이터로 엔티티를 생성해 영속성 컨텍스트의 1차 캐시에 저장한다.
- 조회한 엔티티를 반환한다.
즉, 1차 캐시에 없는 엔티티는 데이터베이스에서 조회한 후, 새롭게 영속 상태로 만들어 관리한다. 이렇게 1차 캐시의 도움을 받으면, 데이터베이스에 접근해야 할 횟수를 줄일 수 있으므로 성능상 이점이 있다.
💡 엔티티 매니저는 클라이언트의 요청이 오면 생성 됐다가 응답 후 자원을 반납하게 된다. 이는 마치 커텍션 풀에서 DB 커넥션을 사용 후 반납하는 것과 같은데, 클라이언트 요청 당 하나의 트랜잭션으로 처리한다는 컨셉을 맞추기 위함이다. 그런데 엔티티 매니저가 반납되면, 그 안에 있던 1차 캐시도 모두 지워진다. 때문에 사실 하나의 요청에 처리되는 비즈니스 로직이 여러 번의 엔티티를 조회하는 것처럼 복잡하지 않다면, 1차 캐시로 얻는 성능상 이점은 거의 없다.
동일성 보장
동일성(identity)은 실제 인스턴스(인스턴스 주소)가 같다는 의미로 참조 값을 비교하는 ==
를 사용한다.
동등성(equality)은 실제 인스턴스가 가지고 있는 값이 같다는 의미로 equals()
메서드를 사용한다.
영속성 컨텍스트에서 관리되는 엔티티를 비교할 때, @Id
값이 같다면 1차 캐시에 있는 동일한 엔티티 인스턴스를 반환한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
따라서 위 코드를 실행한 후, a == b
를 해보면 true
가 나온다. 만약 영속성 컨텍스트를 사용하지 않고 DB를 바로 거쳐 인스턴스를 생성했다면, 매번 새로운 인스턴스가 생성되어 동일성이 보장되지 않았을 것이다.
트랜잭션을 지원하는 쓰기 지연
엔티티 매니저가 관리하는 엔티티의 모든 변경은 트랜잭션 안에서 이루어 져야 한다.
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(memberA);
em.persist(memberB);
tx.commit();
위 코드가 실행됐다고 했을 때, em.persist()
를 호출한다고 해서 DB에 INSERT
쿼리를 날리지 않는다. 엔티티 매니저는 commit()
이 호출되기 전까지는 내부 쿼리 저장소에 실행된 메서드에 해당하는 SQL들을 모아 둔다.

이것을 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)이라 한다.
이렇게 쿼리 저장소에 모아둔 SQL들은 commit()
이 호출된 후 데이터베이스에 날린다. 이 과정이 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업이다. 이때 데이터베이스에서도 커밋이 된다.

이 기능을 잘 활용하면 모아둔 쿼리를 데이터베이스에 한 번에 전달할 수 있으므로 성능을 최적화할 수 있다.
변경 감지(Dirty Checking)
JPA에서는 엔티티를 수정할 때 따로 update()
같은 메서드가 필요 없다. 단지 영속성 컨텍스트 안의 엔티티를 수정하기만 하면, 엔티티의 변경사항을 데이터베이스에 자동으로 반영해 준다.
JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 스냅샷으로 저장한다. 그러다 플러시(flush()
)가 호출되는 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
플러시(flush()
)
플러시(flush()
)는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 기능이다. 즉, 영속성 컨텍스트를 데이터베이스와 동기화하는 작업이라고 생각하면 된다.
플러시가 실행되면 아래와 같은 과정이 일어난다.
- 변경 감지가 동작해서 영속성 컨텍스트 안에 있는 모든 엔티티를 스냅샷과 비교한다.
- 수정된 엔티티가 있으면 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
- 이후
commit()
이 호출되면, 쓰기 지연 SQL 저장소의 모든 쿼리가 데이터베이스에 전송된다.
플러시를 호출하는 방법은 3가지다.
- 직접 호출 : 엔티티 매니저의
flush()
메서드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시한다. - 트랜잭션 커밋 시 플러시 자동 호출 : 트랜잭션이 커밋되기 전에 JPA는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하기 위해 플러시를 자동으로 호출한다.
- JPQL 쿼리 실행 시 플러시 자동 호출 : 데이터베이스에 JPQL 쿼리를 직접 작성해서 날릴 때도 플러시가 자동 실행된다.
em.persist(memberA);
em.persist(memberb);
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
query.getResultList()
가 호출되면 JPQL이 실행되는데, 그 전에 먼저 flush()
가 호출된다. 그렇지 않으면 결과 값에 memberA
와 memberB
가 포함되지 않아 무결성에 큰 문제가 생기기 때문에 이를 방지하기 위함이다.
이제 변경 감지를 테스트 해보기 위해 예를 들어 보겠다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
public Member(String name, String email) {
this.name = name;
this.email = email;
}
public void updateName(String name) {
this.name = name;
}
}
간단한 회원 엔티티로, 필드 업데이트를 위한 updateName()
메서드가 있다. 변경 감지를 위한 간단한 테스트 코드를 작성하겠다.
@SpringBootTest
@Transactional
class MemberTest {
@Autowired EntityManager em;
@Test
void 변경감지() {
//given
Member member = new Member("대상혁", "faker@god.com");
em.persist(member);
Member findMember = em.find(Member.class, member.getId());
//when
findMember.updateName("신상혁");
System.out.println("=========flush() 호출=========");
em.flush();
//then
Member updateMember = em.find(Member.class, member.getId());
Assertions.assertThat(updateMember).isEqualTo(findMember);
Assertions.assertThat(updateMember.getName()).isEqualTo("신상혁");
}
}
member
객체 생성persist()
메서드로 생성한member
를 영속 상태로 만듬- 영속 상태에 저장된
member
엔티티 조회 - 조회한
member
엔티티의name
을 수정하는 메서드 호출 flush()
를 호출해 변경 감지 적용
코드를 보면 어디에도 UPDATE
쿼리를 호출할 만한 메서드는 보이지 않는다. 테스트 결과로 생성된 SQL을 확인해 보자.

flush()
가 호출된 후, UPDATE
쿼리가 생성됐다. 이를 통해, 단순히 영속성 컨텍스트가 관리하는 영속 상태 엔티티의 필드 값을 바꾸는 것만으로도 데이터베이스에 UPDATE
쿼리를 날릴 수 있다는 것을 확인할 수 있다. 또한, 자세히 보면 엔티티를 조회하는 SELECT
쿼리가 없는데, 이는 앞서 설명했던 1차 캐시에서 저장된 엔티티를 메모리에서 바로 조회했기 때문이다.
변경 감지는 아래와 같은 과정으로 일어난다.
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시(
flush()
)가 호출된다. - 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
- 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
- 데이터베이스 트랜잭션을 커밋한다.
중요한 점은, 변경 감지 기능은 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다는 것을 잊지 말아야 한다.
그런데 이상한 점이 하나 있다. 위 코드에서는 name
만 수정했는데, 생성된 JPQL에서는 모든 필드 값을 수정하는 것처럼 보인다. 이는 JPA의 수정 기본 전략이 엔티티의 모든 필드를 업데이트하는 것이기 때문이다.
이렇게 함으로써 얻는 장점이 있다.
- 모든 필드를 사용하면 수정 쿼리가 항상 같다. 따라서 어플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다.
- 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.
단, 데이터베이스에 보내는 데이터 전송량이 증가한다는 단점은 명확하다. 특히나 필드가 많은 엔티티거나 저장되는 내용이 너무 크다면 오히려 장점을 상쇄할 수 있다. 이때는 수정된 데이터만 사용해서 동적으로 UPDATE
쿼리를 생성하는 전략을 선택할 수 있다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
@DynamicUpdate // 추가
public class Member {
// ...
}
@org.hibernate.annotaions.DynamicUpdate
: 수정된 데이터만 사용해서 동적으로UPDATE
쿼리를 생성해 주는 애노테이션

테스트를 재실행 하면, name
필드만 업데이트하는 것을 확인할 수 있다.
상황에 따라 다르지만 컬럼이 대략 30개 이상이 되면 기본 방법인 정적 수정 쿼리보다 동적 수정 쿼리가 더 빠르다고 한다. 단, 컬럼이 30개 이상 된다는 것은 테이블 설계 상 책임이 적절히 분리되지 않았을 가능성이 높다. - 김영한
( +지연 로딩에 대해서는 프록시 기능을 알아야 하고, 즉시 로딩과 비교가 필요하기 때문에 추후 따로 포스팅 하겠다.)
위와 같은 이점들을 종합해 보면, 영속성 컨텍스트를 거쳐 엔티티를 관리하게 되니까 마치 객체를 자바 컬렉션에 저장하고 사용하는 것 같은 느낌을 주게 된다. 이는 JPA가 관계형 데이터베이스와 매핑하는 방식과 더불어 객체 지향 언어와 데이터베이스 간의 패러다임 불일치를 해소하는데 큰 역할을 한다. 결과적으로 좀 더 나은 객체 지향 설계를 지향할 수 있게 된다.
References
- 자바 ORM 표준 JPA 프로그래밍 - 김영한
- https://www.blog.ecsimsw.com/entry/JPA-영속성-컨텍스트-1차-캐시-쓰기-지연
'JPA' 카테고리의 다른 글
[JPA] Fetch 전략(Eager/Lazy)과 Fetch join으로 N+1 문제 해결하기 (0) | 2025.02.28 |
---|---|
[JPA] QueryDSL 간단 (with Gradle, VSCode) (0) | 2023.05.08 |
[JPA] EntityManagerFactory & EntityManager (0) | 2023.05.07 |
- Total
- Today
- Yesterday
- Gradle
- Spring Boot
- 스프링 mvc
- Thymeleaf
- 운영체제 반효경
- 파이썬 for Beginner 연습문제
- JPA
- Python Cookbook
- git branch
- 스프링 테스트
- fetch join
- 선형 회귀
- git merge
- Spring
- git
- 파이썬 for Beginner 솔루션
- 패킷 스위칭
- 스프링
- 쉽게 배우는 운영체제
- 스프링 컨테이너
- Spring Data JPA
- Computer_Networking_A_Top-Down_Approach
- 방명록 프로젝트
- 지옥에서 온 git
- jsp
- 김영환
- 생활코딩 javascript
- 프로그래머스
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문
- 쉘 코드
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |