
지난 주에 스터디한 내용으로는
기존의 방식은 관계형 데이터베이스와 객체와의 패러다임 불일치 등으로 인한 문제가 발생한다는 점을 알아보았고
그에 따라 "왜 JPA를 사용해야 하는가"와
직접 엔티티매니저를 이용한 간단한 CRUD 작업을 JPA로 해보았습니다.
이번주에는 영속성 관리에 대해 정리해보는 시간입니다 😊
JPA가 제공하는 기능은 크게 두 가지입니다.
그 중 이번에는 매핑한 엔티티를 실제 사용하는 부분을 알아 보겠습니다!

★ 3.1 엔티티 매니저 팩토리와 엔티티 매니저
지난 시간의 실습을 통하여 엔티티 매니저를 통해
엔티티와 관련된 일을 처리하는 간단한 방식에 대해 알아보았습니다.
그리고 엔티티 매니러를 생성하는 곳은 바로 엔티티 매니저 팩토리 였습니다.
아래와 같은 코드로 엔티티 매니저 팩토리를 생성합니다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
위의 코드로 엔티티매니저를 생성하게 되면 persistence.xml에 담겨있는 정보로 엔티티 매니저를 생성하게 됩니다.
그러나 이 엔티티 매니저는 생성하는 데 많은 비용이 들기에 데이터베이스를 하나 사용할 경우
엔티티 매니저 또한 하나만 생성하고 어플리케이션 전체에서 공유해서 사용합니다.
엔티티 매니저 팩토리를 만들었다면 이제 엔티티 매니저를 생성해야겠죠?
EntityManager em = emf.createEntityManager();
위의 코드로 엔티티 매니저를 생성하게 됩니다.
엔티티 매니저를 생성할 때는 많은 비용(😨)이 들었지만
엔티티 매니저는 그에 비해 생성하는 데 비용이 거의 들지 않습니다!
엔티티 매니저는 생성하면 비용이 많이 드니까 웬만해선 생성하지 말고 공유하는 거 OK,
근데 아무리 비용이 거의 들지 않다고는 하지만, 그래도 비용이 들긴 하니까...
엔티티 매니저도 공유해서 사용하면 안되나?
라고
생각할 수도 있습니다!
근데?
NOo....
✅ 엔티티 매니저 팩토리 : 여러 스레드가 동시에 접근해도 안전
✅ 엔티티 매니저 : 여러 스레드가 동시에 접근하면 동시성 문제 발생 -> 스레드 간에 절대 공유 금지
그렇기 때문에 엔티티 매니저는 엔티티 매니저 팩토리와 다르게 공유해서는 안됩니다.
하나의 어플리케이션에서 한 개의 엔티티 매니저 팩토리가 생성된 상태이고
두 개의 엔티티 매니저가 있으며,
그 중에 하나면 데이터베이스 커넥션이 되었다고 가정해봅시다.
이러한 상황에서 언제 데이터 베이스 커넥션을 맺을 수 있을까요?
엔티티 매니저 : 저, 꼭 필요할 때 할게요... 진짜로...
엔티티 매니저는 정말 데이터베이스 연결이 필요한 시점에 커넥션을 맺게 되는데
일반적으로 트랜잭션을 시작할 때입니다.
★ 3.2 영속성 컨텍스트란?
✅ 영속성 컨텍스트
: 엔티티를 영구 저장하는 환경
그러면 언제 영속성 컨텍스트에 엔티티를 보관할까요?
-> 엔티티 매니저로 엔티티를 저장하거나 조회하는 경우 !
em.persist(member);
그 전에는 위와 같은 코드가 엔티티를 저장하기 위해 작성된 코드라고 나왔는데용
사실 이 메소드는 엔티티 매니저를 사용하여 회원 엔티티를 영속성 컨텍스트에 저장하는 메소드였습니다
영속성 컨텍스트는 엔티티 매니저를 생성할 때 한 개 만들어집니다.
그리고 엔티티 매니저로 영속성 컨텍스트를 접근 및 관리하게 됩니다!
★ 3.3 엔티티의 생명주기
✅비영속(new/transient) : 영속성 컨텍스트와 상관 없는 상태
✅영속(managed) : 영속성 컨텍스트에 저장된 상태
✅준영속(detached) : 이전에 영속성 컨텍스트에 저장했었는데, 이제 아닌 상태 (분리된 상태)
✅삭제(removed) : 삭제된 상태
엔티티는 위와 같은 4가지 상태가 있습니다 ~
그럼 이제부터 하나씩 알아보겠습니다!
✅ 비영속
객체 생성만 한 상태
영속성 컨텍스트, 데이터베이스와 전혀 관련 없는 상태입니다.
✅ 영속
영속성 컨텍스트가 관리되는 상태
엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장하면
영속성 컨텍스트가 관리하게 되고 영속상태가 됩니다!
em.persist(member);
✅ 준영속
영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않게 되는 상태
즉, 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장했다가
이 엔티티를 영속성 컨텍스트로부터 분리한 상태입니다.
em.detach(member);
위의 코드처럼 detach 메서드를 통해서 특정 엔티티를 준영속 상태로 만들 수 있습니다.
(em.close()로 영속성 컨텍스트를 닫거나 em.clear()로 영속성 컨텍스트를 초기화해도 준영속 상태입니다.)
✅ 삭제
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제
em.remove(member);
★ 3.4 영속성 컨텍스트의 특징
✅영속 상태인 경우, 식별자 값 반드시 존재
영속성 컨텍스트는 여러 개의 엔티티를 각각 어떻게 구분할까요?
영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 pk와 매핑한 값)으로 구분하므로
영속상태에서는 꼭 식별자 값이 있어야 하고
만약 없다면 오류가 발생하게 됩니다.
✅영속성 컨텍스트에 엔티티를 저장하면 일반적으로 트랜잭션을 커밋할 때 데이터베이스에 저장
영속성 컨텍스트에 엔티티를 저장하면 JPA가 이 엔티티를 트랜잭션에 커밋하는 순간에
데이터베이스에 저장하게 되는데, 이게 바로 플러시입니다.
그러면 이제부터 영속성 컨텍스트를 사용할 때의 장점을 CRUD 해보면서 알아보겠습니다.
📍엔티티 조회
영속성 컨텍스트로 엔티티 조회를 할 때의 이점을 살펴보겠습니다.
먼저 대략적인 영속성 컨텍스트 구조는 아래와 같습니다.

