1편에서 JPA 개념과 Entity, Repository 를 봤다. 이번엔 영속성 컨텍스트와 더티체킹이다.
계속 이름만 나왔던 하이버네이트가 실제로 뭘 하는지도 같이 정리한다.
영속성 컨텍스트란?
영속성 컨텍스트(Persistence Context)는 엔티티를 관리하는 논리적인 공간이다.
EntityManager 는 엔티티를 저장할 때 DB 로 바로 쿼리를 날리지 않는다. 일단 영속성 컨텍스트에 담아두고 관리합니다.
여기서 나오는 게 1차 캐시다.
같은 트랜잭션 안에서 같은 엔티티를 두 번 조회해도 실제 SELECT 는 한 번만 나간다. 두 번째부터는 영속성 컨텍스트에서 바로 꺼낸다.
Member member1 = memberRepository.findById(1L).get();
Member member2 = memberRepository.findById(1L).get();
// member1 == member2 → true, SELECT는 한 번만 실행됨
같은 트랜잭션에서 조회한 같은 엔티티는 == 비교로도 같다. 이게 동일성 보장이다.
“1차 캐시” 라는 이름 때문에 Redis 같은 별도 인프라로 오해하기 쉽다. 그런 게 아니라 트랜잭션(정확히는 EntityManager) 하나에 종속된 메모리 공간이다.
트랜잭션이 끝나면 같이 사라지고 다른 요청·스레드와 공유되지 않는다.
여러 트랜잭션이 공유하는 캐시가 필요하면 그건 2차 캐시(Second Level Cache) 영역이다. 별도로 설정해야 켜지는 다른 기능이다.
엔티티 생명주기 — 비영속 / 영속 / 준영속 / 삭제
영속성 컨텍스트를 이해하려면 엔티티가 거치는 상태를 알아야 합니다.

- 비영속(new/transient):
new Member()로 막 만든 상태. JPA가 전혀 관리하지 않음 - 영속(managed):
persist()로 저장했거나,find()/JPQL로 조회해서 영속성 컨텍스트에 들어온 상태. 이때부터 더티체킹 대상이 됨 - 준영속(detached): 영속 상태였다가
detach(),em.clear(),em.close()등으로 영속성 컨텍스트에서 빠져나온 상태. 더 이상 변경 감지가 안 됨 - 삭제(removed):
remove()호출로 삭제 예약된 상태. 트랜잭션 커밋 시점에 실제 DELETE 실행
자주 만나는 실수가 하나 있다. 트랜잭션 밖으로 나가 준영속이 된 엔티티의 값을 바꿔놓고 “왜 DB에 반영이 안 되지” 하는 경우다.
준영속 상태는 영속성 컨텍스트가 더 이상 추적하지 않는다. setter 를 아무리 호출해도 더티체킹이 동작하지 않는다.
merge() 로 다시 영속 상태로 만들거나, 애초에 트랜잭션 안에서 변경을 끝내야 한다.
더티체킹(Dirty Checking) — JPA의 시그니처 기능
JPA 는 엔티티 값만 바꾸면 save() 나 update() 를 호출하지 않아도 UPDATE 쿼리를 날린다.
@Transactional
public void updateMemberName(Long id, String newName) {
Member member = memberRepository.findById(id).orElseThrow();
member.setName(newName);
}
직접 돌려서 나온 로그다. save() 는 어디에도 없다.
select m1_0.id, m1_0.age, m1_0.email, m1_0.name, m1_0.team_id
from member m1_0 where m1_0.id=? -- 조회
update member
set age=?, email=?, name=?, team_id=? -- 트랜잭션이 끝나면서 알아서
where id=?
하나 더 보인다. 바꾼 건 name 하나인데 UPDATE 문에는 컬럼이 전부 들어간다. 하이버네이트 기본 동작이다.
member.setName() 만 호출했는데 트랜잭션 커밋 시점에 UPDATE 가 실행된다. 원리는 이렇다.
- 엔티티가 영속성 컨텍스트에 처음 들어올 때, 그 상태를 스냅샷으로 저장해둠
- 트랜잭션 커밋 시점(정확히는 flush 시점)에, 현재 엔티티 상태와 스냅샷을 비교
- 변경된 필드가 있으면 자동으로 UPDATE 쿼리를 만들어서 실행
이게 더티체킹이다. “더럽혀진(dirty) 상태를 체크한다” 는 뜻이고, 변경 쿼리를 직접 짜지 않아도 되게 해줍니다.
더티체킹은 엔티티의 모든 컬럼을 UPDATE 에 포함시킨다. 필드 하나만 바꿔도 나머지가 전부 SET 절에 들어간다.
@DynamicUpdate 를 붙이면 변경된 컬럼만 나가게 할 수 있다. 대신 매번 어떤 컬럼이 바뀌었는지 판단하는 비용이 붙어서, 컬럼이 아주 많은 테이블이 아니면 굳이 안 쓴다.
하이버네이트(Hibernate)는 뭐를 하는 애일까
1편에서 “JPA 는 명세일 뿐이고 실제 구현체가 필요하다” 고 했다. 그 구현체가 하이버네이트다.
- 영속성 컨텍스트를 실제로 관리하는 것도 하이버네이트
- 더티체킹으로 변경 감지하고 실제 SQL을 만들어서 실행하는 것도 하이버네이트
- DB마다 다른 SQL 문법 차이(Dialect)를 흡수해서 처리하는 것도 하이버네이트
JPA 가 “이런 기능을 제공해야 한다” 는 규칙이라면, 하이버네이트는 그걸 실제로 동작하게 만드는 엔진이다.
Spring Boot 에서 spring-boot-starter-data-jpa 만 추가하면 하이버네이트가 기본으로 따라온다. 별도 설정 없이 이미 쓰고 있었던 것입니다.
영속성 컨텍스트와 더티체킹 정리
- 영속성 컨텍스트는 엔티티를 관리하는 공간이자 1차 캐시 (트랜잭션에 종속, 2차 캐시와는 다른 개념)
- 엔티티는 비영속 → 영속 → 준영속/삭제 순으로 상태가 바뀌고, 준영속 상태에선 더티체킹이 동작 안 함
- 더티체킹은 필드 변경만으로 자동 UPDATE를 만들어주는 JPA의 핵심 기능 (기본은 전체 컬럼 UPDATE)
- 하이버네이트는 이 모든 걸 실제로 구현하고 동작시키는 JPA의 구현체
다음 편은 동적 쿼리를 짤 때 쓰는 QueryDSL 이다.