티스토리 뷰

Java+Spring/Spring

[Spring]JPA(Java Persistence API)

Vagabund.Gni 2022. 8. 31. 17:59
728x90
반응형

 

 

 

지난 글에서 JDBC의 단점을 극복하며 나온 ORM에 대해 다루었다.

 

2022.08.26 - [개발/Spring] - [Spring]SQL Mapper vs. ORM

 

[Spring]SQL Mapper vs. ORM

지난 글에선 웹 앱의 계층 구조와 JDBC Workflow에 대해 알아봤다. 출처:https://herbertograca.com/2017/08/03/layered-architecture/ Layered Architecture In a layered architecture, the layers can be used..

gnidinger.tistory.com

간단히 요약하자면 ORM(Object Relational Mapping)Object ↔ DB Table을 매핑하는 기술로서,

 

객체지향RDB라는 두 개의 기둥 위에 존재하기 때문에 배우기가 어렵다고 했었다.

 

지난 글까지의 Spring Data JDBC에 이어서 이번 글에선 JPA에 대해 알아보자.

 

 

JPA(Java Persistence API)

 

JPA는 한 마디로 말하면 Java 진영의 ORM 기술 표준(또는 명세, Specification)이며, 인터페이스들의 모음이다.

 

인터페이스의 모음이기 때문에 당연하게도 구현체가 필요하며, 대략적인 구조는 아래와 같다.

 

애플리케이션이 JPA를 사용하며, 그 구현체로는 Hibernate, EclipseLink 등이 존재함을 확인할 수 있다.

 

다양한 구현체가 있지만 사실상 Hibernate를 제외하고는 잘 쓰이지 않으며,

 

이 글에서 살펴볼 것도 Hibernate ORM 구현체이다.

 

Data Access Layer에서의 JPA 위치

 

기존의 웹 앱의 구조를 가져오면 아래와 같다.

 

여기서 우리가 지금 관심있는 데이터 액세스 영역을 들여다보면 아래와 같은데,

 

JPA가 계층의 앞부분에 위치하는 것을 확인할 수 있다.

 

구체적으로는 DB작업은 JPA를 거쳐 Hibernate ORM을 통해 이루어지며,

 

Hibernate ORM은 내부적으로 JDBC API를 이용해서 데이터베이스에 접근하게 된다.

 

즉 이번 글에서 확인할 것은 JPA에서 지원하는 API를 사용해 DB에 접근하는 방식이 된다.

 

Persistence Context

 

지난 글에서 데이터 액세스 영역을 세분화하면 도메인 모델과 영속성 계층으로 나뉜다고 했었다.

 

2022.08.26 - [개발/Spring] - [Spring]JDBC(Java DataBase Connectivity)

 

[Spring]JDBC(Java DataBase Connectivity)

지난 글까지 웹 앱의 서비스 계층까지 구현을 마쳤다. 이번 글부터는 더 깊이 들어가 데이터 액세스 계층에 대해 다룰 텐데, 다시 짧게 복습하고 가자. Presentation Layer - API Layer라고 불리며 클라

gnidinger.tistory.com

여기서 영속성이란 데이터의 영속성, 즉 단어 그대로 앱이 종료되어도 저장된 데이터는 남는다는 뜻이며

 

JDBC는 바로 이 영속성(Persistence)을 보장하기 위해 자바에서 지원하는 기능이다.

 

따라서 이 글에서 다룰 JDBC 기반의 ORM인 JPA기술 역시 영속성 계층에 속한다고 볼 수 있다.

 

그렇다면 JPA는 데이터의 영속성을 어떻게 확보할 수 있을까?

 

답은 영속성 컨텍스트(Persistence Context)이다.

 

영속성 컨텍스트는 쉽게 말해 엔티티를 저장하고 관리하는 저장소라고 생각하면 편하며,

 

영속성 컨텍스트에 저장된 엔티티 정보는 DB에 CRUD작업을 하는 데 사용된다.

 

처음과 끝을 이어서 말하자면 결국 JPA의 목적은

 

  • Object ↔ DB Table 매핑 (설계)
  • 영속성 컨텍스트 (JPA 내부 동작)

라고 할 수도 있다.

 

들어가기 전에 먼저 영속성 컨텍스트의 생성 구조를 보고 가자.

 

