인프런 김영한 강사님 강의를 듣고 정리한 내용입니다.
Java Persistence Api
자바 진영의 ORM 기술 표준이다.
Object-Relational Mapping
JPA는 애플리케이션과 JDBC 사이에서 동작한다.
JPA 구현체로는 하이버네이트, EclipseLink, DataNucleus 등이 있으며, 주로 하이버네이트를 사용한다.
엔티티는 JPA가 관리할 객체이다.
클래스 위에 @Entity
를 붙여서 엔티티로 선언하며, 반드시 primary key가 필요하다. 그래서 pk로 선언하는 필드 위에 @Id
를 붙인다. 만약 surrogate key로써 DB가 생성해주는 값을 사용하고 싶다면, pk 필드 위에 @Generated
를 함께 붙여준다.
엔티티 매니저를 생성하고 관리한다. 엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유하도록 한다. 스프링 사용 시에 엔티티 매니저 팩토리는 자동으로 빈으로 등록되고, 엔티티 매니저의 의존관계 주입에서 스프링이 관리하여 엔티티를 생성해준다.
엔티티를 관리한다. 엔티티 매니저는 스레드간 공유를 해서는 안되며, 하나의 스레드에서 사용 후 버려야한다. 또한 JPA의 모든 데이터 변경은 트랜잭션 안에서 실행된다. 엔티티 매니저는 앞서 말한 것과 같이 final로 선언하면 스프링이 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성하고, 이를 생성자를 통해 DI 해준다.
JPA에서 사용하는 객체 지향 쿼리 언어이다. SQL을 추상화한 언어로 SQL과 문법이 유사하다. 하지만 SQL과 달리 엔티티 객체를 대상으로 쿼리를 해야한다. (SQL은 DB 테이블 대상으로 쿼리)
영속성 컨텍스트는 논리적인 개념으로, 엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있다. 영속성 컨텍스트는 엔티티를 기억하고 관리하는 역할을 한다.
영속성 컨텍스트는 엔티티를 기억하고 관리한다. 영속성 컨텍스트는 트랜잭션의 시작과 함께 초기화되고, 트랜잭션이 끝날 때 함께 사라진다.
DB에서 매번 쿼리를 해서 값을 가져오는 것은 비용이 많이 든다. 그래서 엔티티 매니저로 엔티티를 조회할 때, 영속성 컨텍스트의 1차 캐시에 조회한 엔티티의 데이터가 저장된다. 그래서 다음번에 같은 엔티티를 조회하면, 1차 캐시로부터 값을 가져올 수 있다.
데이터 조회 시, 첫번째로 영속성 컨텍스트의 1차 캐시를 확인함.
두번째로 DB에 쿼리를 해서 데이터를 읽어들임.
1차 캐시 덕분에 영속성 컨텍스트에 저장된 엔티티는 여러번 조회하더라도 항상 동일한 객체를 반환한다.
동등성 : 두 객체의 값이 같음
동일성 : 두 객체의 주소가 같음 (완전히 동일한 객체)
데이터 삽입, 삭제, 수정이 일어날 때, 매번 SQL을 수행하지 않는다.
대신 모든 트랜잭션이 끝난 후 commit될 때, 필요에 따라 쿼리를 한다.
트랜잭션이 수행되는 동안에는 영속성 컨텍스트에 값을 저장하거나 변경하여 DB에서 읽는 것처럼 사용한다.
영속성 컨텍스트에 저장된 엔티티들에 대해서 flush()
를 했을 때 Dirty Checking을 수행한다. flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것이다.
다시 말해, Dirty Checking 이란 flush()를 했을 때 1차 캐시에 저장된 값들이 변경되었을 경우, 이를 감지하여 DB의 값에 반영시키는 것이다.
Dirty Checking에 사용하기 위해서 엔티티를 최초 조회했을 때의 값을 저장한 스냅샷(복사본)을 영속성 컨텍스트에 저장해둔다.
flush()가 일어날 때마다 Dirty Checking이 수행되지만, 영속성 컨텍스트는 유지된다. (영속성 컨텍스트의 초기화는 clear()
를 통해서 할 수 있다.)
일반적인 애플리케이션에서는 트랜잭션이 끝날 때(commit 혹은 rollback),
자동으로 Dirty Checking을 하게 된다. (flush() 수동호출 안해도 됨)
혹은 JPQL 쿼리 실행 시에도, 쿼리 실행 직전에 플러시를 자동 호출한다.
연관관계에 있는 엔티티까지 조회할 필요가 없을 때 지연 로딩을 사용할 수 있다. fetch type이 lazy인 경우에 지연 로딩이 수행된다. 지연 로딩으로 설정한 경우에는 연관관계에 있는 엔티티에 실제 값이 들어가거나 null이 들어가는 대신에 프록시가 들어간다.
그리고 해당 연관관계 필드를 실제로 조회할 때(연관관계 필드의 메서드 호출) 프록시에서 타겟을 호출하여 값을 조회하게 된다. (단, 일반적인 join으로 값을 읽을 경우에는 연관관계에 있는 엔티티는 영속성 컨텍스트에 저장되지 않는다. 연관관계 엔티티까지 영속화하고 싶다면 fetch join을 사용해야한다.)
N+1 문제 참고 : https://velog.io/@suhsein/JPA-N1-%EB%AC%B8%EC%A0%9C-Fetch-join-EntityGraph
엔티티의 상태는 영속성 컨텍스트와 관련하여 크게 4가지로 나눌 수 있다.
영속 상태 엔티티와, 삭제 엔티티는 트랜잭션이 끝날 때 자동으로 수행되는 flush()의 영향을 받는다.
영속 상태 엔티티는 Dirty Checking을 하게 된다. 그리고 삭제 엔티티는 DB에서 삭제된다.
영속 상태에 있는 엔티티를 강제로 준영속 상태로 만들려면 detach()
를 사용하면 된다. 세션이 종료되는 경우(트랜잭션이 끝나서 영속성 컨텍스트가 사라짐)에는 자동으로 준영속 상태가 된다.
준영속 상태일 때는 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다. 특히 동일성 보장이 안되고, Dirty Checking 이 안된다는 점에 주의해야 한다.
영속 상태의 엔티티는 객체를 수정하는 것처럼 수정하고, Dirty Checking을 통해 반영시킬 수 있다.
준영속 엔티티를 수정하려면 두가지 방법이 있다.
두 방법 모두 준영속 엔티티를 직접 수정하는 것은 아니며, 준영속 엔티티와 pk가 같은 영속 엔티티를 조회하여 수정한다.
주의 : 준영속 엔티티와, 준영속 엔티티 pk로 조회한 영속 엔티티 사이에는 동일성이 보장되지 않는다. 영속 엔티티의 값을 수정한 후에 준영속 엔티티를 수정하더라도 둘은 다른 객체이기 때문에 영속 엔티티에는 아무런 영향을 미치지 않는다.
(준영속일 경우에는 영속성 컨텍스트의 관리를 받지 못하고, 동일성이 보장되지 않기 때문이다.)
먼저 파라미터로 수정된 값이 담긴 준영속 엔티티가 들어온다고 가정하자.
준영속 엔티티의 pk를 사용해 엔티티를 조회한다.
먼저 1차 캐시를 조회하고, 있다면 영속 엔티티를 반환한다.
없으면 DB를 조회해 영속성 컨텍스트에 저장하고, 영속 엔티티를 반환한다.
반환된 영속 엔티티를 수정한다.
파라미터로 들어온 준영속 엔티티의 값을 이용하여 setter 혹은 수정 메서드로 영속 엔티티를 수정할 수 있다. 원하는 속성(필드)만 변경이 가능하다.
트랜잭션 종료 시 Dirty Checking을 통해서 수정한 영속 엔티티의 값을 DB에 반영시킬 수 있다.
준영속 엔티티를 merge()
의 파라미터로 넘겨주고, merge를 수행한다.
다음은 merge 수행 과정이다.
준영속 엔티티의 pk를 사용해 엔티티를 조회한다.
먼저 1차 캐시를 조회하고, 있다면 영속 엔티티를 가져온다.
없으면 DB를 조회해 영속성 컨텍스트에 저장하고, 영속 엔티티를 가져온다.
파라미터로 들어온 준영속 엔티티의 모든 값을 조회된 영속 엔티티에 적용한다. 엔티티의 특정 필드만 적용되는 것이 아니라 모든 필드가 덮어 씌워진다.
merge가 완료되었고, 값이 수정된 영속 엔티티를 반환한다.
주의 : merge는 모든 필드를 덮어씌운다. 만약 필드에 값이 없다면 null로 업데이트 하는 위험이 있을 수 있다.
모든 필드를 덮어씌운다는 점이 merge와 Dirty Checking 간의 차이점이다.