영속성 컨텍스트 안에는 1차 캐시가 존재하고
데이터베이스에 저장하기 전에 이곳에 식별자값과 엔티티를 저장하고
식별자값을 이용해서 엔티티를 조회할 수 있습니다.
Member member = new Member();
member.setId("member1");
member.setUsername("user1");
em.persist(member);
위와 같은 코드를 실행하게 되면
멤버 객체를 만든 후 영속상태로 만들고
이제 영속성 컨텍스트에서 이 엔티티를 관리하게 됩니다.
그러므로, 영속성 컨텍스트의 1차 캐시에 이 엔티티가 위의 이미지와 같이 저장되게 됩니다.
(다만 아직 데이터베이스에는 저장되지 않습니다.)
이렇게 저장된 엔티티를 이제 조회해보겠습니다.
Member member = em.find(Member.class, "member1");
find 메서드를 통해서 해당 엔티티를 1차 캐시에서 조회했습니다.
find를 호출하게 되면 1차 캐시에서 member1, 즉 식별자 값으로 해당 엔티티를 찾고 조회하게 됩니다.
이때에는 데이터베이스가 아닌 메모리에서 조회하므로 빠르게 조회가 가능하다는 장점이 있습니다.
그러면 해당 엔티티가 1차 캐시에 존재하지 않는 경우에는
엔티티를 어떻게 조회할 수 있을까요?
이 경우에는
엔티티 매니저가 데이터베이스를 조회하여 엔티티를 생성한 후 1차 캐시에 저장합니다.
그 후, 영속 상태의 엔티티를 반환하게 됩니다.

