728x90
JPA ?
- Java Persistence API 의 약자로 관계형 데이터베이스를 사용하기 위한 양식이다.
- Java 객체와 관계형 DB 사이를 매핑하는 ORM 기술 표준
- 인터페이스이므로 실제 구현체는 Hibernate 등등
- SQL 코드를 구현하지 않고, 객체 지향 프로그래밍 방식으로 DB를 사용
- Java 에서 애플리케이션과 JDBC 사이에서 동작한다.
Hibernate
- JPA의 구현체이다. 실제로 JPA의 핵심체인
EntityManagerFactory
,EntityManager
,EntityTransaction
을 Hibernate에서는SessionFactory
,Session
,Transaction
으로 상속 받고 각각 Impl로 구현하고 있다.
- 중간에 ORM이 JDBC API를 직접 사용하여 DB에 접근하는 방식
장점
- 생산성 향상 : 반복적인 SQL 코드 구현, CRUD 작업을 알아서 해준다.
- 유지보수 : 객체 수정에 따른 SQL 코드수정 작업이 불필요
- 데이터 관계 매핑 : 객체와 관계형 데이터를 매핑하는 과정을 자동화
- 성능 : 캐싱을 지원하여 수행한 결과를 빠르게 도출
- 데이터 접근 및 추상화 및 벤더 독립 : 데이터의 관례를 매핑하는 과정을 DB마다 다른데, 이러한 작업의 번거로움이 없다.
그러나
- JPA는 데이터를 처리하기 위해 Entity의 생명주기, 캐시, 쓰기 지연, 상속 전력, 컬렉션 매퍼 등 DB를 사용할 수 있는 다양한 옵션들이 존재한다.
- 이들의 사용에 따라서 성능에 많은 영향을 주고, 오류 발생 등이 갈리기 때문에 SQL 코드를 사용하지않는다는 점에서는 러닝 커브가 낮아보이지만 여러가지 상황을 고려한다면 결코 낮은 러닝 커브를 가진 것은 아니다.
Spring Data JPA
- JPA를 Spring Framewrok에서 편하게 사용할 수 있도록 만들어놓은 모듈이다.
- 평소 REST API 개발 시,
@Entity
,@Column
등을 가지고 엔티티를 정의하고 DDL을 수행하는 등의 작업을 한게 Spring Data JPA 이다.
- 어플리케이션에서 JpaRepository를 사용하여 JPA의 구현체인 Hibernate로 요청을 전달하고, Hibernate는 이에 맞게 JDBC API를 사용하여 DB에 접근하고 그 결과를 애플리케이션에 돌려주는 방식이다.
JPA의 영속선 컨텍스트, 생명주기
생명주기 (Entity의 Lifecycle)
- 객체를 생성하는 것부터 어떤식으로 DB에 들어가는지 순의 생명주기에 대해서
- 비영속 (new/transient)
- 영속성 컨텍스트에 들어가기 전 상태
Cafe cafe = new Cafe();
cafe.setName("Americano");
cafe.setPrice(3000);
→ DB에 아무런 영향이 가지 않는다.
- 영속 (managed)
- 영속성 컨텍스트에 저장된 상태
- Entity가 영속성 컨텍스트에 의해 관리되지만 DB에는 영향이 없다.
- 컨텍스트에 미리 객체에 대한 정보를 DB 형태로 저장
- Transaction과 관련된 commit 메서드가 호출되었을 때 DB에 적용
EntityManager em = em.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태
em.persist(cafe);
→ JPA는 기본적으로 캐시를 사용한다. DB에서 객체로 가져올 때는 캐시에 저장되며 차후 다시 호출될 때 1차 캐시에 이미 불러왔던 내용이 있다면, 캐시를 사용해서 불러오게 된다. 이 때 영속성 컨텍스트를 사용하며 DB에 저장할 때 커밋 메서드가 호출되지 전까지는 영속성 컨텍스트가 그 정보를 가지고만 있는 상태가 된다.
- 준영속(detached)
- 영속성 컨텍스트에서 분리된 상태
//카페 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(cafe);
→ 더 이상 해당 객체에 대한 변경 사항이 없을 경우에 EntityManager에서 준영속 상태로 돌려야 한다.
- 삭제 (Removed)
- DB에서 삭제된 상태
//객체를 삭제
em.remove(cafe);
→ 삭제는 DB에도 반영이 된다. 물론 remove 이후에 다시 영속 후, commit을 올리면 그 정보가 다시돌아온다. 하지만, 준영속이랑 관계 없이 삭제를 하게 된다면 DB에 반영되므로 사용에 유의해야한다.
영속성 컨텍스트
- Entity를 영구적으로 저장하는 환경
- DB에 있는 데이터가 읽히고 쓰는 과정
- 커넥션 생성 → 커넥션 연결 → Statement 생성 → Statement 실행 → Transaction 커밋 → 커넥션 연결 끊기
- ORM에서는 SQL 쿼리를 사용하지 않고 객체 지향적으로 처리한다고 생각하면, 먼저 객체를 생성하고 그 다음 DB처리가 이루어져야 한다.
- 영속성 컨텍스트는 DB 처리 이전에 객체를 먼저 생성하는 단계이다.
- 논리적인 개념에 속하며 Spring에서는 EntityManager를 사용한다.
- 하지만 영속선 컨텍스트는 Java에서 무엇을 사용하냐에 따라 사용가능한 것이 제한되어 있다.
- J2SE의 경우, EntityManager와 영속성 컨텍스트가 1:1 관계를 갖는다.
- Spring의 경우, 같은 트랜잭션 범위에 있는 EntityManager는 동일한 영속성 컨텍스트를 사용한다.
- 즉, Spring의 IoC 컨테이너의 기능인 의존성 주입처럼 재활용한다는 것이다.
영속성 컨텍스트의 이점
1. Cache
- 영속성 컨텍스트에는 1차 캐시라는 것이 있다. Entity가 DB에 저장되기 전에 사용되는 공간인데, 반대로 DB를 조회하고 날 때에도 저장하게 된다.
Cafe americano = new Cafe();
cafe.setName("Americano");
cafe.setPrice(3000);
Cafe espresso = new Cafe();
cafe.setName("Espresso");
cafe.setPrice(3000);
Cafe cafuchino = new Cafe();
cafe.setName("Cafuchino");
cafe.setPrice(4000);
// 캐시에 저장됨
em.persist(americano);
em.persist(espresso);
em.persist(cafuchino);
// 캐시에서 조회
em.find(Cafe.class, 1);
- 영속성 컨텍스트는 2차 캐시까지 존재하지만 독립적으로 사용되는 것은 1차 캐시이며, 2차 캐시의 경우에 애플리케이션 전체 Entity에 사용하는 캐시이다.
- 따라서 애플리케이션이 종료될 때까지 2차 캐시는 존재하지만 Entity의 Transcation Thread가 종료되는 경우, 1차 캐시는 삭제된다.
- 즉, 트랜잭션 범위 안에서만 사용하는 짧은 캐시이다.
- DB에서 조회할 때
- 1차 캐시를 먼저 조회한 후, 있으면 1차 캐시의 내용을 불러온다.
- 1차 캐시에서 Entity를 찾지 못할 경우, 2차 캐시에서 조회하며 여기에도 없다면, DB에서 조회한다.
- DB에서 조회한 Entity를 1차 캐시와 2차 캐시에 저장한다.
- 조회한 Entity를 반환한다.
- DB에 저장할 때
- 객체로 만든 Entity를 영속 상태로 전환하면 1차 캐시와 2차 캐시에 저장된다. (DB에 없음)
- 1차 캐시에 저장된 Entity는 트랜잭션 커밋 메서드를 통해 DB로 저장된다.
- 작업이 끝났으면 준영속 상태로 전환된다.
- 1차 캐시에 저장됨으로써 중간에 수정할 사항이 있다면 UPDATE 쿼리를 사용하지 않고도 INSERT 쿼리만으로 바로 저장할 수 있다.
- 1차 캐시
- Entity마다 독립된 형태로 사용하면 만약, 1000명의 사용자가 DB 요청이 오면 EntityManager가 100개 생성되며 Thread가 종료되면 모두 소멸된다.
- 2차 캐시
- Application 전체에서 사용하는 캐시로 애플리케이션이 종료될 때까지 계속 유지되는 캐시
- JPA 2.0 에서부터 표준이 되었으며 Hibername의 2차 캐시와 동일하게 동작한다.
2. 동일성 보장
- 캐시에 보관하고 있는 데이터와 DB에 있는 데이터가 100% 동일하는 것을 보장받을 수 있다.
- 트랜잭션 내부에서
persist
메서드가 호출되면, Entity 들을 1차 캐시에 생성하고, 논리적으로 구현되어있는 Write-behind SQL 저장소에 INSERT 쿼리 등의 DML 쿼리를 생성하며 쌓아둔다.
- 최종적으로
commit()
메서드가 호출될 때 저장소에 있는 모든 쿼리가 DB로 전달되며, 이 때부터 DB에 내용이 반영된다.,
- 그러나 여기에서
commit()
메서드 내에는flush()
메서드를 같이 호출한다.flush()
메서드는 Write-behind SQL 저장소에 있는 모든 쿼리를 DB에 저장하는 메서드 이속, 그 내용을 반영하는 것이commit()
메서드이다.
- 실제 트랜잭션 내의 commit은 이 2가지 기능을 수행한다는 점을 인지해야한다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin();
// 트랜잭션 시작
em.persist(americano);
// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
// 이 때 flush()와 commit()이 같이 실행됨
transaction.commit();
- SQL 저장소에 최대 쌓일 수 있는 SQL갯수는 application.yml 파일 등을 통해서 hibernate batch size를 조절할 수 있다.
spring.jpa.properties.hibernate.jdbc.batch_size=20
3. 엔티티 수정 시 작동하는 Dirty Checking
- Entity를 수정해야 할 경우에는 단순히 아래와 같이 코드를 짤 수 있다.
// EntityManager 생성
EntityManager em = emf.createEntityManager();
// 영속 Entity 조회
Cafe cafe = em.find(Cafe.class, 1);
// 영속 Entity 수정
cafe.setName("iceAmericano");
// 트랜잭션 생성 및 커밋
EntityTransaction transaction = em.getTransaction();
transaction.begin();
transaction.commit();
- 이 경우 INSERT가 아닌 UPDATE 쿼리가 실행된다.
- 그 이유는 JPA 캐시에 있는 스냅샷 때문이다.
- 1차 캐시에 저장할 때 ID와 Entity, 그리고 Snapshot이 저장되는데,
commit()
또는flush()
가 발생했을 때 Snapshot을 비교하면 변경 사항일 경우에는 UPDATE 쿼리를 만들어준다.
- 이러한 변경을 감지해주는 것이 DirtyChecking이다.
@DynamicUpdate
를 이용하면 변경한 필드만 UDPATE 쿼리를 작성한다.
출처
'Spring' 카테고리의 다른 글
DBMS의 트랜잭션과 @Transactional (1) | 2022.09.23 |
---|---|
Sring Boot + Redis를 이용한 Cachcing (0) | 2022.04.10 |
[JPA] 더티 체킹 (Dirty Checking) (0) | 2022.04.08 |
[JPA] Bulk Insert 적용기 (0) | 2022.01.06 |