JPA 기반 데이터 액세스 계층(1) - JPA란?

Backend kwon·2023년 9월 15일
0

JPA란

JPA(Java Persistence API)는 Java 진영에서 사용하는 ORM(Object-Relational Mapping) 기술의 표준 사양(또는 명세, Specification)입니다.

표준 사양(또는 명세)이라는 의미는 다시 말하면 Java의 인터페이스로 사양이 정의되어 있기 때문에 JPA라는 표준 사양을 구현한 구현체는 따로 있다는 것을 의미합니다.

 

Hibernate ORM

JPA 표준 사양을 구현한 구현체로는 Hibernate ORM, EclipseLink, DataNucleus 등이 있습니다.

Hibernate ORM은 JPA에서 정의해 둔 인터페이스를 구현한 구현체로써 JPA에서 지원하는 기능 이외에 Hibernate 자체적으로 사용할 수 있는 API 역시 지원하고 있습니다.

 

데이터 액세스 계층에서의 JPA

데이터 액세스 계층에서 JPA는 데이터 액세스 계층의 상단에 위치합니다.

데이터 저장, 조회 등의 작업은 JPA를 거쳐 JPA의 구현체인 Hibernate ORM을 통해서 이루어지며 Hibernate ORM은 내부적으로 JDBC API를 이용해서 데이터베이스에 접근하게 됩니다.

 

영속성 컨텍스트(Persistence Context)

ORM은 객체(Object)와 데이터베이스 테이블의 매핑을 통해 엔티티 클래스 객체 안에 포함된 정보를 테이블에 저장하는 기술입니다.

JPA에서는 테이블과 매핑되는 엔티티 객체 정보를 영속성 컨텍스트(Persistence Context)라는 곳에 보관해서 애플리케이션 내에서 오래 지속되도록 합니다.

그리고 이렇게 보관된 엔티티 정보는 데이터베이스 테이블에 데이터를 저장, 수정, 조회, 삭제하는 데 사용됩니다.

영속성 컨텍스트에는 1차 캐시라는 영역과 쓰기 지연 SQL 저장소라는 영역이 있습니다.

JPA API 중에서 엔티티 정보를 영속성 컨텍스트에 저장(persist)하는 API를 사용하면 영속성 컨텍스트의 1차 캐시에 엔티티 정보가 저장됩니다.

 

JPA API를 이용한 영속성 컨텍스트 이해

JPA API 사용 하기 위한 사전 준비

1. build.gradle 설정

dependencies {

	implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 
	.
    .
}

위와 같이 추가하면 기본적으로 Spring Data JPA 기술을 포함해서 JPA API를 사용할 수 있습니다.

 
2. JPA설정(application.yml)

spring:
  h2:
    console:
      enabled: true
      path: /h2     
  datasource:
    url: jdbc:h2:mem:test
  jpa:
    hibernate:
      ddl-auto: create  # (1) 스키마 자동 생성
    show-sql: true      # (2) SQL 쿼리 출력

(1)과 같이 설정을 추가해 주면 우리가 JPA에서 사용하는 엔티티 클래스를 정의하고 애플리케이션 실행 시, 이 엔티티와 매핑되는 테이블을 데이터베이스에 자동으로 생성해 줍니다.

즉, Spring Data JDBC에서는 schema.sql 파일을 이용해 테이블 생성을 위한 스키마를 직접 지정해 주어야 했지만 JPA에서는 (1)의 설정을 추가하면 JPA가 자동으로 데이터베이스에 테이블을 생성해 줍니다.

(2)와 같이 설정을 추가해 주면 JPA의 동작 과정을 이해하기 위해 JPA API를 통해서 실행되는 SQL 쿼리를 로그로 출력해 줍니다.

 
3. 영속성 컨텍스트에 엔티티 저장

@Configuration
public class JpaBasicConfig {
    private EntityManager em;
    

    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) { // (1)
        this.em = emFactory.createEntityManager();  // (2)

        return args -> {
            example01();
        };
    }

    private void example01() {
        Member member = new Member("hgd@gmail.com");
        
        // (3)
        em.persist(member);

				// (4)
        Member resultMember = em.find(Member.class, 1L);
        System.out.println("Id: " + resultMember.getMemberId() + ", email: " + 
                resultMember.getEmail());
    }
}

JPA의 영속성 컨텍스트는 EntityManager 클래스에 의해서 관리되는데 이 EntityManager 클래스의 객체는 (1)과 같이 EntityManagerFactory 객체를 Spring으로부터 DI 받을 수 있습니다.