이렇게 하게되면 이제 1차캐시에 해당 엔티티가 존재하게 되기 때문에
나중에 이 엔티티를 조회하게 될 때는 바로 1차 캐시에서 불러오면 되므로
불러오는 속도가 훨씬 향상됩니다.
지금까지는 1차 캐시를 사용함에 따라 불러오는 속도가 향상되는 성능상의 이점을 살펴보았습니다.
다른 이점을 없을까요?
영속 엔티티의 동일성 보장도 가능합니다!
먼저 동일성과 동등성이 무엇인지 알아봅시다.
지난 2장에서 살펴보았던 개념인데요!
✅ 동일성 : 참조값 같음 ( 인스턴스 동일 )
✅ 동등성 : 인스턴스 상의 값이 동일
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b);
위의 코드를 실행하게 되면 true가 출력됩니다.
즉, 여기서 a, b의 인스턴스가 동일하다는 뜻입니다.
영속성 컨텍스트에 있는 1차 캐시에서 같은 엔티티 인스턴스를 반환하여 주었기 때문입니다.
📍 정리
영속성 컨텍스트를 사용할 때는
1차 캐시를 통한 조회 성능 향상, 동일성 보장으로 인해 좋음 😁😁
📍엔티티 등록
엔티티를 영속성 컨텍스트에 등록할 때의 이점을 살펴봅시다.
EntityManager em = emf.createEntityManager();
EntityTransation transaction = em.getTransaction();
transaction.begin();
em.pesist(member1);
em.persit(member2);
transaction.commit();
위의 코드는 member1, member2 를 영속성 컨텍스트에 등록하고 트랜잭션을 커밋하는 코드입니다.
엔티티 매니저는 트랜잭션을 커밋하기 전까지 엔티티를 저장하지 않고 1차 캐시에 저장한 채로 두고
내부 쿼리 저장소에 INSERT SQL을 저장해둡니다.
그리고 트랜잭션을 커밋할 때, 이 쿼리들을 데이터 베이스에 보냅니다.
이렇게 영속성컨텍스트에 등록할 때 데이터베이스 쿼리를 보내는 게 아니라,
트랜잭션을 커밋할 때 쿼리를 데이터베이스에 보내는 것이 바로 쓰기 지연입니다.
위의 코드에서는 어떤 순서로 쓰기 지연이 진행되는지 알아보겠습니다.
1. member1 영속화 (1차 캐시에 저장) -> 만들어진 등록 쿼리를 쓰기 지연 SQL 저장소에 저장
2. member2 영속화 (1차 캐시에 저장) > 만들어진 등록 쿼리를 쓰기 지연 SQL 저장소에 저장
3. 트랜잭션 커밋 -> 저장된 쿼리를 데이터베이스로 전달(영속성 컨텍스트 플러시)
중요한 점은 트랜잭션을 커밋해야 데이터베이스에 SQL을 전달된다는 점입니다.
커밋하지 않고 SQL을 데이터베이스에 전달해봤자 아무 의미 없습니다!
이렇게 쓰기지연에 대해 알아보았는데요
이러한 쓰기지연을 잘 활용하면 성능 최적화에 도움이 됩니다!
📍엔티티 수정
직접 SQL을 다루게 되면 SQL을 수정하는 과정에서 계속 쿼리를 추가해야하고 실수를 할 가능성이 커집니다.
즉, 이 과정에서 작성해야 할 쿼리가 많다는 점 외에도 계속 SQL을 직접 개발자가 확인해야 한다는 문제가 생깁니다.
영속성 컨텍스트가 이런 문제를 해결할 수 있습니다 👍👍😎
어떻게 해결할 수 있는지 살펴봅시다!
✅변경 감지
JPA로 엔티티를 수정해보겠습니다.
tx.begin();
Member member1 = em.find(Member.class, "id1");
member1.setAge(20);
tx.commit();
update와 같은 메서드가 존재하지 않았는데
set을 했다고해서 반영이 될까요?
결과는 "YES"입니다.
이렇게 자동으로 엔티티의 변경 사항을 데이터베이스에 반영해주는 기능이 변경감지입니다.
그러면 어떻게 JPA 가 변경감지를 할 수 있을까요?
바로 스냅샷 덕분입니다!
✅스냅샷
JPA가 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 복사해서 저장하는 것
변경감지하는 순서를 알아보겠습니다.
1. 트랜잭션 커밋하면, 엔티티 매니저가 플러시함.(쿼리를 데이터베이스로 전달)
2. 엔티티와 스냅샷(최초 상태) 비교해서 변경된 엔티티를 찾아냄
3. 변경된 엔티티의 경우에 수정 쿼리를 생성해 쓰기 지연 SQL 저장소에 보관
4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보냄
5. 트랜잭션 커밋 완료
즉, 스냅샷과 엔티티를 비교하는 과정에서 변경감지를 하게 됩니다.
이 스냅샷은 영속성 컨텍스트에 보관할 때 생성되므로
영속 상태의 엔티티가 아닌 경우에는 변경감지를 할 수 없습니다!
변경된 엔티티의 경우에 수정 쿼리를 생성한다고 했는데
member1.setAge(20);처럼 한 가지 필드만 업데이트하면,
한 가지 필드만 업데이트하는 쿼리를 자동으로 만들어줄까요?
답은 "NO"입니다.
JPA는 기본적으로 수정 시에 엔티티의 모든 필드를 업데이트하는 쿼리를 만들어냅니다.
이렇게 모든 필드를 업데이트하는 경우 생기는 장단점을 살펴보겠습니다.
✅ 장점
1. 수정 쿼리가 항상 같아서 수정쿼리를 미리 생성해 재사용할 수 있음
2. 데이터베이스에 동일한 쿼리를 보내므로 이전에 한 번 파싱된 쿼리를 재사용할 수 있음
❓단점
1. 데이터베이스에 보내는 데이터 전송량이 증가한다.
그러나 이러한 단점을 없애줄 방법도 존재합니다.
@org.hibernate.annotation.DynamicUpdqt 어노테이션 사용 시에 동적으로 UPDATE SQL을 생성하기에
컬럼이 30개 이상 되는 경우 사용하면 됩니다.
그러나 컬럼이 30개 이상 되는 쿼리는 테이블 설계가 잘못될 확률이 크고,
기본적으로는 모든 필드를 업데이트하는 기본 방식을 이용하는 것이 좋습니다.
📍엔티티 삭제
엔티티를 삭제하기 전에 조회하고, 삭제를 해줍니다.
Member member1 = em.find(Member.class, "member1");
em.remove(member1);
이 때에도 기존과 마찬가지로 삭제 쿼리를 쓰기 지연 SQL 저장소에 보관하고
트랜잭션을 커밋하는 과정에서 삭제쿼리를 데이터베이스에 전달하게 됩니다.
이렇게 삭제된 엔티티는 재사용하지 않는 게 좋습니다.
★ 3.5 플러시(Flush)
플러시는 쿼리를 데이터베이스에 전달함에 따라
영속성 컨텍스트의 변경 내용을 데이터베이스에 적용하는 것입니다.
그래서 플러시를 실행하게 되면,
변경감지를 통해 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교하여
수정된 엔티티를 찾고
수정된 엔티티에 대한 수정 쿼리 작성 후
이를 쓰기지연SQL 저장소에 저장하고
이 쿼리를 데이터베이스에 보내게 되는 것입니다.
그러면 이제 영속성 컨텍스트를 플러시하는 방법을 알아봅시다! 😊
✅직접 호출
flush() 메서드를 직접 호출하여 강제로 플러시하는 방법인데
실제로는 거의 사용하지 않습니다.
✅트랜잭션 커밋 시 플러시 자동 호출
이전에 계속 언급된 내용!!
✅JPQL 쿼리 실행 시 플러시 자동 호출
em.persist(member1);
em.persist(member2);
em.persist(member3);
query = em.createQuery("SELECT m FROM Member m", Member.Class);
List<Member> members = query.getResultList();
위의 코드는 멤버를 저장하고 멤버를 조회하는 내용인데 이 과정에서 JPQL을 사용했습니다.
persist하는 과정에서 영속성 컨텍스트에 등록이 되었고
해당 엔티티를 생성하는 쿼리가 쓰기지연 SQL 저장소에 보관된 상태입니다.
JPQL 쿼리를 실행할 때 해당 쿼리들이 플러시되어 데이터베이스에 반영되어
결과적으로 JPQL을 통해 멤버를 조회할 수 있게 됩니다.
만약 JPQL 쿼리를 실행할 때 플러시 되지 않는다면 데이터베이스에 멤버들이 생성되지 않아서
조회하지 못할 것입니다.
또한, find()의 경우에는 플러시가 실행되지 않습니다.
📍플러시 모드 옵션
플러시 모드를 따로 설정하지 않으면 AUTO 모드로 작동하고
트랜잭션 커밋, JPQL 실행 시 자동으로 플러시가 호출됩니다.
COMMIT 모드(커밋 시에만 플러시)를 통해서 성능 최적화를 할 수도 있다고 합니다.
// javax.persistence.FlushModeType 사용
em.setFlushMode(FlushMode.Type.COMMIT);
★ 3.6 준영속
준영속이란 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 상태입니다.
그러므로, 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없으므로
해당 기능을 사용하기 위해서는
다시 영속 상태로 만들어주어야 할 것입니다.
먼저 영속->준영속의 방법을 살펴보겠습니다.
📍엔티티를 준영속 상태로 전환 : detach()
em.detach(entity) 메소드는 특정 엔티티를 준영속 상태로 만듭니다.
즉, 영속성컨텍스트에게 "해당 엔티티를 관리하지 마셈"이라고 알려주는 역할입니다.
detach 메소드를 호출하면
1차 캐시, 쓰기 지연 SQL 저장소에 있던 해당 엔티티에 관한 정보가 지워집니다.
📍영속성 컨텍스트 초기화: clear()
em.clear()는 영속성 컨텍스트를 초기화하여
영속성 컨텍스트에 있는 모든 엔티티를 모두 준영속상태로 변경하게 되는 것입니다.
📍영속성 컨텍스트 종료: close()
영속성 컨텍스트를 완전히 종료하게 되면 그 안의 내용도 사라지게 되면서 모든 엔티티가 준영속상태가 됩니다.
tx.begin();
Member member1 = em.find(Member.Class, "member1");
tx.commit();
em.close();
초기화와 다른 점은, 초기화는 영속성 컨텍스트는 살아있고 그 안의 영속 상태였던 객체가 모두 준영속으로 바뀌는 것이라면
종료는 영속성 컨텍스트 자체가 없어지는 것이라고 볼 수 있습니다.
* 개발자가 직접 준영속 상태로 변경하는 일은 실제로 드뭅니다.
📍준영속 상태의 특징
준영속 상태의 엔티티는 어떻게 되는지 살펴보겠습니다.
✅ 거의 비영속 상태에 가까움
영속성 컨텍스트에 있으면 활용가능한 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등이 동작하지 않습니다.
(영속 / 준영속의 차이)
✅ 식별자 값을 가짐
이미 한 번 영속 상태였기에 식별자 값을 무조건 가지고 있습니다.그러나 비영속은 한 번도 영속 상태였던 적이 없었기 때문에, 식별자값이 있을수도 없을 수도 있습니다...
( 누군가와 연인관계를 할 때 사랑을 무조건 한다고 가정할 때,
비영속은 모태솔로인데 짝사랑도 사랑이니까.. 사랑을 해본적이 있을수도 없을수도 있고
준영속은 연애를 해본 경험이 있으니까 무조건 사랑을 해본 경험이 있는 거라고 제멋대로 생각해보았어요 😅😅)
✅ 지연 로딩을 할 수 없음
지연 로딩은 프록시 객체를 로딩한 상태에서 실제로 불러올 때 가져오는 방식이었는데
이제는 영속상태가 아니니 지연로딩을 할 수 없습니다.
📍병합 : merge()
이제 반대로 준영속을 영속으로 되돌리는 방법을 알아보겠습니다.
merge() 메소드를 이용하면 되는데
이 메소드는 준영속 상태의 엔티티를 가져와서 새로운 영속 상태의 엔티티를 반환합니다.
Member member = em.merge(entity);
✅ 준영속 병합
먼저 엔티티를 영속상태로 만들어주고
영속성 컨텍스트를 종료한 이후에
준영속상태인 엔티티를 merge()를 통해 영속상태로 되돌리면,
merge()를 실행할 때 준영속 엔티티의 식별자 값(@Id로 정의한 값)을 1차 캐시에서 찾을텐데
준영속상태이므로 1차 캐시에서 찾을 수 없을 것입니다.
그래서 데이터베이스에 엔티티를 조회하고 1차 캐시에 저장합니다.
그리고, 조회한 영속 엔티티에 준영속상태인 엔티티 값을 넣어주게 됩니다.
✅ 비영속 병합
merge는 비영속도 영속으로 전환할 수 있습니다.
이번 장에서는 영속성 컨텍스트에 대해 알아보았습니다.
눈에 보이지 않는 개념인데 책에 실린 그림을 보고 파악하니 이해가 쉬웠습니다.
영속성 컨텍스트를 사용할 때 이점과, 플러시,
영속상태, 준영속상태, 비영속상태의 특징과 각 상태를 변환하는 방식을 이해할 수 있는 좋은 시간이었습니다.
다음 장에서는 엔티티와 테이블을 매핑하는 설계 부분을 다룰 것입니다.
실제로도 지금 프로젝트를 진행하면 엔티티 매핑을 하고 있어서
다음 장을 배우면서 많은 도움을 받을 수 있을 것 같다...!

'BE > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA STUDY] 6장. 다양한 연관관계 매핑 (0) | 2025.04.13 |
---|---|
[JPA STUDY] 5. 연관관계 매핑 기초 (0) | 2025.04.06 |
[JPA STUDY] 4장. 엔티티 매핑 (0) | 2025.03.30 |
[ JPA STUDY ] 2장.JPA 시작 (0) | 2025.03.15 |
[ JPA STUDY ] 1장. JPA 소개 (0) | 2025.03.15 |