1. 데이터베이스 Dialect
1-1. 데이터베이스 Dialect(Database dialect)
(1) JPA는 특정 데이터베이스에 종속적이지 않도록 설계되었다.
(2) 각각의 관계형 데이터베이스는 SQL syntax와 제공되는 관련 함수들이 조금씩 상이하다.
(3) 데이터베이스 Dialect는 위의 (2)번과 같은 특징으로 SQL 표준을 지키지 않는 특정 데이터베이스만의 고유한 성질이나 기능을 말한다.
(4) JPA 구현체인 Hibernate는 40가지 이상의 데이터베이스 Dialect를 지원하고 있다.
(5) javax.persistence.~ :
- Hibernate 라이브러리에 종속적이지 않은 다른 JPA 구현체 라이브러리들을 사용할 수 있다.(표준을 지킨 property로 볼 수 있음)
2. EntityManagerFactory & EntityManager 요약
2-1. EntityManagerFactory 구동 방식
(1) Persistence 클래스에서 시작된다.
(2) persistence.xml(빌드 도구가 Maven인 경우 설정 파일)을 통해 EntityManagerFactory라는 클래스를 생성하고 EntityManagerFactory에서 클라이언트의 요청이 올 때마다 EntityManager를 생성한다.
(3) EntityManagerFactory를 생성하는 코드
EntityManagerFactory emf = Persistence.createEntityManagerFactory(persistenceUnitName);
- EntityManagerFactory를 사용하고 나면 close()로 자원을 반환해서 닫아준다.
(4) EntityManagerFactory는 애플리케이션 로딩 시점에 1개만 생성해야 한다
(하나만 생성하고 애플리케이션 전체에서 공유한다.)
2-2. EntityManager
(1) EntityManager 생성 코드
EntityManager em = emf.createEntityManager();
(2) 주요 특징으로 EntityManager는 스레드 간 공유하지 않는다. (사용 후 반납)
(3) 각 Transaction 단위마다 EntityManager를 생성한다.
(JPA의 모든 데이터 수정은 Transaction 내부에서 진행된다).
(4) EntityManager는 내부적으로 데이터베이스 커넥션을 사용해서 데이터베이스와 통신한다.
(5) EntityManager의 Transaction 시작
- JPA의 모든 작업 처리 단위는 Transaction 내부에서 시작되어야 함.
EntityManager tx = em.getTransaction();
tx.begin();
(6) Transaction commit
tx.commit();
3. JPQL(Java Persistence Query Language)
3-1. JPQL이란?
(1) 자바에서 사용하는 객체지향 쿼리 언어(Object Oriented Query Language)를 말한다.
(2) JPA는 JPQL을 다룰 때 관련된 엔티티를 다루는 코드를 작성하게 된다.
(데이터베이스 테이블과 직접적으로 연관지어 작성하지 않는다)
(3) 예시 코드
List<Member> result = em.createQuery("select m from member as m", Member.class)
.setFirstResult(5)
.getFinalResults(8)
.getResultList();
(4) JPQL의 경우 persistence.xml 또는 build.gradle에서 작성한 Database dialect를 바탕으로 JPQL를 해석해서 관련된 RDBMS 쿼리를 작성한다.
3-2. JPQL 특징
(1) JPA를 사용하면 JPQL를 많이 작성하게 되는데 JPA를 통해 객체 중심으로 개발을 진행할 수 있다.
(2) JPQL, 문제는 조회(쿼리) 쿼리에서 발생한다.
- 조회에서 테이블이 아닌 엔티티를 탐색한다.
- 검색의 경우 단건 조회를 제외하면 각 조건에 맞게 데이터베이스에 존재하는 데이터들을 필터링해서 가져와야 한다.
- 테이블에서 직접 가져오면 JPA의 컨셉이 깨지기 때문에 객체를 대상으로 쿼리를 짤 수 있는 문법이 필요한데 이게 바로 JPQL이다.
(3) 실제 RDB 테이블을 대상으로 쿼리를 작성하면 테이블에 종속적인 쿼리가 생성된다. JPA는 SQL를 추상화한 JPQL이라는 객체지향 쿼리를 지원하고 있다. (특정 데이터베이스에 종속적이지 않게 설계되었다.)
(4) SQL 문법과 유사하지만 JPQL은 객체를 중심으로 쿼리를 작성한다는 특징이 있다.
(5) 위에서 언급한 것처럼 dialect를 변경해도 SQL를 추상화했다는 특징 때문에 데이터베이스가 변경되어도 JPQL을 수정하지 않아도 된다.
4. JPA 영속성 컨텍스트(Persistence Context)
(1) JPA를 이해하기 위해 반드시 알아야 하는 내용
(2) JPA에서 가장 중요한 것은 2가지이다. 첫 번째는 객체와 관계형 데이터베이스를 올바르게 매핑하는 것, 두 번째는 JPA의 내부 동작 방식을 이해할 수 있는 영속성 컨텍스트를 이해하는 것이다.
(3) 영속성(Persistence)
- 우선 영속성이란 컴퓨터 과학(Computer Science)에서 데이터를 유지할 수 있는 애플리케이션이 종료되어도 데이터를 보존할 수 있는 자체적인 성질이나 특성을 의미하고 있다.
4-1. 영속성 컨텍스트의 정의
(1) 영속성 컨텍스트(Persistence context)란, JPA에서 객체를 다루는 단위인 Entity(엔티티)를 영구 저장할 수 있는 환경을 의미한다.
(2) 영속성 컨텍스트는 가시적이지 않은 추상화된 논리적 개념으로 위에서 언급된 EntityManager를 통해 영속성 컨텍스트에 접근할 수 있다.
(3) EntityManager와 영속성 컨텍스트는 N:1 관계에 있다.
EntityManager.persist(entity);
(4) 위의 코드는 영속성 컨텍스트를 통해 Entity를 영속화하는 것을 말하며 엔티티를 영속성 컨텍스트에 저장하는 것이다.
4-2. JPA에서 관리되는 Entity의 생명 주기(Life-cycle)
(1) 비영속 상태(New / Transient status)
// 객체 생성 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
- 영속성 컨텍스트와 전혀 관계없는 새로운 상태를 의미한다.
- 단순히 객체만 생성된 상태로도 볼 수 있다.
(2) 영속 상태(Managed)
// 객체 생성 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// 객체를 저장한 상태(영속)
// member 엔티티를 영속성 컨텍스트에 영속화한다.
em.persist(member);
- 영속성 컨텍스트에 의해 관리되는 상태
- 객체를 생성한 뒤 EntityManager 내부 영속성 컨텍스트에 객체를 저장해서 영속성 컨텍스트에 관리되는 상태를 말함
(3) 준영속 상태(Detached)
em.detach(member);
- 영속성 컨텍스트에 저장되었다가 분리된 상태
(4) 삭제(Removed)
- 객체 자체를 삭제한 상태
5. 영속성 컨텍스트의 이점
5-1. 엔티티 조회, 영속화된 엔티티의 경우 1차 캐시에서 바로 조회한다.
(1) 조회 시 이점을 가질 수 있는데 JPA는 우선 영속성 컨텍스트에서 1차 캐시를 탐색해서 캐시에 엔티티가 존재한다면 캐시에서 해당 엔티티를 조회하게 된다.
(2) 1차 캐시에 없는 엔티티를 조회할 때는 우선 데이터베이스에서 조회하고 조회한 엔티티를 1차 캐시에 저장해서 이후에 필요할 때 1차 캐시에서 엔티티를 불러온다.
(3) EntityManager는 Transaction 단위로 생성되고 종료된다. 요청에 대한 비즈니스가 완료되면 영속성 컨텍스트를 완전히 지움과 동시에 1차 캐시도 같이 지워버리게 되고 1차 캐시의 경우 데이터베이스 Transaction 내부에서만 의미가 있기 때문에 조회 성능에서 극적인 성능 향상을 기대하기는 어렵다.
5-2. 영속화된 엔티티의 동일성 보장
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);
System.out.println("result = " + (findMember1 == findMember2)); // true
(1) JPA가 동일한 Transaction 내부에서 영속 엔티티가 서로 동일함을 보장해준다.
(2) 1차 캐시로 반복 가능한 읽기(Repeatable read) 등급의 Transaction isolation 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
(3) 동일성이 보장되는 이유는 간단히 말하면, 1차 캐시가 존재하기 때문이다.
5-3. Transaction을 지원하는 쓰기 지연(Transactional write-behind)
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 함
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL를 보내지 않음
// 커밋 순간 DB에 쿼리를 보냄
transaction.commit();
(1) 위의 코드처럼 persist()를 통해 memberA를 1차 캐시에 저장한 후 INSERT SQL를 생성해서 쓰기 지연 보관소에 저장한다. memberB에 대한 persist도 마찬가지이다.
(2) 위의 그림처럼 쓰기 지연 저장소에 보관되어 있던 쿼리들은 트랜잭션을 커밋하는 순간 Flush되어 데이터베이스로 실제 쿼리를 전송하게 된다.
(3) 영속화 시점에 바로 쿼리를 날리지 않고 트랜잭션 커밋 시점에 쿼리를 전송함으로써 성능적인 측면에서 이득을 볼 수 있다.
5-4. 엔티티 수정 발생 - 변경 감지(Dirty Checking)
(1) 아래와 같은 코드를 살펴보자.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);
// em.update(member)와 같은 쿼리가 있어야 하지 않을까?
transaction.commit();
- 위와 같이 엔티티 내부의 데이터를 변경했다. 그런데 update와 같은 수정된 내용을 반영하는 메서드는 보이지 않는다. "변경된 내용을 반영할 수 있도록 관련된 메서드를 작성해줘야 하지 않을까?" 라는 생각을 할 수 있지만 사실은 그렇지 않다.
(2) JPA에서는 변경 감지(Dirty checking)를 통해 엔티티의 값을 수정할 수 있다.
(3) JPA는 데이터베이스 트랜잭션을 커밋하는 시점에 내부적으로 플러시(Flush)가 호출된다. Flush를 통해 내용이 변경된 엔티티와 스냅샷을 비교한다.
(4) 스냅샷(Snapshot)은 1차 캐시로 엔티티가 들어온 시점에서의 값과 상태 정보들을 저장한 정보라고 보면 된다. 데이터베이스 트랜잭션 커밋 시점에 내부적으로 플러시가 호출되면서 JPA가 엔티티와 스냅샷을 비교해서 변경된 사항이 있다면 쓰기 지연 저장소에 UPDATE SQL를 작성해두고 데이터베이스에 해당 SQL를 보내고 커밋하게 된다.
(5) 이러한 매커니즘을 변경 감지(Dirty checking)이라고 한다.
5-5. JPA 플러시(Flush)
(1) 위에서 언급되었지만 우선 플러시(Flush)란, 영속성 컨텍스트에서 발생한 변경 내용을 실제 데이터베이스에 반영하는 작업을 의미한다.
(2) 데이터베이스에 실제로 동기화 작업을 해 주는 과정이라고 생각할 수 있다.
(영속성 컨텍스트에 존재하는 쓰기 지연 SQL 저장소에 적재된 쿼리들이 실제 데이터베이스에 반영되는 것)
(3) 플러시가 발생하면 내부적으로 아래와 같은 작업이 진행된다.
- 변경 감지 발생
- 수정된 엔티티에 대한 쿼리가 쓰기 지연 SQL 저장소에 보관됨
- 쓰기 지연 저장소에 적재된 쿼리를 실제 데이터베이스에 전송(등록, 수정, 삭제 쿼리)
(4) 영속성 컨텍스트를 Flush하는 방법
- em.flush() : 플러시를 직접적으로 호출하는 것
- Transaction commit : 트랜잭션 커밋 시 자동으로 플러시가 호출됨
- JPQL 쿼리를 실행하는 경우 : 자동으로 플러시가 호출된다.
- 트랜잭션이라는 특정 작업에 대한 단위가 중요하다. JPA는 기본적으로 데이터를 동기화하는 등 동시성에 대한 문제는 트랜잭션 단위에 위임해서 진행하게 된다.
5-6. JPQL 쿼리와 플러시
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
(1) JPQL 특성상 SQL로 바로 번역되어 실행된다. JPA는 기본적으로 JPQL를 실행하는 순간 플러시를 무조건 실행하고 쿼리를 전송한다.
5-7. 준영속 상태(Detached state)
(1) 영속 상태(1차 캐시에 올라간 상태, JPA가 직접적으로 관리하는 상태를 말함)에서 준영속 상태로 변경될 수 있다.
(2) 준영속 상태란 영속 상태의 엔티티가 영속성 컨텍스트에서 분리되는 것을 말하며 이때 영속성 컨텍스트가 제공하는 기능을 사용할 수 없게 된다.
(3) 아래와 같이 3가지 방법으로 준영속 상태로 변환이 가능하다.
- em.detach(entity) : 특정 엔티티에 대해 준영속 상태로 변환
- em.clear() : 영속성 컨텍스트를 완전히 초기화한다.
- em.close() : 영속성 컨텍스트를 종료한다.
6. Reference
※ 해당 포스팅은 InFlearn에서 (전) 우아한형제들, 배달의 민족 서비스 개발팀장(기술이사)으로 재직하셨던 김영한님의 "자바 ORM 표준 JPA 프로그래밍(기본편)" 강의를 듣고 공부한 내용을 정리하였습니다.
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
※ 해당 포스팅에 대해 내용 추가가 필요하다고 생각되면 기존 포스팅 내용에 다른 내용이 추가될 수 있습니다.
개인적으로 공부하며 정리한 내용이기에 오타나 틀린 부분이 있을 수 있으며, 이에 대해 댓글로 알려주시면 감사하겠습니다!
'백엔드(Back-End) > JPA(Java Persistence API)' 카테고리의 다른 글
[Spring Data JPA] - 쿼리 메서드 기능, JPA NamedQuery, Repository 사용자 정의 쿼리 메서드 (0) | 2023.10.11 |
---|---|
[Spring Data JPA] - 공통 인터페이스(Common Interface) 기능, 스프링 데이터 JPA 상속 계층도 (2) | 2023.10.09 |
[Spring Data JPA] - Spring Data, Spring Data JPA는 무엇일까? (0) | 2023.10.06 |
[JPA] - 객체와 테이블 매핑, 필드와 컬럼 매핑, 기본 키 매핑 (0) | 2023.10.05 |
[JPA] - JPA란 무엇일까? JPA를 통해 해결할 수 있는 문제들 (0) | 2023.09.19 |
댓글