JPA의 핵심: 객체와 DB 매핑과 영속성 컨텍스트
들어가기 전에..
JPA(Java Persistence API)에서 가장 중요한 두가지 개념
1. 객체와 관계형 데이터베이스 매핑: 데이터베이스와 객체 지향 프로그래밍 간의 설계 차이를 어떻게 극복할 것인가?
2. 영속성 컨텍스트: JPA 내부에서 데이터를 어떻게 관리하는가?
이 글에서는 이 두 핵심 개념에 대해 자세히 알아보겠다.
객체와 관계형 데이터베이스 매핑
객체와 데이터베이스는 각각의 세계에서 동작하며, 이 둘의 괴리를 해소하는 것이 JPA의 주 목적이다.
이를 위해 저번 글에서 언급했듯이, JPA는 EntityMangerFactory와 EntityManager라는 구성 요소를 제공한다.
EntityMangerFactory: DB 하나당 하나의 EntityMangerFactory 인스턴스가 생성된다.
EntityManager: EntityMangerFactory는 클라이언트의 요청이 올 때마다 EntityManager를 생성한다.
EntityManager는 내부적으로 DB 커넥션 풀을 사용한다.
영속성 컨텍스트
영속성 컨텍스트는 "엔티티를 영구 저장하는 환경"이라고 할 수 있다.
이는 눈에 보이지 않고 논리적인 개념으로, EntityManager를 통해 영속성 컨텍스트에 접근할 수 있다.
엔티티의 생명주기
엔티티의 생명주기 엔티티는 크게 4가지 상태를 가진다.
비영속 상태(Transient): JPA와 전혀 관계가 없는 상태.
영속 상태(Persistent): EntityManager가 관리하는 상태.
준영속 상태(Detached): EntityManager의 관리를 받았다가 분리된 상태.
삭제 상태(Removed): 삭제된 상태.
비영속 상태
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태이며 엔티티를 세팅만 한 상태이다.
persist를 하지 않은 상태이므로 JPA와 전혀 관련이 없다.
// 비영속
Member member = new Member();
member.setId(100L);
member.setName("Review Jpa");
영속
EntityManger가 관리하는 상태.
EntityManger 안에는 영속성 컨텍스트가 있는데, em.persist를 했을 때 영속 상태가 되고, 영속성 컨텍스트가 엔티티를 관리하게 된다.
// 영속
Member member = new Member();
member.setId(100L);
member.setName("Review Jpa");
em.persist(member)
EntityManger.persist(enitty) - 이 메서드는 사실 DB에 저장하는 것이 아니라 영속성 컨텍스트에 저장하는 것이다. 실제로 DB에 저장되는 것은 트랜잭션이 커밋되는 시점이다.
준영속 상태
회원 엔티티를 영속성 컨텍스트에서 분리하는 것이다.
em.detach(member)
삭제 상태
em.remove(member);
가장 중요한 것은 .. DB와 Application 사이에 영속성 컨텍스트가 존재한다는 것이다!
영속성 컨텍스트의 이점
1차 캐시
영속성 컨텍스트는 내부에 1차 캐시라는 것을 갖고 있다.
1차 캐시는 내부에 PK와 Entity를 Key, Value 값으로 갖고 있다.
여기서 PK는 엔티티에서 @Id로 적어놓은 칼럼이다.
1차 캐시의 장점
영속성 컨텍스트는 내부에 1차 캐시를 가지고 있어, 먼저 1차 캐시에서 엔티티를 조회한다. 이로 인해 DB 접근 비용을 줄일 수 있다.
만약에 DB에 없는 상태이면? DB에서 찾고, 1차 캐시에 저장한다.
사실..엄청 큰 도움은 되지 않는다..
EntityManger는 DB Transaction 단위로 만들고, DB Transaction이 끝날 때 같이 종료를 시켜버린다.
고객의 요청이 들어와서 비즈니스가 끝나면 영속성 컨텍스트를 지운다. 그러므로 1차 캐시도 날아간다.
그렇기 때문에 짧은 찰나의 순간에 일어난다. 여러 명의 고객이 사용하는 것이 아닌, 한 고객이 요청을 2번 했을 때만 도움이 되기 때문에 엄청나게 큰 도움이 있지는 않다. 애플리케이션에서 전체 공유하는 캐시는 JPA와 Hibernate에서는 2차 캐시라고 한다.
또한 영속 엔티티의 동일성을 보장한다. 1차 캐시가 있기 때문에 같은 객체인지 구분할 수 있다,
Member findMember1 = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
System.out.println(findMember1 == findMember2);
-> true를 return
트랜잭션을 지원하는 쓰기 지연 (Write-Behind Caching)
JPA는 em.persist() 메서드 호출 시에 즉시 데이터베이스에 저장하지 않는다.
대신, 쓰기 지연 저장소 (Write-Behind Cache) 라는 곳에 SQL을 모아둡니다
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 DB에 보내지 않는다.
// 커밋하는 순간 DB에 INSERT SQL을 보낸다.
transaction.commit();
랜잭션이 커밋되는 순간(transaction.commit()), 해당 쿼리가 데이터베이스에 한 번에 전송된다.
이렇게 하면 네트워크 비용을 줄일 수 있고, RDBMS가 트랜잭션을 처리하는 데 필요한 리소스를 효율적으로 사용할 수 있다.
버퍼링과 성능 최적화 Hibernate 설정에서 hibernate.jdbc.batch_size value = 10 같은 옵션을 주면, 10개의 SQL문을 한 번에 데이터베이스에 전송한다. 이런 방식으로 성능을 향상시킬 수 있다.
변경 감지(dirty checking)
JPA는 변경 감지라는 기능을 통해 엔티티의 상태 변화를 자동으로 감지하고 관리합니다.
Member member = em.find(Member.class, 1L);
member.setName("ZZZ");
System.out.println("---------------------------");
tx.commit(); // 성공하면 커밋
수정한 객체를 em.persist를 안 해줘도 된다. JPA는 변경감지 기능을 통해 자동으로 수정해 준다.
내부 동작 메커니즘
스냅샷: 1차 캐시에는 엔티티 뿐만 아니라, 해당 엔티티의 최초 상태를 복사한 스냅샷이 존재한다.
커밋 시점에 현재 엔티티와 스냅샷을 비교하여 변경된 부분만을 찾아내고, 이를 바탕으로 UPDATE SQL을 생성하여 쓰기 지연 저장소에 저장한다.
플러시
영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 작업이다.
영속성 컨텍스트를 플러시 하는 방법
1. em.flush() - 직접 호출
2. 트랜잭션 커밋 - 플러시 자동 호출
3. JPQL 실행
em.flush()의 작용
1차 캐시는 그대로 유지된다.
쓰기 지연 저장소에 있는 쿼리만 데이터베이스에 반영된다.
Member member = em.find(Member.class, 1L);
em.persist(member);
// commit 시점에 자동으로 flush가 되지만,
// 미리 DB에 넘어가는 쿼리를 보고 싶거나 미리 DB에 저장하고 싶을 때 사용함
em.flush();
System.out.println("--------------------");
tx.commit(); // 성공하면 커밋
실행 결과
Hibernate:
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_
from
Member member0_
where
member0_.id=?
--------------------
commit 하기 전에 SQL이 나가는 모습을 볼 수 있다.
em.flush()를 실행하면 1차 캐시에 있는 내용이 지워지는가?
1차 캐시는 그대로 있다. 쓰기 지연 SQL 저장소에 있는 SQL들만 반영되는 것이다.
JPQL 쿼리 실행 시 플러시가 자동으로 호출
Member memberA = em.find(Member.class, 1L);
Member memberB = em.find(Member.class, 1L);
Member memberC = em.find(Member.class, 1L);
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
위 코드를 보자. em.persist를 해서 memberA, memberB, memberC는 영속성 컨텍스트에서 관리를 하게 된다.
하지만 commit이나 flush를 하지 않았다.
이러한 상황에 바로 밑에서 쿼리문으로 조회하게 되면 어떻게 될까?
우리가 예상할 때는 저장이 안 된 것이므로 memberA, memberB, memberC가 조회되지 않을 것이다.
하지만 이러한 것을 방지하기 위해 JPA는 JPQL 쿼리 실행 시 플러시가 자동으로 일어난다.
하지만 다른 상황을 생각해 보자. 만약에 여기서 JPQL이 member가 아닌 다른 table을 조회한다면 굳이 member를 flush 할 필요가 있을까? 필요하지 않을 때 설정할 수 있는 것이 em.setFlushMode이다.
FlushModeType.AUTO
커밋이나 쿼리를 실행할 때 플러시 (기본 값)
FlushModeType.COMMIT
커밋할 때만 플러시
하지만 웬만하면 기본값을 두고 사용하는 것이 좋다.
정리해서 말하자면 플러시는 영속성 컨텍스트를 비우지 않는다.
영속성 컨텍스트의 변경내용을 데이터베이스에 동기화하는 것이다.
준영속 상태
em.persist()해서 db에 저장되기 전, 영속성 컨텍스트의 1차 캐시에서 관리되는 것이 영속 상태인데, em.find()해서 1차 캐시에 없다면 DB에서 조회해서 1차 캐시에 저장해 놓는다. 이 상태 또한 영속 상태이다.
그렇다면 준영속 상태는 무엇일까
영속 상태의 엔티티가 영속성 컨텍스트에서 분리되는 것이다 (detached)
영속성 컨텍스트가 제공하는 기능을 사용하지 못한다. (1차 캐시, 버퍼링, 쓰기 지연, 변경 감지 등)
준영속 상태로 만드는 방법은 아래 3가지와 같다.
// 특정 엔티티 준영속 상태로 이전
em.detacher(Entity)
// 모든 영속성 컨텍스트 준영속 상태로 이전
em.clear();
// 영속성 종료
em.close();
이러한 상세한 내용을 통해 JPA의 다양한 기능과 영속성 컨텍스트의 역할을 이해하면, 효율적인 애플리케이션을 개발할 수 있다.
'JPA > ORM 표준 JPA' 카테고리의 다른 글
[ORM 표준 JPA] 6. 연관관계 매핑 1 (0) | 2023.09.23 |
---|---|
[ORM 표준 JPA] 5. 엔티티 매핑2 - 기본키 전략 (0) | 2023.09.20 |
[ORM 표준 JPA] 4. 엔티티 매핑1 - 객체와 테이블 매핑 (0) | 2023.09.18 |
[ORM 표준 JPA 프로그래밍] 2. JPA와 Hibernate: 데이터베이스와의 다리 역할 (0) | 2023.09.13 |
[ORM 표준 JPA 프로그래밍] 1. JPA 등장 배경과 소개 (0) | 2023.09.13 |