위 그림은 JPA의 javax.persistence 패키지에 속한 클래스와 애너테이션 사이의 관계이다.

 

  • EntityManagerFactory

    • JPA는 EntityManagerFactory를 앱 로딩시점에 DB 당 딱 하나만 생성한다.
    • WAS가 종료되는 시점에 EntityManagerFactory 를 닫아야 커넥션 풀에 대한 리소스가 Release 된다. 
  • EntityManager

    • EntityManagerFactory는 클라이언트의 요청이 있을 때마다 EntityManager를 생성한다.
    • 스레드 간에 공유해선 안되며, 트랜잭션 수행 후에는 반드시 닫아야 DB 커넥션을 반환한다.
  • PersistenceContext

    • 직역하면 영속성 환경이라는 뜻이다. 즉, 엔티티를 영구 저장하는 환경을 말한다.
    • 정확하게는 엔티티를 DB에 저장하는 것이 아닌 영속성 컨텍스트에 저장하는 것이다.
    • EntityManager가 생성되면 1:1로 영속성 컨텍스트가 생성된다. → EntityManager를 통해 접근해야 한다.

계속해서 영속성 컨텍스트의 내부 구조를 보면 아래와 같다.

 

1차 캐시와 SQL버퍼로 이루어진 것을 확인할 수 있다.

 

위 구조는 실제 구조라기 보단 편의성에 의해 나눠둔 논리적 구조이다.

 

계속해서 JPA API를 이용해 영속성 컨텍스트에 엔티티를 저장해 보자.

 

JPA API로 Persistence Context에 접근

 

먼저 https://start.spring.io/에서 새로운 Spring 프로젝트를 생성하자.

 

  • build.gradle
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA, JPA 추가
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

이후 위와 같은 의존성을 추가해 데이터베이스와 JPA사용을 선언한다.

 

  • application.yml
spring:
  h2:
    console:
      enabled: true
      path: /h2 // Context path
  datasource:
    url: jdbc:h2:mem:test // (2) JDBC URL
  jpa:
    hibernate:
      ddl-auto: create // (3) 스키마 자동 생성
    show-sql: true // (4) 로그에 SQL 쿼리 출력

(3)과 같이 작성하면 앱 실행시 엔티티와 매핑되는 테이블을 자동으로 생성해 준다.

 

  • Configuration 클래스

샘플 실행을 위한 구성 클래스를 만든다.

package com.gnidinger;

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 -> {
        };
    }
}

@Configuration을 붙여 앱 실행 시 @Bean이 붙은 리턴 객체를 스프링 빈으로 추가한다.

 

준비는 끝났으니 계속해서 엔티티 클래스를 만들어보자.

 

  • Member 클래스
package com.gnidinger;

import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@Setter
@NoArgsConstructor
@Entity
public class Member {
    @Id
    @GeneratedValue  // 식별자 생성
    private Long memberId;

    private String email;

    public Member(String email) {
        this.email = email;
    }
}

@GeneratedValue는 식별자 자동 생성 전략을 지정할 때 사용한다.

 

자세한 내용은 다음 글에 나온다.

 

계속해서 Configuration 클래스를 수정하자.

