어제 오늘 내일

[SpringBoot/JPA] "SQL을 직접 짜지 마세요" 영속성 컨텍스트(Persistence Context)의 비밀 본문

IT/SpringBoot

[SpringBoot/JPA] "SQL을 직접 짜지 마세요" 영속성 컨텍스트(Persistence Context)의 비밀

hi.anna 2026. 3. 12. 00:23

과거에 MyBatis나 JDBC를 쓸 때는 개발자가 직접 SQL을 한 땀 한 땀 짰습니다. "DB에 저장해!" 하면 INSERT 쿼리를 날리고, "수정해!" 하면 UPDATE 쿼리를 날렸죠.

하지만 JPA(ORM)의 세계는 다릅니다.
우리는 그저 자바 객체(Entity)를 다루기만 하면 됩니다. 그러면 JPA가 알아서 적절한 SQL을 생성해 DB에 날려줍니다.

이 모든 것을 가능하게 하는 JPA의 심장, '영속성 컨텍스트'에 대해 알아볼까요?

 


 

1. 영속성 컨텍스트가 뭔가요?

쉽게 말해 "애플리케이션과 데이터베이스 사이에 있는 중간 관리자(캐시)"입니다.

우리가 객체를 저장하거나 조회할 때, 바로 DB로 가는 게 아닙니다. 먼저 이 영속성 컨텍스트라는 곳에 보관됩니다. 그리고 JPA는 이 컨텍스트에 있는 정보를 바탕으로 필요한 시점(트랜잭션 커밋)에 DB와 통신합니다.

논리적인 개념입니다: 눈에 보이지는 않지만, EntityManager라는 객체를 생성할 때 그 안에 1:1로 만들어집니다.


 

2. 왜 중간 관리자를 두나요? (4가지 이점)

그냥 바로 DB에 넣으면 편할 텐데, 왜 굳이 중간 단계를 거칠까요? 여기엔 어마어마한 성능 최적화의 비밀이 숨어 있습니다.

① 1차 캐시 (First Level Cache)

영속성 컨텍스트는 내부에 Map 형태의 캐시를 가지고 있습니다.

// 1. 첫 번째 조회: DB에서 가져와서 1차 캐시에 저장
Member member1 = em.find(Member.class, "member1");

// 2. 두 번째 조회: DB 안 가고 캐시에서 바로 줌 (빠름!)
Member member2 = em.find(Member.class, "member1");
  • 장점: 똑같은 걸 두 번 조회해도 SQL은 한 번만 나갑니다.

② 동일성 보장 (Identity)

자바 컬렉션에서 꺼낸 객체처럼, JPA가 관리하는 객체는 == 비교가 참이 됩니다.

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b); // true (주소값까지 같음!)

③ 트랜잭션을 지원하는 쓰기 지연 (Transactional Write-behind) ★

이게 대박입니다. em.persist(member)를 한다고 바로 INSERT SQL이 날아가지 않습니다.

transaction.begin(); // 트랜잭션 시작

em.persist(memberA);
em.persist(memberB);
// 여기까지는 SQL을 안 보내고 "쓰기 지연 SQL 저장소"에 모아둡니다.

transaction.commit(); // 커밋하는 순간! 모아둔 INSERT 쿼리 2개를 한방에 쏩니다. (Flush)
  • 장점: 네트워크 통신 횟수를 줄여 성능을 최적화할 수 있습니다. (버퍼링 기능)

④ 변경 감지 (Dirty Checking) ★★★

JPA의 하이라이트입니다. 수정(Update) 쿼리를 날릴 필요가 없습니다.

// 1. 조회
Member member = em.find(Member.class, "member1");

// 2. 데이터 수정 (자바 객체 값만 바꿈)
member.setName("JPA Master");

// 3. update 코드 필요 없음! (em.update(member) 같은 거 없음)
transaction.commit(); 
  • 원리:
  1. JPA는 값을 읽어올 때 최초 상태(스냅샷)를 복사해둡니다.
  2. commit 시점에 스냅샷과 현재 객체를 비교합니다.
  3. "어? 이름이 바뀌었네?" -> 자동으로 UPDATE 쿼리 생성 후 DB에 반영.

 


 

3. 엔티티의 생명주기 (Lifecycle)

객체가 영속성 컨텍스트와 어떤 관계냐에 따라 상태가 나뉩니다.

  1. 비영속 (New/Transient): 객체를 생성만 한 상태. (JPA와 관계없음)
  2. 영속 (Managed): em.persist(member)를 통해 영속성 컨텍스트에 저장된 상태. (이제부터 관리 대상!)
  3. 준영속 (Detached): 관리되다가 쫓겨난 상태.
  4. 삭제 (Removed): 삭제된 상태.

 


 

4. 주의사항: Flush(플러시)란?

Flush는 영속성 컨텍스트의 변경 내용을 DB에 반영(동기화)하는 작업입니다.

  • 언제 발생하나요?
  1. transaction.commit() 자동 호출
  2. JPQL 쿼리 실행 시 자동 호출
  3. em.flush() 직접 호출 (거의 안 씀)

주의: 플러시는 캐시를 비우는 게 아닙니다! 변경 내용을 DB에 보내기만 할 뿐, 1차 캐시는 그대로 유지됩니다.


 

마치며

JPA를 쓴다는 것은 "DB 중심 설계"에서 "객체 중심 설계"로 패러다임을 전환하는 것입니다.

오늘 배운 1차 캐시변경 감지(Dirty Checking) 덕분에 우리는 더 이상 SQL 작성에 시간을 쏟지 않고, 비즈니스 로직에만 집중할 수 있게 되었습니다.

하지만 실무에서 EntityManager를 직접 쓸 일은 거의 없습니다. 다음 포스팅에서는 이것을 더 편하게 감싸서 사용하는 Spring Data JPA와 JpaRepository의 마법에 대해 알아보겠습니다.

도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

 

반응형
Comments