JPA EntityManager란 무엇인가: 영속성 컨텍스트와 엔티티 상태를 실무 흐름으로 이해하기
빠른 답
- EntityManager의 핵심 역할은 DB에 바로 쓰는 것이 아니라 영속성 컨텍스트를 통해 엔티티 상태를 관리하는 데 있다.
- persist를 호출해도 INSERT가 즉시 나가지 않을 수 있으며, 실제 SQL 시점은 flush와 commit에 달려 있다.
- 1차 캐시와 변경 감지는 영속 상태의 엔티티에서만 기대한 대로 동작한다.
- merge, detach, clear를 무심코 쓰면 예상하지 못한 UPDATE 누락이나 중복 조회가 생길 수 있다.
목차
흐름으로 보기
실무에서는 이 여섯 단계를 머릿속에 두는 것만으로도 JPA 동작을 훨씬 예측하기 쉬워집니다. 객체를 만들었다고 바로 DB와 연결되는 것이 아니고, persist()로 영속성 컨텍스트에 올린 뒤에야 관리가 시작됩니다. 이후 필드 변경은 메모리 안에서 추적되다가 flush()나 commit 시점에 SQL로 반영됩니다. 마지막으로 detach()는 관리 대상에서 분리하고, remove()는 삭제 예정 상태로 바꿉니다.
왜 EntityManager를 영속성 컨텍스트로 이해해야 할까
EntityManager를 메서드 모음으로만 보면 persist()는 저장, find()는 조회, remove()는 삭제처럼 따로따로 보입니다. 하지만 실제로는 모두 영속성 컨텍스트를 조작하는 행위입니다.
예를 들어 find()는 단순 조회가 아닙니다. 먼저 1차 캐시를 확인하고, 없으면 DB에서 읽어온 뒤 그 엔티티를 영속 상태로 올립니다. persist()도 즉시 INSERT를 보낸다기보다 새 엔티티를 관리 대상으로 등록하는 동작에 가깝습니다. 이 차이를 이해해야 다음 개념들이 자연스럽게 연결됩니다.
- 1차 캐시: 같은 트랜잭션에서 중복 조회를 줄입니다.
- 변경 감지: 영속 상태 엔티티의 필드 변경을 추적합니다.
- 쓰기 지연: SQL을 모아 적절한 시점에 보냅니다.
- 동일성 보장: 같은 식별자의 엔티티를 같은 객체처럼 다룹니다.
즉, EntityManager는 DB API가 아니라 상태 관리 API로 이해하는 편이 맞습니다.
영속성 컨텍스트와 EntityManager는 어떤 관계일까
영속성 컨텍스트는 엔티티를 관리하는 메모리상의 작업 공간입니다. EntityManager는 그 공간을 다루는 인터페이스입니다. 개발자는 EntityManager를 통해 엔티티를 넣고, 찾고, 분리하고, 삭제 예약을 합니다.
이 관계를 간단히 말하면 다음과 같습니다.
- 영속성 컨텍스트: 엔티티를 보관하고 상태를 추적하는 곳
EntityManager: 그 컨텍스트를 조작하는 창구
Spring Boot에서는 보통 개발자가 EntityManager를 직접 생성하지 않습니다. 트랜잭션 범위 안에서 프록시 형태의 EntityManager가 연결되고, 그 내부에서 실제 영속성 컨텍스트가 동작합니다. 그래서 코드가 단순해 보여도 실제로는 트랜잭션과 상태 관리가 함께 묶여 돌아갑니다.
엔티티 상태는 어떻게 바뀔까
JPA를 이해할 때 가장 중요한 축은 메서드 이름보다 엔티티 상태입니다. 엔티티는 크게 비영속, 영속, 준영속, 삭제 상태를 오갑니다.
- 비영속:
new로 만들었지만 아직 JPA가 관리하지 않는 상태 - 영속: 영속성 컨텍스트가 관리하는 상태
- 준영속: 한때 관리되었지만 지금은 분리된 상태
- 삭제: 삭제 예정으로 관리되는 상태
상태 변화는 코드로 보면 더 명확합니다.
Member member = new Member("sancho"); // 비영속
em.persist(member); // 영속
member.changeName("sancho-park"); // 변경 감지 대상
em.detach(member); // 준영속
em.remove(em.merge(member)); // 삭제 예약
비영속 상태에서는 객체를 아무리 바꿔도 DB와 상관이 없습니다. 영속 상태가 되면 JPA가 필드 변화를 추적할 수 있고, flush() 시점에 UPDATE나 INSERT를 실행합니다. 준영속이 되면 다시 평범한 객체가 되므로 값을 바꿔도 자동 반영되지 않습니다. 삭제 상태 역시 즉시 DELETE가 나가는 것이 아니라 보통 flush()나 commit 시점에 실제 SQL이 실행됩니다.
Spring Boot에서 EntityManager와 트랜잭션은 어떻게 묶일까
실무에서 EntityManager는 트랜잭션 안에서 가장 자연스럽게 이해됩니다. @Transactional이 시작되면 그 범위에서 영속성 컨텍스트가 유지되고, 메서드 종료 시점에 flush()와 commit이 이어집니다.
아래 코드는 가장 흔한 흐름입니다.
@Service
@RequiredArgsConstructor
public class MemberService {
@PersistenceContext
private EntityManager em;
@Transactional
public Long join(String name) {
Member member = new Member(name); // 비영속
em.persist(member); // 영속
member.changeName(name + "-joined"); // 변경 감지
return member.getId();
}
@Transactional(readOnly = true)
public Member find(Long id) {
return em.find(Member.class, id);
}
}
이 코드에서 persist()가 호출되는 순간 엔티티는 영속 상태가 됩니다. 하지만 INSERT가 꼭 그 줄에서 나간다고 생각하면 안 됩니다. 실제 SQL은 트랜잭션 종료 직전, 혹은 쿼리 실행 직전의 flush() 타이밍에 나갈 수 있습니다. 그래서 JPA를 볼 때는 코드 순서와 SQL 순서를 구분해서 봐야 합니다.
persist, find, detach, remove, flush를 코드로 따라가 보기
아래 예시는 실무에서 가장 자주 마주치는 메서드들을 한 번에 보여줍니다.
@Transactional
public void entityManagerFlow(Long id) {
Member created = new Member("sancho");
em.persist(created);
created.changeName("sancho-park");
Member first = em.find(Member.class, created.getId());
Member second = em.find(Member.class, created.getId());
System.out.println("same instance = " + (first == second));
em.flush();
em.detach(first);
first.changeName("detached-name");
Member target = em.find(Member.class, id);
em.remove(target);
}
여기서 볼 포인트는 세 가지입니다.
persist()는 영속 상태로 만드는 호출이지, 반드시 즉시INSERT를 보내는 호출은 아닙니다.- 같은 트랜잭션 안에서 같은 식별자를
find()로 두 번 조회하면 1차 캐시 때문에 같은 객체를 돌려줄 수 있습니다. detach()이후에는 필드 값을 바꿔도 변경 감지가 동작하지 않습니다.
즉, JPA의 편의 기능은 영속 상태를 전제로 한다는 점을 기억해야 합니다.
merge가 특히 헷갈리는 이유
merge()는 많이 쓰이지만 가장 자주 오해되는 메서드이기도 합니다. 핵심은 전달한 객체 자체를 다시 관리하는 것이 아니라, 그 상태를 복사한 관리 객체를 반환한다는 점입니다.
이 차이를 놓치면 merge() 이후 원본 객체를 계속 수정하면서 왜 반영이 안 되는지 헷갈리게 됩니다. 실무에서는 아래 상황이 자주 문제를 만듭니다.
- 준영속 객체를
merge()한 뒤, 반환값이 아니라 원본 객체를 계속 수정하는 경우 - DTO 일부 필드만 채운 객체를
merge()해 의도치 않게 값이 덮어써지는 경우 - 수정 전에 관리 엔티티를 조회하지 않아 예상보다 많은
SELECT가 발생하는 경우
그래서 신규 저장은 persist() 중심으로, 수정은 가능하면 조회한 영속 엔티티를 직접 바꾸는 방식이 더 예측 가능합니다. 즉, "찾고 바꾸기"가 "통째로 합치기"보다 안전한 경우가 많습니다.
SQL 로그로 보면 동작이 더 선명해진다
JPA를 디버깅할 때 가장 중요한 단서는 코드가 아니라 SQL 로그입니다. persist()를 호출했는데 왜 SQL이 안 보이는지, 왜 갑자기 UPDATE가 나갔는지, 왜 같은 엔티티를 또 조회하는지 같은 질문은 로그를 보면 바로 풀리는 경우가 많습니다.
먼저 로컬에서 자주 쓰는 설정은 아래 정도면 충분합니다.
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: debug
org.hibernate.orm.jdbc.bind: trace
이제 실제 출력 예시를 보겠습니다.
2026-04-05 14:21:03 DEBUG org.hibernate.SQL
select m1_0.id, m1_0.name
from member m1_0
where m1_0.id=?
2026-04-05 14:21:03 TRACE org.hibernate.orm.jdbc.bind
binding parameter [1] as [BIGINT] - [1]
same instance = true
2026-04-05 14:21:04 DEBUG org.hibernate.SQL
insert into member (name, id)
values (?, ?)
2026-04-05 14:21:04 TRACE org.hibernate.orm.jdbc.bind
binding parameter [1] as [VARCHAR] - [sancho-park]
binding parameter [2] as [BIGINT] - [10]
2026-04-05 14:21:05 DEBUG org.hibernate.SQL
delete from member
where id=?
이 로그에서 읽어야 할 핵심은 다음입니다.
find()호출로 먼저SELECT가 나갔습니다.- 같은 엔티티를 다시 조회했을 때 애플리케이션 출력이
same instance = true로 찍혔습니다. persist()직후가 아니라flush()또는 트랜잭션 종료 시점에INSERT가 실행됐습니다.remove()역시 호출 즉시가 아니라 동기화 시점에DELETE가 나갑니다.
운영 중 "값을 바꿨는데 UPDATE가 없다"는 문제가 보이면 먼저 그 엔티티가 정말 영속 상태였는지, 중간에 detach()나 clear()가 있었는지부터 확인하는 것이 빠릅니다.
실무에서 자주 놓치는 주의점
EntityManager를 직접 많이 쓰지 않더라도 Spring Data JPA의 Repository는 결국 이 모델 위에 서 있습니다. 그래서 Repository만 사용하더라도 아래 원칙은 알아두는 편이 좋습니다.
- 수정 로직은 가능하면 조회한 영속 엔티티를 변경하는 방식으로 작성합니다.
flush()와commit은 같은 개념이 아닙니다.flush()는 동기화이고,commit은 최종 확정입니다.clear()는 1차 캐시를 비우므로 이후 조회에서 다시SELECT가 나갈 수 있습니다.detach()된 객체는 겉보기엔 같아 보여도 더 이상 변경 감지 대상이 아닙니다.merge()는 편리해 보여도 관리 객체와 원본 객체를 헷갈리게 만들 수 있습니다.
결국 EntityManager는 "DB에 명령을 내리는 객체"보다 "엔티티 생명주기를 관리하는 객체"로 이해해야 실전에서 덜 헷갈립니다. 이 관점이 잡히면 persist, find, flush, detach, remove가 각각 따로 떨어진 API가 아니라 하나의 상태 전이 흐름으로 보이기 시작합니다.
원문 참고
https://www.maeil-mail.kr/question/29
댓글
댓글 쓰기