Spring Boot/MVC

[Spring MVC] JPA(Java Persistence API) 개요

WY J 2022. 8. 31. 17:46

JPA란?

Java 진영에서 사용하는 ORM(Object-Relational Mapping) 기술의 Spec이다.

다시 말해, Java의 인터페이스로 사양이 정의되어 있기 때문에 JPA라는 표준 사양을 구현한 구현체는 따로 있다는 것을 의미한다.

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

 

Hibernate ORM

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

앞으로 다룰 Hibernate ORM은 JPA에서 정의해둔 인터페이스를 구현한 구현체로써 JPA 지원 기능 이외에 Hibernate 자체 API 역시 지원하고 있다.

 

데이터 저장, 조회 등의 작업 순서

  1. JPA를 거친다.
  2. JPA의 구현체인 Hibernate ORM을 통해서 작업
  3. Hibernate ORM은 내부적으로 JDBC API를 이용하여 데이터베이스에 접근

 

JPA에서 P(Persistence)의 의미와 영속성 컨텍스트(Persistence Context)

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

 

영속성 컨텍스트에는 1차 캐시라는 영역과 쓰기 지연 SQL 저장소라는 영역이 있다. 엔티티 정보를 영속성 컨텍스트에 저장하는 API를 사용하면 영속성 컨텍스트의 1차 캐시에 엔티티 정보가 저장된다.

 

 

JPA API를 사용하여 영속성 컨텍스트에 엔티티 저장하기

build.gradle 설정

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

'org.springframework.boot:spring-boot-starter-data-jpa'를 추가하면 Spring Data JPA를 포함한 JPA API를 사용할 수 있다.

 

 

application.ymi 설정

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

ddl-auto: create

  • JPA에서 사용하는 엔티티 클래스를 정의하고 애플리케이션 실행 시, 이 엔티티와 매핑되는 테이블을 데이터베이스에 자동으로 생성

show-sql: true

  • JPA API를 통해 실행되는 SQL 쿼리를 로그로 출력해 준다.

 

 

영속성 컨텍스트에 저장할 엔티티 정의

@Getter
@Setter
@NoArgsConstructor
@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long memberId;
    private String email;
    
    public Member(String email) {
        this.email = email;
    }
}

@GeneratedValue

  • 데이터베이스 테이블에서 기본키가 되는 식별자를 자동으로 설정해 준다.

 

 

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

/** 
 * Spring에서 Bean 검색 대상인 Configuration 클래스로 간주하여 @Bean 메서드를 검색한 후,
 * 리턴하는 객체를 Spring Bean으로 추가 한다.
 */ 
@Configuration
public class JpaBasicConfig {
    private EntityManager em;
    private EntityTransaction tx;

    /**
     * JPA 영속성 컨텍스트는 EntityManager 클래스에 의해 관리되고,
     * 이 클래스의 객체는 EntityManagerFactory 객체를 Spring 으로부터 DI 받을 수 있다.
     */
    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {

        /**
         * EntityManagerFactory 의 createEntityManager() 메서드를 이용하여
         * EntityManager 클래스의 객체를 얻는다.
         * 얻은 EntityManager 객체를 통해 JPA API 를 사용할 수 있다.
         */
        this.em = emFactory.createEntityManager();

        /**
         * EntityManager 를 통해 객체를 얻는다.
         * JPA 에서는 이 Transaction 객체를 기준으로 데이터베이스의 테이블에 데이터를 저장한다.
         */
        this.tx = em.getTransaction();

        /**
         * CommandLineRunner 객체를 람다 표현식으로 정의해주면 애플리케이션 부트스트랩 과정이 완료된 후,
         *  이 람다 표현식에 정의한 코드를 실행해 준다.
         */
        return args -> {
            example02();
        };
    }

    private void example02() {
        /** Transaction 을 시작하기 위해 tx.begin() 메서드를 호출한다. */
        tx.begin();
        Member member = new Member("hgd@gmail.com");

        /**
         * em.persist(Entity객체)를 호출하면 영속성 컨텍스트의 1차 캐시에 Entity 객체가 저장되고,
         * 쓰기 지연 SQL 저장소에 INSERT 쿼리가 등록된다.
         */
        em.persist(member);

        /**
         * tx.commit() 메서드를 호출하는 시점에 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리가 실행되고,
         * 저장되어 있는 member 객체를 데이터베이스 테이블에 저장한다. INSERT 쿼리는 제거된다.
         */
        tx.commit();

        /** 
         * em.find(EntityType, EntityId) 를 호출하면 해당 객체를 1차 캐시에서 조회하고,
         * 없다면 테이블에 SELECT 쿼리를 전송한다.
         */
        Member resultMember1 = em.find(Member.class, 1L);

        /** 로그에는 출력되지만 테이블에 SELECT 쿼리 전송은 되지 않는다. */
        System.out.println("Id :" + resultMember1.getMemberId() + ", email: " + resultMember1.getEmail());

        /** 식별자 값이 2L인 member 객체는 존재하지 않기 때문에 테이블에 SELECT 쿼리를 전송한다. */
        Member resultMember2 = em.find(Member.class, 2L);

        /** 식별자 값이 2L인 member 객체는 없으므로 결과는 true 가 된다. */
        System.out.println(resultMember2 == null);
    }
}

em.persist()를 호출하면 영속성 컨텍스트의 1차 캐시에 엔티티 클래스의 객체가 저장되고, 쓰기 지연 SQL 저장소에 INSERT 쿼리가 등록된다.

 

tx.commit()을 하는 순간 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리가 실행되고, 실행된 INSERT 쿼리는 쓰기 지연 SQL 저장소에서 제거된다.

tx.commit()을 호출하면 내부적으로 em.flush()가 호출된다.

em.flush()를 사용해서 영속성 컨텍스트의 변경 사항을 테이블에 반영할 수 있다.

em.find()를 호출하면 먼저 1차 캐시에서 해당 객체가 있는지 조회하고, 없으면 테이블에 SELECT 쿼리를 전송해서 조회한다.

 

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

@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(); // 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리 실행

        tx.begin();
        Member member1 = em.find(Member.class, 1L); // 영속성 컨텍스트 1차 캐시에서 조회
        member1.setEmail("hgd1@yahoo.co.kr"); // setter 메서드로 이메일 정보 변경
        tx.commit(); // 쓰기 지연 SQL 저장소에 등록된 UPDATE 쿼리 실행
    }
}

UPDATE 쿼리 실행 과정

  1. 엔티티가 저장되는 시점의 상태를 가지고 있는 스냅샷을 생성한다.
  2. setter 메서드로 엔티티의 값을 변경한다.
  3. commit() 을 호출하면 변경된 엔티티와 스냅샷을 비교한 후, 변경된 값이 있으면 UPDATE 쿼리를 등록하고 실행한다.

 

 

영속성 컨텍스트와 테이블에 엔티티 삭제

@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(); // 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리 실행

        tx.begin();
        Member member = em.find(Member.class, 1L); // 영속성 컨텍스트 1차 캐시에서 조회
        em.remove(member); // 영속성 컨텍스트의 1차 캐시에 있는 엔티티 제거 요청
        tx.commit(); // 1차 캐시에 있는 엔티티를 제거하고, 쓰기 지연 SQL 저장소에 등록된 DELETE 쿼리 실행
    }
}

em.remove()를 사용해서 엔티티 객체를 영속성 컨텍스트에서 제거할 수 있다.