JPA는 Java 진영에서 사용하는 ORM(Object-Relational Mapping) 기술의 표준 사양(또는 명세, Specification)이다.
표준 사양은 Java의 인터페이스로 사양이 정의되어 있기 때문에 JPA라는 표준 사양을 구현한 구현체는 따로 있다는 것을 의미한다.
JPA 표준 사양을 구현한 구현체로 Hibernate ORM, EclipseLink, DataNucleus 등이 있다.
이 Hibernate ORM은 JPA에서 정의해둔 인터페이스를 구현한 구현체로써 JPA에서 지원하는 기능 이외에 Hibernate 자체적으로 사용할 수 있는 API 역시 지원하고 있다.
데이터 액세스 계층에서 JPA는 계층의 상단에 위치한다.
데이터 저장, 조회 등의 작업은 JPA를 거쳐 JPA의 구현체인 Hibernate ORM을 통해서 이루어지며 Hibernate ORM은 내부적으로 JDBC API를 이용해서 데이터베이스에 접근하게 된다.
Persistence는 영속성, 지속성이라는 뜻을 가지고 있다. 즉, 무언가를 금방 사라지지 않고 오래 지속되게 한다는 것이 Persistence의 목적이다.
ORM은 객체(Object)와 데이터베이스 테이블의 매핑을 통해 엔티티 클래스 객체 안에 포함된 정보를 테이블에 저장하는 기술이다.
JPA에서는 테이블과 매핑되는 엔티티 객체 정보를 영속성 컨텍스트(Persistence Context)라는 곳에 보관해서 애플리케이션 내에서 오래 지속되도록 한다.
이렇게 보관된 엔티티 정보는 데이터베이스 테이블에 데이터를 저장, 수정, 조회, 삭제하는데 사용된다.
위 그림과 같이 영속성 컨텍스트에는 1차 캐시라는 영역과 쓰기 지연 SQL 저장소라는 영역이 있다.
JPA API 중에서 엔티티 정보를 영속성 컨텍스트에 저장하는 API를 사용하면 영속성 컨텍스트의 1차 캐시에 엔티티 정보가 저장된다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // (1)
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
jpa:
hibernate:
ddl-auto: create # (1) 스키마 자동 생성
show-sql: true # (2) SQL 쿼리 출력
@Configuration
애너테이션을 추가하면, Spring에서 Bean 검색 대상인 Configuration 클래스로 간주한다.@Bean
애너테이션이 추가된 메서드를 검색한 후, 해당 메서드에서 리턴하는 객체를 Spring Bean으로 추가해준다.import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
// (1)
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
// (2)
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
(3)
};
}
}
@Entity
와 @ID
애너테이션을 추가하면 JPA에서 해당 클래스를 엔티티 클래스로 인식한다.@GeneratedValue
애너테이션은 식별자를 생성해주는 전략을 지정할 때 사용한다.import lombok.Getter;
import javax.persistence.*;
@Getter
@Setter
@NoArgsConstructor
@Entity // (1)
public class Member {
@Id // (2)
@GeneratedValue // (3)
private Long memberId;
private String email;
public Member(String email) {
this.email = email;
}
}
EntityManager
를 통해서 Transaction
객체를 얻는다. JPA에서는 이 Transaction
객체를 기준으로 데이터베이스의 테이블에 데이터를 저장한다.Transaction
을 시작하기 위해서 tx.begin()
메서드를 먼저 호출해야한다.member
객체를 영속성 컨텍스트에 저장한다.tx.commit()
을 호출하는 시점에 영속성 컨텍스트에 저장되어 있는 member
객체를 데이터베이스의 테이블에 저장한다.em.find(Member.class, 1L)
을 호출하면 (3)에서 영속성 컨텍스트에 저장한 member
객체를 1차 캐시에서 조회한다.em.find(Member.class, 2L)
를 호출해서 식별자 값이 2L인 member
객체를 조회한다. 하지만 영속성 컨텍스트에는 식별자 값이 2L인 member 객체는 존재하지 않기 때문에 (7)의 결과는 true가 된다.import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
// (1)
this.tx = em.getTransaction();
return args -> {
example02();
};
}
private void example02() {
// (2)
tx.begin();
Member member = new Member("hgd@gmail.com");
// (3)
em.persist(member);
// (4)
tx.commit();
// (5)
Member resultMember1 = em.find(Member.class, 1L);
System.out.println("Id: " + resultMember1.getMemberId() + ", email: " + resultMember1.getEmail());
// (6)
Member resultMember2 = em.find(Member.class, 2L);
// (7)
System.out.println(resultMember2 == null);
}
}
위 그림은 코드 실행 시 영속성 컨텍스트의 상태이다.
tx.commit()
을 했기 때문에 member
에 대한 INSERT 쿼리는 실행되어 쓰기 지연 SQL 저장소에서 사라진다.
실행 결과를 보면 (1)에서 SELECT 쿼리가 실행된 것을 볼 수 있다.
이 SELECT 쿼리를 통해 (6)에서 em.find(Member.class, 2L)
로 조회를 했는데 식별자 값이 2L에 해당하는 member2
객체가 영속성 컨텍스트의 1차 캐시에 없기 때문에 추가적으로 한번 더 조회한다.
Hibernate: drop table if exists member CASCADE
Hibernate: drop table if exists orders CASCADE
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table member (member_id bigint not null, email varchar(255),
primary key (member_id))
Hibernate: create table orders (order_id bigint not null, created_at timestamp,
primary key (order_id))
Hibernate: call next value for hibernate_sequence
Hibernate: insert into member (email, member_id) values (?, ?)
Id: 1, email: hgd@gmail.com
// (1)
**Hibernate: select member0_.member_id as member_i1_0_0_,
member0_.email as email2_0_0_ from member member0_ where member0_.member_id=?**
true
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
example03();
};
}
private void example03() {
tx.begin();
Member member1 = new Member("hgd1@gmail.com");
Member member2 = new Member("hgd2@gmail.com");
em.persist(member1); // (1)
em.persist(member2); // (2)
tx.commit(); // (3)
}
}
위 그림은 tx.commit()
이 실행되기 직전의 영속성 컨텍스트 상태를 표현한 것이다.
tx.commit()
을 하기 전까지는 em.persist()
를 통해 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리가 실행이 되지 않는다.
따라서 테이블에 데이터가 저장되지 않는다.
위 그림은 tx.commit()
이 실행된 직후의 영속성 컨텍스트 상태를 표현한 것이다.
tx.commit()
이 실행된 이후에는 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리가 모두 실행되고 실행된 쿼리는 제거된다.
따라서 테이블에 데이터가 저장된다.
코드 실행 결과 (1)과 같이 쓰기 지연 SQL 저장소에 저장된 INSERT 쿼리가 실행된 것을 확인할 수 있다.
Hibernate: drop table if exists member CASCADE
Hibernate: drop table if exists orders CASCADE
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table member (member_id bigint not null, email varchar(255), primary key (member_id))
Hibernate: create table orders (order_id bigint not null, created_at timestamp, primary key (order_id))
Hibernate: call next value for hibernate_sequence
Hibernate: call next value for hibernate_sequence
// (1)
Hibernate: insert into member (email, member_id) values (?, ?)
Hibernate: insert into member (email, member_id) values (?, ?)
tx.commit()
을 호출해서 영속성 컨텍스트의 쓰지 지연 SQL 저장소에 등록된 INSERT 쿼리를 실행한다.tx.commit()
을 실행하면 쓰기 지연 SQL 저장소에 등록된 UPDATE 쿼리가 실행된다.tx.commit()
을 하면 변경된 엔티티와 이 전에 이미 떠 놓은 스냅샷을 비교한 후, 변경된 값이 있으면 쓰기 지연 SQL 저장소에 UPDATE 쿼리를 등록하고 UPDATE 쿼리를 실행한다.import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
example04();
};
}
private void example04() {
tx.begin();
em.persist(new Member("hgd1@gmail.com")); // (1)
tx.commit(); // (2)
tx.begin();
Member member1 = em.find(Member.class, 1L); // (3)
member1.setEmail("hgd1@yahoo.co.kr"); // (4)
tx.commit(); // (5)
}
}
tx.commit()
을 호출해서 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리를 실행한다.em.remove(member)
을 통해 영속성 컨텍스트의 1차 캐시에 있는 엔티티의 제거를 요청한다.tx.commit()
을 실행하면 영속성 컨텍스트의 1차 캐시에 있는 엔티티를 제거하고, 쓰기 지연 SQL 저장소에 등록된 DELETE 쿼리가 실행된다.EntityManager
의 flush()
APItx.commit()
메서드가 호출되면 JPA 내부적으로 em.flush()
메서드가 호출되어 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
example05();
};
}
private void example05() {
tx.begin();
em.persist(new Member("hgd1@gmail.com")); // (1)
tx.commit(); //(2)
tx.begin();
Member member = em.find(Member.class, 1L); // (3)
em.remove(member); // (4)
tx.commit(); // (5)
}
}