JPA

JPA와 영속성 컨텍스트 이해하기

뚜코맨 2024. 7. 5. 18:48

 

 

JPA는 간단하게 말하자면 자바 ORM(Object-Relatinal Mapping) 기술 표준으로 사용되는 인터페이스의 모음이다.

즉, 실제적으로 구현된 것이 아니라 구현된 클래스와 매핑을 해주기 위해 사용되는 프레임워크인 것이다.

JPA를 구현한 대표적인 오픈소스는 Hibernate가 있다.

 

주로 사용해 왔던 JPA지만 지나간 이론적인 내용들을 다시 한번 살펴보겠다.

 

JPA에서 가장 중요한 2가지는 다음과 같다

  • 객체와 관계형 데이터베이스를 어떻게 매핑할 것인가? (Object Relational Mapping)
  • 영속성 컨텍스트

그럼 여기서 영속성 컨텍스트가 무엇이냐?

 

영속성 컨텍스트를 이해하기전 엔티티 매니저 팩토리엔티니 매니저를 알아야 한다.

 

(그림)

영속성 컨텍스트란?

  • JPA를 이해하는데 가장 중요한 용어이다.
  • "엔티티를 영구 저장하는 환경" 이라는 뜻이다.
  • EntityManager.persist(entity);

엔티티의 생명주기

  • 비영속(new/transient)
    영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속(managed)
    영속성 컨텍스트에 관리되는 상태
  • 준영속(detached)
    영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed)
    삭제된 상태

비영속

Member member = new Member();
member.setId("member1");
member.setName("회원1");

 

현재 이 상태가 객체를 생성한 상태 즉, 비영속 상태를 말한다.

 

영속

Member member = new Member();
member.setId("member1");
member.setName("회원1");

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

// 객체를 영속성 컨텍스트에 저장한 상태(영속)
em.persist(member);

 

엔티티 매니저를 생성하고 트랜잭션 안에서 em.persist(entity)를 호출하게 되면 해당 객체가 영속된 상태가 된다.

트랜잭션이란?

  • 데이터베이스내에서 명령어들의 논리적인 작업 단위이다.
  • ex) 물건 구매 행위에서 결제와 주문이 한 트랜젝션으로 묶여야한다. 결제는 성공했는데 주문이 안들어가면 돈만 빠져나가는 경우가 생길수 있기 때문에, 주문이 실패하면 rollback이 되어야 한다.
  • All OR Nothing, 모 아니면 도
  • 트랜잭션의 특성
    - 원자성: 부분적인 성공은 허용하지 않는다. (ex: 송금을 실패하면 출금도 실패해야한다.)
    - 일관성: 데이터간의 정확성을 맞춰야한다. (ex: 내가 송금을 하려면 돈이 있어야한다.)
    - 독립성: 트랜잭션 내 기능은 다른 트랜잭션의 영향을 끼쳐선 안된다.
    - 지속성: 데이터는 영구적으로 가지고 있어야 한다.

JPA의 모든 데이터의 변경은 트랜잭션 안에서 실행된다.

 

이렇게 복잡한 영속성 컨텍스트를 대체 왜쓰는 걸까?

다음과 같은 이점이 있다.

영속성 컨텍스트의 이점

  • 1차캐시
  • 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

엔티티 조회, 1차 캐시

// 엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setName("회원1");

// 엔티티를 영속
em.persist(member);

 

영속성 컨텍스트 내부엔 1차 캐시가 있다. @Id는 DB PK이고, Entity는 객체 자체가 들어간다. 1차 캐시가 어떤 장점이 있을까?

1차캐시의 장점은 바로 조회에서 큰 이점이 있다.

 

find()로 조회를 할 경우 디비에서 데이터를 select를 하는 것이 아니라 1차 캐시 내부에 있는 곳에서 바로 조회를 해온다.

만약 1차 캐시에 없는 데이터를 조회 할 경우는 어떻게 되는가? 영속성 컨텍스트의 조회의 흐름은 다음과 같다.

  1. 1차 캐시에서 엔티티를 찾는다.
  2. 있으면 메모리에 있는 1차 캐이세어 엔티티를 조회한다.
  3. 없으면 데이터베이스에서 조회한다.
  4. 조회한 데이터로 엔티티를 생성해 1차 캐시에 저장한다. (엔티티를 영속상태로 만듬)
  5. 조회한 엔티티를 반환한다.

영속 엔티티의 동일성 보장