package com.gnidinger;

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) { // (1)
        this.em = emFactory.createEntityManager(); // (2)
        this.tx = em.getTransaction();

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

    private void example01() {
        Member member = new Member("gni@gmail.com");

        em.persist(member); // (3)

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

(1) - Entity Manager 클래스에 의해 관리되는 엔티티는 EntityManagerFactory 객체를 Spring으로부터 DI 받을 수 있다.

 

(2) - EntityManagerFactory의 createEntityManager() 메서드를 이용해 EntityManager 클래스의 객체를 만들 수 있다.

        이제 이 EntityManager 클래스의 객체를 통해서 JPA의 API 메서드를 사용, 영속성 컨텍스트에 접근할 수 있다.

 

(3) - EntityManager의 persist()메서드를 이용해 영속성 컨텍스트에 member의 정보를 저장한다.

 

(4) - find(엔티티 클래스, 식별자 값) 메서드를 이용해 영속성 컨텍스트를 조회한다.

 

위와 같이 작성하고 실행하면 영속성 컨텍스트는 다음과 같은 상태가 된다.

 

em.persist(member)를 통해 1차 캐시에 member 객체가 저장되고, SQL Buffer에 INSERT 쿼리 형태로 등록된다.

 

위와 같이 작성하고 앱을 시작하면 다음과 같은 로그를 확인할 수 있다.

Hibernate: create table member (member_id bigint not null, email varchar(255), primary key (member_id))

Hibernate: call next value for hibernate_sequence
Id: 1, email: gni@gmail.com

JPA가 내부적으로 테이블을 자동 생성하고, 테이블의 기본키를 할당해주는 것을 확인할 수 있다.

 

하지만 em.persist()메서드 만으로는 실제 테이블에 정보가 저장되지 않는다.

 

계속해서 Configuration 클래스를 고쳐보자.

package com.gnidinger;

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(); // (1)

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

    private void example02() {
        tx.begin(); // (2)
        
        Member member = new Member("gni@gmail.com");

        em.persist(member); // (3)

        tx.commit(); // (4)

        Member resultMember1 = em.find(Member.class, 1L); // (5)

        System.out.println("Id: " + resultMember1.getMemberId() + ", email: " + resultMember1.getEmail());

        Member resultMember2 = em.find(Member.class, 2L); // (6)

        System.out.println(resultMember2 == null); // (7)

    }
}

(1) - EntityManager를 통해 Transaction 객체 생성. JPA에서는 Transaction을 기준으로 DB의 테이블에 데이터를 저장.

 

(2) - JPA에서는 Transaction을 시작하기 위해서 tx.begin() 메서드를 먼저 호출해야 한다.

 

(3) - member 객체를 영속성 컨텍스트에 저장

 

(4) - tx.commit()을 호출해 영속성 컨텍스트에 저장되어 있는 member 객체를 데이터베이스의 테이블에 저장

 

(5) - em.find(Member.class, 1L)을 호출하면 (3)에서 영속성 컨텍스트에 저장한 member 객체를 1차 캐시에서 조회

 

(6) - em.find(Member.class, 2L)를 호출해서 식별자 값이 2L인 member 객체를 1차로 조회.

        하지만 영속성 컨텍스트에는 식별자 값이 2L인 member 객체가 존재하지 않기 때문에 (7)은 true 출력.

        또한 (6)에서는 영속성 컨텍스트에서 식별자 값이 2L member 객체가 존재하지 않기 때문에

        테이블에 직접 SELECT 쿼리를 전송해서 추가 조회.

 

수정 후 실행하면 영속성 컨텍스트는 아래와 같은 상황이 된다.

 

Buffer에 있던 쿼리가 DB로 이동해 실행되는 것을 확인할 수 있다.

 

참고로 flush()는 트랜잭션을 DB로 전송만 하고 commit()을 하지 않은 상태를 말한다.

 

즉, 동기화는 되었지만 아직 commit 되지 않아 롤백이 가능한 상태를 말하며,

 

commit()을 실행하면 내부적으로 flush()를 수행하게 된다.

 

두 개 이상의 엔티티를 한 번에 넣으려면 단순히 한 번 더 적어주면 된다.

    private void example02() {
        tx.begin();

        Member member1 = new Member("gni1@gmail.com");
        Member member2 = new Member("gni2@gmail.com");

        em.persist(member1);
        em.persist(member2);

        tx.commit();
        }

계속해서 저장된 정보의 업데이트 및 삭제에 대해서 보자.

 

마찬가지로 메서드만 바꿔주면 된다.

    private void example03() {
        tx.begin();
        em.persist(new Member("gni@gmail.com"));
        tx.commit();

        tx.begin();
        Member member1 = em.find(Member.class, 1L);
        member1.setEmail("gni@naver.com"); // setter()로 정보 업데이트
        tx.commit();
    }

먼저 위와 같이 데이터를 저장하고 이메일 주소를 변경했다.

 

특이한 점은 em.update()와 같은 메서드가 아닌 setter() 메서드를 이용해 값을 변경하는 것으로 끝이라는 점이다.

 

이는 위에서 알아본 flush()가 변경사항을 DB에 동기화해주기 때문이다.

 

따라서 commit()과 함께 DB에선 UPDATE 쿼리가 실행된다.

 

이어서 삭제다.

    private void example03() {
        tx.begin();
        em.persist(new Member("gni@gmail.com"));
        tx.commit();

        tx.begin();
        Member member = em.find(Member.class, 1L);
        em.remove(member); // 정보 삭제
        tx.commit();
    }

먼저 em.find()로 객체를 조회하고 em.remove()로 제거한 것을 확인할 수 있다.

 

이번엔 당연히 commit과 함께 DELETE 쿼리가 실행된다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함