ORM(Object Relatinal Mapping)이란 객체는 객체대로, 관계형 데이터베이스는 관계형 데이터베이스대로 설계를 하고 둘 사이의 관계를 중간에서 매핑시켜 관계형 데이터베이스를 객체지향적으로 사용하게 해주는 기술이다. JPA는 자바 애플리케이션과 JDBC 사이에서 JDBC API를 이용해 DB에 SQL 전달하여 동작하는 자바 ORM 기술 표준 인터페이스로 대표적인 구현체로는 하이버네이트 JPA가 있다.
먼저 JPA를 사용하려면 EntityManagerFactory, EntityManager, 영속성 컨텍스트, 트랜잭션에 대해 알아야 한다.
EntityManagerFactory는 웹서버가 올라오는 시점에 DB당 하나만 생성되어 애플리케이션 전체에서 공유되며 멀티 스레드 환경에서 사용할 수 있으며 createEntityManager()를 통해 EntityManager를 생성할 수 있다.
엔티티 매니저는 트랜잭션을 수행하고 엔티티, SQL을 관리하며 커넥션을 통해 DB에 접근하는 등 핵심 동작을 담당하는 중요한 역할을 하며 트랜잭션에 대해 생각해보면 당연히 스레드 간에 공유는 불가능하며 트랜잭션 수행 후에 반드시 소멸(close)시켜야한다.
// Persistence에서 하나의 EntityManagerFactory를 생성해서 애플리케이션 전체에서 공유
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// EntityManagerFactory에서 EntityManager 생성, 쓰레드간에 공유할 수 없고 사용하고 버려야 한다.
EntityManager em = emf.createEntityManager();
// JPA의 모든 데이터 변경은 트랜잭션 안에서 실행
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setId(1L);
member.setName("Kim");
//save
em.persist(member);
//read
Member findMember = em.find(Member.class, 1L);
//update
findMember.setName("Lee");
//delete
em.remove();
tx.commit();
} catch (Exception e) {
// 문제가 생기면 롤백
tx.rollback();
} finally {
// 나중에 생성된 순서대로 close
em.close();
}
emf.close();
영속성 컨텍스트
영속성 컨텍스트는 자바 애플리케이션과 데이터베이스 사이에서 엔티티를 저장, 관리하는 논리적인 개념으로 엔티티 매니저를 통해 접근할 수 있다.
엔티티의 생명 주기
비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 (new Member)
영속(managed) : 영속성 컨텍스트에 관리되는 상태 (em.persist)
준영속 (datached) : 영속성 컨텍스트에 저장되었다가 분리된 상태 (em.detach)
삭제 (removed) : 삭제된 상태 (em.remove)
특징
1) 1차 캐시
영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 엔티티를 저장, 관리하기 위해서 내부에 1차 캐시 저장소를 가지고 있는데 엔티티를 persist() 하면 1차 캐시에 Key(@Id, PK) Value(Entity), 최초 상태의 엔티티를 복사해둔 스냅샷(변경 감지)을 저장해둔다.
2) 동일성
엔티티 매니저를 통해 조회(em.find)를 하면 먼저 1차 캐시를 확인해서 엔티티 참조값을 반환해주기 때문에 새로운 객체를 생성하지 않고 동일성을 보장한다. 조회하려는 엔티티가 1차 캐시에 없을 경우 DB에서 조회를 해서 1차 캐시에 저장을 하고 반환을 해준다.
3) 쓰기 지연
영속성 컨텍스트는 SQL을 쓰기 지연 저장소에 보관해두었다가 flush()가 호출되는 시점에 SQL을 버퍼처럼 모아서 전송한다.
4) 변경 감지
1차 캐시에 엔티티 스냅샷을 만들어두고 flush() 호출 시, 엔티티와 스냅샷을 비교하여 변경이 있을 경우 쓰기 지연 저장소에 update 쿼리를 추가하고 flush()를 실행한다.
5) 지연 로딩
JPA에서 테이블 간 연관 관계는 객체의 참조를 통해 이루어지는데 이는 하나의 객체를 조회하려다가 해당 객체가 참조하는 N개의 객체를 전부 조회하게 되는(N+1) 등의 문제를 발생시킬 수 있다. 이를 방지하기 위해 엔티티가 실제 사용되기 전까지 DB 조회를 지연하는 지연 로딩(Lazy Loading) 전략을 지원한다.
JPQL
JPQL은 객체 지향 쿼리를 작성하기 위해 JPA가 제공하는 문법으로 SQL을 추상화하여 특정 데이터베이스에 의존하지 않는다.
JPQL은 SQL을 바로 실행하기 때문에 영속 엔티티가 DB 동기화 되지 않아 의도하지 않은 결과가 반환될 수 있어 JPA는 쿼리를 실행할 때 플러시를 자동 호출하는 것을 기본값으로 사용한다.
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
// flush 호출이 안 됐다고 하면 memberA,B,C가 없는 상태인데 의도한 것은 memberA,B,C가 포함된 리스트
List<Member> members= query.getResultList();
JPQL는 조회 시 영속성 컨텍스트가 아닌 데이터베이스에 우선적으로 조회를 하고 반환값을 1차 캐시에 저장하는데 이때 해당 엔티티가 이미 1차 캐시에 존재하는 경우 반환값은 버리고 1차 캐시의 값을 반환한다.
이는 JPQL 조회 기능의 트랜잭션 격리 수준이 REPEATABLE READ이기 때문인데 이 단계에서는 하나의 트랜잭션 내에서 같은 SELECT 쿼리를 계속 실행하면 항상 같은 결과가 반환되어야 한다. (트랜잭션 격리 수준이란 동시에 여러 트랜잭션이 처리될 때, 특정 트랜잭션이 변경, 조회중인 데이터를 다른 트랜잭션에서 어느정도까지 접근 가능한지 레벨을 나눈 것) REPEATABLE READ는 MySQL의 InnoDB 스토리지 엔진에서 기본적으로 사용되는 격리수준인데 이는 트랜잭션의 롤백 가능성에 대비해 변경 전 레코드를 UNDO 영역에 백업해두고 실제 레코드 값을 변경하는 MVCC 방식을 사용한다.
모든 InnoDB 트랜잭션은 순차적으로 증가하는 고유한 트랜잭션 번호를 가지며 UNDO 영역에 백업된 레코드에는 변경을 발생시킨 트랜잭션의 번호가 저장되어 있다. 그래서 이 번호를 보고 어떤 데이터를 보여줄지 결정하게 되는데 트랜잭션 10번이 조회한 값을 트랜잭션 12번이 변경하고 커밋한 경우 트랜잭션 10번은 다시 이 값을 조회하면 바뀐 값이 아닌 변경 전 UNDO 영역의 값이 보여지는 것이다. (더티 리드 방지)
플러시는 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송하여 영속성 컨텍스트의 변경 내용을DB에 동기화하는데 직접 수동으로 호출하거나 트랜젝션 커밋, JPQL 쿼리 실행에 의해 자동 호출이 된다. (JPA가 JPQL 쿼리 실행시 flush를 자동 호출하는 이유는 JPQL은 우선적으로 데이터베이스에서 조회를 하기 때문에 영속 상태의 엔티티가 조회가 안 되는 )
변경 감지를 통해 1차 캐시 엔티티의 변경 사항(스냅샷을 통해 비교)이 있을 경우 update 쿼리를 flush() 직전에 생성해주기 때문에 사용자는 UPDATE가 필요한 경우 엔티티의 값만 바꿔주면 된다. 영속성 컨텍스트를 비우는 것이 아니며 트랜젝션이라는 작업 단위가 중요하다.