영속성 컨텍스트는 엔티티의 동일성을 보장해준다.

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.print(a==b) // true

 

엔티티 등록,  쓰기 지연

EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션

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

// 비영속
Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

// 영속
em.persist(member);

// 엔티티 등록
tx.commit();

 

  • em.persist(member) : member 엔티티를 영속 컨텍스트에 저장하지만, 데이터베이스에는 반영되지 않는다.
  • tx.commit() : 트랜잭션을 커밋하는 순간 데이터베이스에 INSERT SQL을 보내 저장하게 된다.
  • persist()를 실행할 때, 영속 컨텍스트의 1차 캐시에는 member 엔티티가 저장되고, 쓰기 지연 SQL 저장소에는 member 엔티티의 INSERT SQL 쿼리문이 저장된다.
  • tx.commit()을 실행하는 순간 쓰기 지연 SQL 저장소에 저장된 INSERT SQL 쿼리를 보내 데이터베이스에 저장하는 것이다.

따라서, 여러 개의 엔티티를 생성하고 persist()를 하더라도, commit()을 하기 전에는 데이터베이스에 저장되지 않는다. 이를 쓰기 지연이라 하며, 영속 컨텍스트의 장점이다.

엔티티 수정, 변경 감지(Dirty Checking)

JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 해당 데이터를 변경하면 된다. 코드를 통해 알아보자.

Member member = em.find(Member.class, "member1");
member.setName("changedName");

// 아래처럼 해당 데이터를 변경해주는 메서드를 호출해야할 것 같지만 사실은 그렇지 않다.
// em.update(member)

transaction.commit();

 

주석을 잘 읽어보면 em.update()와 같은 변경된 데이터를 데이터베이스에 적용하는 과정을 실행해야할 것 같지만, JPA에서는 엔티티를 수정할 때 최초 조회 이후 스냅샷을 가지고 있다. 이 스냅샷과 변경된 데이터를 자동으로 감지해 변경된 내용에 대한 update 쿼리가 나가게 된다. 변경감지의 흐름은 아래와 같다.

  1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시가 호출된다.
  2. 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다.
  3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 저장한다.
  4. 쓰기 지연 저장소의 SQL을 플러시한다.
  5. 데이터베이스 트랜잭션을 커밋한다.

주의! 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티만 적용된다.

 

자주 나온 단어인 플러시에 대해 알아보자

플러시 - flush()

트랜잭션 커밋을 실행하면 변경 내용을 데이터베이스에 반영하게 된다. 트랜잭션 커밋이 일어날 때 플러시도 함께 발생하게 되어 데이터베이스에 반영할 수 있는 것이다. 즉, 플러시는 영속성 컨텍스트에 있는 변경 내용을 데이터베이스에 반영하는 역할을 한다.

 

플러시 발생시

  • 변경 감지(Dirty Checking)
  • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)

영속성 컨텍스트를 데이터베이스에 적용하는 방법

  • em.flush() - 직접 호출(강제 호출)
  • tx.commit() - 트랜잭션 커밋을 통한 자동 호출
  • JPQL 쿼리 실행 - 플러시 자동호출 옵션에 의해 자동으로 플러시가 처리 된다.

직접호출 예시

Member member = new Member(200L, "member200");
em.persist(member);

em.flush(); // 강제 호출
System.out.println("------------");

tx.commit();
  • flsuh()는 변경을 감지하여 데이터베이스에 반영하는 역할을 한다.
  • 따라서 이후에 commit()이 발생해도 쿼리를 다시 실행하지는 않는다.
  • 또한, flush()를 한다고 해서 1차 캐시의 내용이 사라지지 않는다.

JPQL 쿼리 실행 시 플러시가 자동으로 호출되는 이유

em.persist(memberA);
em.persist(memberB);

//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();

 

위 코드와 같이 persist()를 실행한 뒤, JPQL로 쿼리를 보내면 members에는 데이터베이스로부터 결과를 얻을 수 없을 것이다.

쿼리 발생 이전에 데이터베이스에 반영하는 flush()가 호출되어야 하기 때문이다.

직접 플러시를 호출하거나, 쿼리 이전에 commit()을 해야 한다.

이러한 문제점을 방지하기 위해 중간에 JPQL이 실행하게 되면 자동으로 플러시를 호출하여 JPQL 쿼리를 반영할 수 있도록 하는 것이다.

 

오늘은 이렇게 JPA에서 가장 중요한 개념인 영속성 컨텍스트에 대해 알아보았다.