(2)와 같이 EntityManagerFactory의 createEntityManager() 메서드를 이용해서 EntityManager 클래스의 객체를 얻을 수 있습니다.
이제 이 EntityManager 클래스의 객체를 통해서 JPA의 API 메서드를 사용할 수 있습니다.

(3)과 같이 persist(member) 메서드를 호출하면 영속성 컨텍스트에 member 객체의 정보들이 저장됩니다.

em.persist(member)를 호출하면 1차 캐시에 member 객체가 저장되고, 이 member 객체는 쓰기 지연 SQL 저장소에 INSERT 쿼리 형태로 등록이 됩니다.

그런데, em.persist(member)를 호출할 경우, 영속성 컨텍스트에 member 객체를 저장하지만 실제 테이블에 회원 정보를 저장하지는 않습니다.

 
4. 영속성 컨텍스트와 테이블에 엔티티 저장

@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);

    }
}

(1)에서는 EntityManager를 통해서 Transaction 객체를 얻습니다. JPA에서는 이 Transaction 객체를 기준으로 데이터베이스의 테이블에 데이터를 저장합니다.

(4)와 같이 tx.commit()을 호출하는 시점에 영속성 컨텍스트에 저장되어 있는 member 객체를 데이터베이스의 테이블에 저장합니다.

(5)에서 em.find(Member.class, 1L)을 호출하면 (3)에서 영속성 컨텍스트에 저장한 member 객체를 1차 캐시에서 조회합니다.
1차 캐시에 member 객체 정보가 있기 때문에 별도로 테이블에 SELECT 쿼리를 전송하지 않습니다.

(6)에서는 영속성 컨텍스트에서 식별자 값이 2L인 member 객체가 존재하지 않기 때문에 테이블에 직접 SELECT 쿼리를 전송합니다

tx.commit()을 했기 때문에 member에 대한 INSERT 쿼리는 실행되어 쓰기 지연 SQL 저장소에서 사라집니다.

즉,

  • em.persist()를 호출하면 영속성 컨텍스트의 1차 캐시에 엔티티 클래스의 객체가 저장되고, 쓰기 지연 SQL 저장소에 INSERT 쿼리가 등록된다.
  • tx.commit()을 하는 순간 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리가 실행되고, 실행된 INSERT 쿼리는 쓰기 지연 SQL 저장소에서 제거된다.
  • em.find()를 호출하면 먼저 1차 캐시에서 해당 객체가 있는지 조회하고, 없으면 테이블에 SELECT 쿼리를 전송해서 조회한다.

 
5. 영속성 컨텍스트와 테이블에 엔티티 업데이트

@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)
    }
}

(3)과 같이 (2)에서 테이블에 저장된 member 객체를 영속성 컨텍스트의 1차 캐시에서 조회합니다.
테이블에서 조회하는 것이 아니고, 영속성 컨텍스트의 1차 캐시에 이미 저장된 객체가 있기 때문에 영속성 컨텍스트에서 조회합니다.

(4)에서 setter 메서드로 이메일 정보를 변경합니다.
여기서 중요한 사실은 em.update() 같은 JPA API가 있을 것 같지만 (4)와 같이 setter 메서드로 값을 변경하기만 하면 업데이트 로직은 완성이 됩니다.

tx.commit()을 실행하면 쓰기 지연 SQL 저장소에 등록된 UPDATE 쿼리가 실행이 됩니다.

UPDATE 쿼리가 실행이 되는 과정
-> 영속성 컨텍스트에 엔티티가 저장될 경우에는 저장되는 시점의 상태를 그대로 가지고 있는 스냅샷을 생성합니다.
변경된 값이 있으면 쓰기 지연 SQL 저장소에 UPDATE 쿼리를 등록하고 UPDATE 쿼리를 실행합니다.

 
6. 영속성 컨텍스트와 테이블의 엔티티 삭제

@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)
    }
}

(4)에서 em.remove(member)을 통해 영속성 컨텍스트의 1차 캐시에 있는 엔티티를 제거를 요청합니다.

(5)에서 tx.commit()을 실행하면 영속성 컨텍스트의 1차 캐시에 있는 엔티티를 제거하고, 쓰기 지연 SQL 저장소에 등록된 DELETE 쿼리가 실행이 됩니다.

 
(참고)
-EntityManager의 flush() API

tx.commit() 메서드가 호출되면 JPA 내부적으로 em.flush() 메서드가 호출되어 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영합니다.

profile
백엔드개발자를 향해서

0개의 댓글