/ #JAVA#SPRING#JPA

JPA(Java Persistence API)

JPA란?

JPA(Java Persistence API)는 자바 진영의 ORM 기술 표준으로, ORM 프레임워크를 쉽게 사용하기 위한 인터페이스의 모음입니다.

데이터베이스 연동에 사용되는 기술은 JDBC에서부터 Spring DAO, MyBatis, Hibernate 등 다양합니다. 이 중에서 Hibernate 같은 ORM 프레임워크는 SQL까지 프레임워크에서 제공하여 개발자들이 처리해야 할 업무가 상당히 감소했습니다.

이전에는 데이터베이스에 연동하기 위해 SQL Query를 직접 작성하여 영속 데이터를 가져왔습니다. 이 경우 여러 문제점이 있는데 데이터베이스의 테이블이 변경되더라도 SQL Query는 String 형식으로 되어있어서 컴파일 에러가 나오지 않습니다. 또한 Query문을 잘못 작성하더라도 컴파일시 확인할 수 없어서 런타임에서 에러를 발생시킵니다. 이러한 이유로 개발자들은 순수 객체지향 프로그래밍에 집중하지 못하고 Query문 작성에 많은 비용을 지불해야만 했습니다.

하지만 Hibernate의 등장으로 이런 문제들이 해소되어 이후 많은 ORM 프레임워크가 등장했으며, 이런 ORM을 보다 쉽게 사용할 수 있도록 표준화 시킨 것이 JPA(Java Persistence API) 입니다.

img02

JPA는 애플리케이션과 JDBC 사이에서 동작합니다. 기존 마이바티스 같은 프레임워크는 SQL을 개발자가 직접 XML 파일에 등록하여 사용했지만, JPA를 사용하면 JPA 내부에서 JDBC API를 통해 SQL을 호출하여 DB와 통신하게 됩니다. 즉, 개발자가 직접 JDBC API를 사용하지 않습니다.

JPA의 장점은 CRUD SQL을 작성할 필요가 없고 조회된 결과를 객체로 매핑하는 작업을 자동으로 처리하기 때문에 데이터 저장 계층에서 작성해야할 코드가 대폭적으로 줄어듭니다. 또한 SQL이 아닌 객체 중심으로 개발하여 생산성과 유지보수가 좋아집니다.

그러나 배우기가 어렵고 잘 이해하지 않으면 데이터 손실이 있을 수 있으며, 복잡한 작업에 대해서는 성능상 문제가 있을 수 있습니다.

예제 파일

JPA 예제 파일

JPA 관련 용어

객체-관계 매핑(Object Relational Mapping, ORM)

ORM이란 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 기술을 말합니다.

img01

기본적으로 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용하기 때문에 객체 모델과 관계형 모델 간에 불일치가 존재합니다. ORM은 객체 간의 관계를 바탕으로 SQL을 자동으로 생성함으로써 이런 패러다임의 불일치 문제를 해결해줍니다.

Hibernate

img06

JPA가 제공하는 인터페이스를 이용하여 데이터베이스를 처리하면 실제로는 Hibernate 같은 구현체(Implimentations)가 동작합니다. 그렇기 때문에 Hibernate를 통해 개발하더라도 실제 서비스에서는 다른 ORM 기술인 EclipseLink로 변경하더라도 동일한 서비스를 제공할 수 있습니다.

5 레이어 아키텍처(5 Layer Architecture)

스프링 프레임워크는 MVC 패턴을 좀더 세분화 하여 5 레이어 아키텍처(5 Layer Architecture)로 나타낼 수 있습니다.

img04

모델(Model)비즈니스 로직 계층(Business Logic Layer)영구 계층(Persistence Layer), 도메인 모델 계층(Domain Model Layer)에 해당합니다.

여기에서 비즈니스 로직 계층(Business Logic Layer)은 주로 상태 변화를 처리하는 역할을 하며, 컨트롤러와 JPA를 연결해주는 서비스(Service)와 객체를 연관 테이블과 매핑해주는 도메인(Domain)으로 나눌 수 있습니다.

기존에는 비즈니스 로직과 ORM 기능을 모두 이 계층에서 수행했지만 이를 분리하여 비즈니스 로직은 서비스에서, ORM 기능은 도메인에서 수행합니다.

5 레이어 아키텍처는 도메인 주도 설계(Domain Driven Design, DDD)와 관련있는데, 아직 이에 대한 이해도가 부족하여 생략하겠습니다.

아래에는 비즈니스 로직 계층과 관련된 객체들에 대한 설명입니다.

DAO(Data Access Object)

DAO(Data Access Object)는 DB에 접근하여 CRUD를 할 수 있는 객체입니다. 서비스와 DB를 연결해주며, 구현체에 CRUD 기능을 구현하고 이를 의존성 주입(DI)해주는 방식으로 사용됩니다. 스프링에서는 Repository가 DAO 역할을 하는데, DB 접근 방식에 약간의 차이가 있다고 합니다.

DTO(Data Transfer Object)

DTO(Data Transfer Object)는 계층 간 데이터 교환을 하기 위해 사용하는 객체입니다. 로직을 가지지 않는 순수한 데이터 객체로서, getter나 setter 정도의 메소드를 가질 수 있습니다.

img05

이와 비슷한 VO(value Object) 객체는 오직 읽기만 가능한 read-Only 특징을 가지며, getter 메소드만 가지고 있습니다.

Board.java
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Date;

@Getter
@Setter
@ToString
public class Board {
    private Long seq;
    private String title;
    private String writer;
    private String content;
    private Date createDate;
    private Long cnt;
}

Domain(=Entity)

도메인은 DB 테이블과 매핑되는 객체입니다. 변경사항이 생기면 여러 다른 클래스에 영향을 주기때문에 변경사항이 많은 DTO와 분리하여 사용됩니다.

도메인의 생성에는 @Entity, @Column, @Id 등과 같은 어노테이션(annotation)이 사용됩니다.

Board.java
import lombok.*;

import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@ToString
@Entity
public class Board {
    @Id @GeneratedValue
    private Long seq;
    private String title;
    private String writer;
    private String content;
    @Temporal(TemporalType.DATE)
    private Date createDate;
    private Long cnt;
}

JPA 설정

Hibernate를 사용하여 JPA를 구현해보도록 하겠습니다. 빌드는 gradle을 사용하며, Oracle DB에 연결하겠습니다.

의존성(dependency) 설정

gradle에 의존성 패키지로 hibernate-entitymanager를 추가합니다.

implementation 'org.hibernate:hibernate-entitymanager'

Entity 작성을 편하게 하기 위해 Lombok 패키지도 의존성에 추가하고, 암호키로 오라클DB에 접근하기 위해 ojdbc와 security 패키지도 추가합니다.

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
implementation 'com.oracle.database.security:oraclepki'
implementation 'com.oracle.database.security:osdt_core'
implementation 'com.oracle.database.security:osdt_cert'

전체 코드입니다.

build.gradle
plugins {
    id 'org.springframework.boot' version '2.7.2'
    id 'io.spring.dependency-management' version '1.0.12.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {

    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    implementation 'org.hibernate:hibernate-entitymanager'

    runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
    implementation 'com.oracle.database.security:oraclepki'
    implementation 'com.oracle.database.security:osdt_core'
    implementation 'com.oracle.database.security:osdt_cert'
}

tasks.named('test') {
    useJUnitPlatform()
}

영속성 컨텍스트(Persistence Context) 설정

영속성(Persistence)이란 데이터를 생성한 프로그램이 종료되더라도 사라지지 않는 데이터의 특성을 말합니다. 영속성이 없는 데이터는 메모리에서만 존재하기 때문에 프로그램을 종료하면 사라져 버립니다. 데이터가 영속성을 가지기 위해서는 파일 시스템이나 데이터베이스 등을 통해 영속성을 부여해야만 합니다.

JPA는 영속성이 없는 데이터에 영속성을 부여하는 역할을 하며, 이렇게 영속성이 부여되는 환경을 영속성 컨텍스트(Persistence Context)라고 합니다. 프로그램이 종료되기 전까지 Entity가 영구적으로 저장되는 환경입니다.

영속성 컨텍스트는 기본적으로는 persistence.xml 파일을 통해 설정됩니다.

/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">
    <persistence-unit name="practice">
        <class>com.example.demo.Board</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="oracle.jdbc.OracleDriver"/>
            <property name="javax.persistence.jdbc.user" value="아이디"/>
            <property name="javax.persistence.jdbc.password" value="암호"/>
            <property name="javax.persistence.jdbc.url" value="주소"/>

            <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle12cDialect"/>

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="false"/>
            <property name="hibernate.id.new_generator_mappings" value="false"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
        </properties>
    </persistence-unit>
</persistence>

영속성 유닛(persistence-unit) 설정

persistence.xml 파일의 루트는 <persistence>이며, 영속성 유닛인 <persistence-unit>을 엘리먼트로 가집니다.

영속성 유닛은 데이터베이스당 하나씩 설정할 수 있습니다. 즉, 데이터베이스가 하나 이상이면 영속성 유닛 또한 여러 개 이므로 이를 구분하기 위해 유일한 이름(name)을 지정해야 합니다. 위의 코드에서는 practice라는 이름으로 영속성 유닛을 하나 생성합니다.

img07

영속성 유닛은 EntityManagerFactory를 생성할 수 있습니다. JPA를 사용하려면 EntityManager 객체가 필요한데, EntityManager는 EntityManagerFactory로부터 생성됩니다. 생성 비용이 크기때문에 싱글톤으로 만들어 사용하며, Thread-safe 입니다.

EntityManager는 CRUD를 처리합니다. Thread-unsafe 이기 때문에 Thread 환경에서는 주의해야 합니다. 각각 영속성 컨텍스트를 가지고 있으며, 데이터를 변경하려면 트랜잭션(transaction)이 이루어져야 합니다.

EntityManager 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("practice");
EntityManager em = emf.createEntityManager();

Entity 클래스 등록

Entity 클래스 목록은 영속성 유닛 설정에서 가장 먼저 등록되는 정보입니다. 순수 JPA만 사용한다면 Entity 클래스를 영속성 유닛에 명시적으로 등록해야만 합니다. Entity 작성은 아래 Entity 매핑 설정항목을 참조하세요.

프로퍼티(property) 설정

이제 각종 프로퍼티를 설정해줍니다.

우선 데이터소스(Datasource)를 설정합니다. JPA 구현체는 데이터소스를 참조하여 특정 데이터베이스와 연결할 수 있습니다. 데이터소스의 설정은 프로퍼티 이름만으로 어느정도 의미를 알 수 있습니다.

데이터 소스(Datasource) 프로퍼티
프로퍼티 의미
javax.persistence.jdbc.driver JDBC 드라이버
javax.persistence.jdbc.user DB 아이디
javax.persistence.jdbc.password DB 암호
javax.persistence.jdbc.url DB 주소

다음으로 Dialect를 설정합니다. Dialect는 번역하면 방언 입니다. JPA의 큰 장점 중 하나가 SQL을 자동으로 생성한다는 점인데 데이터베이스마다 SQL이 조금씩 차이가 있습니다. 그러므로 Dialect를 설정하여 특정 데이터베이스에 최적화된 SQL이 생성되도록 합니다.

hibernate-core 패키지에는 대부분의 데이터베이스에 해당하는 Dialect가 포함되어 있습니다.

데이터베이스 Dialect
DB2 org.hibernate.dialect.DB2Dialect
PostgreSQL org.hibernate.dialect.PostgreDialect
MySQL org.hibernate.dialect.MySQLDialect
Oracle org.hibernate.dialect.OracleDialect
Sybase org.hibernate.dialect.SybaseDialect
Microsoft SQL Server org.hibernate.dialect.SQLServerDialect
SAP DB org.hibernate.dialect.SAPDBDialect
H2 org.hibernate.dialect.H2Dialect

Hibernate에서 지원하는 Dialect의 종류는 공식문서를 참조하세요.


마지막으로 JPA 구현체를 설정하겠습니다. Hibernate를 사용할 것이므로 이에 대한 프로퍼티를 설정하면 됩니다.

프로퍼티 의미
hibernate.show_sql 하이버네이트에서 생성한 SQL을 콘솔에 출력
hibernate.format_sql SQL을 출력할 때 특정 포맷으로 출력
hibernate.use_sql_comments SQL에 포함된 주석을 포함하여 출력
hibernate.id.new_generator_mapping 키(key) 생성전략 사용 여부
hibernate.hbm2ddl.auto DDL 자동 실행 여부

여기에서 hibernate.hbm2ddl.auto는 SessionFactory가 생성될 때 실행될 DDL을 정의합니다.

옵션 의미
create 기존 테이블을 삭제하고 다시 생성 (DROP + CREATE)
create-drop SessionFactory가 종료되면 테이블을 삭제
update 변경된 부분에만 UPDATE 적용
validate Entity와 테이블이 정상적으로 매핑되었는지 확인
none (default) 아무것도 하지 않음

주의할 점은 개발 단계나 테스트 단계에서만 사용하며, 운영서버에서는 validate나 none으로 설정해야 합니다.

Entity 매핑 설정

JPA는 어노테이션(annotation)만 적절히 설정한다면 Entity를 통해 데이터를 쉽게 관리할 수 있습니다.

Board.java
import lombok.*;
import javax.persistence.*;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import java.util.Date;

@Getter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicInsert
@DynamicUpdate

public class Board {
    
    @Id @GeneratedValue
    private Long seq;

    @Setter
    @Column(nullable = false)
    private String title;

    @Column(length = 40, nullable = false)
    private String writer;

    @Setter
    @Column(columnDefinition = "varchar(100) default 'No Contents'")
    private String content;

    @Temporal(TemporalType.DATE)
    @Column(updatable = false)
    private Date createDate;

    @ColumnDefault("0")
    private Long cnt;

    @PrePersist
    protected void onCreate() {
        createDate = new Date();
    }
}

@Entity

클래스를 JPA가 관리하는 Entity로 인식하도록 합니다.

@DynamicInsert

null이 아닌 필드만 사용하여 동적인 INSERT Query를 생성합니다.

@DynamicUpdate

변경된 필드만 사용하여 동적인 UPDATE Query를 생성합니다.

@Id

다른 객체와 식별할 수 있는 고유값을 가지는 필드로, 테이블의 기본키(Primary key)와 매핑됩니다.

@GeneratedValue

식별자값을 자동으로 증가시키며, 다음과 같은 속성을 줄 수 있습니다.

@GeneratedValue
속성 설명
strategy 기본키 자동 생성 전략 선택
generator 이미 생성된 키 생성기 참조

기본키를 생성하는 전략은 다음과 같습니다.

기본키 생성 전략
strategy 설명
GenerationType.TABLE 테이블을 사용하여 기본키 생성 (기본키를 생성하는 별도의 테이블 필요)
GenerationType.SEQUENCE 시퀀스를 이용하여 기본키 생성 (시퀀스를 지원하는 DB에서 사용 가능)
GenerationType.IDENTITY auto_increment 또는 IDENTITY를 이용하여 기본키 생성
GenerationType.AUTO (기본값) Hibernate가 DB에 맞는 전략을 자동으로 선택

@Table

Entity를 테이블과 매핑할 때 사용하며, 다음과 같은 속성을 줄 수 있습니다.

@Table
속성 설명
name 테이블 이름 지정
catalog DB 카탈로그 지정
schema DB 스키마 지정
uniqueConstraints 결합 unique 제약조건을 지정

@Column

Entity 필드와 테이블 column을 매핑할 때 사용하며, 다음과 같은 속성을 줄 수 있습니다.

@Column
속성 설명
name column 이름 지정 (생략시 프로퍼티명과 동일)
unique 제약조건 추가 (기본값: false)
nullable null 허용 여부 (기본값: true)
insertable INSERT 가능 여부 (기본값: true)
updatable UPDATE 가능 여부 (기본값: true)
columnDefinition column에 대한 DDL을 직접 기술
length 문자열 타입 column 길이 지정 (기본값: 255)
precision 숫자 타입의 자리수 지정 (기본값: 0)
scale 숫자 타입의 소수점 자리수 지정 (기본값: 0)

@ColumnDefault

스키마 생성시 필드의 기본값을 설정합니다.

@Transient

Entity 필드를 매핑에서 제외할 때 사용합니다.

@Temporal

java.util.Date 타입의 날짜 데이터를 매핑할 때 사용하며, 다음과 같은 값을 가집니다.

@Temporal
설명
TemporalType.DATE 날짜만 출력
TemporalType.TIME 시간만 출력
TemporalType.STAMP 날짜와 시간 모두 출력

@PrePersist

Entity가 persist 되기 전에 호출됩니다.

@PreUpdate

Entity가 update 되기 전에 호출됩니다.

JPA 이해

JPA를 이용하여 CRUD 프로그램을 구현하기에 앞서 영속성 컨텍스트와 Entity의 생명주기(Lifecycle)에 대해 알아보도록 하겠습니다.

영속성 컨텍스트(Persistence Context)

영속성 유닛을 설정할 때 잠깐 언급했지만 영속성 컨텍스트는 데이터가 영속성을 부여받는 환경을 말합니다. 영속성을 부여받았다고 해서 완전히 사라지지 않는 것은 의미하지는 않습니다. 앞서 말했지만 영속성 컨텍스트에 저장된 데이터는 프로그램이 종료되면 컨텍스트와 함께 메모리에서 사라져 버립니다.

영속성 컨텍스트는 EntityManager가 가지고 있습니다. EntityManager는 영속성 컨텍스트를 통해 CRUD를 수행할 수 있습니다.

영속성 컨텍스트는 크게 1차 캐시 저장소SQL 저장소로 구분됩니다.

img12

1차 캐시 저장소에는 Entity 정보를 저장합니다. 이 상태를 영속 상태라고 합니다.

SQL 저장소에는 SQL Query를 저장합니다. 저장된 쿼리는 EntityManager의 flush() 메소드에 의하여 DB에 반영됩니다.

Entity 생명주기(Lifecycle)

Entity는 EntityManager가 제공하는 메소드를 통해 관리되며, 다음과 같이 4가지 상태로 존재합니다.

img08

비영속 상태(New)

비영속 상태는 Entity 객체만 생성하고 아직 Entity를 영속성 컨텍스트에 저장하지 않은 상태입니다.

img09

비영속 상태
Board board = Board.builder()
    .title("테이블 제목")
    .writer("Rubisco")
    .content("첫번째 글입니다.")
    .build();

영속 상태(Managed)

영속 상태는 Entity 객체가 영속성 컨텍스트에 저장된 상태입니다. Id를 키(key)로 하여 Entity가 1차 캐시 저장소에 저장됨과 동시에 SQL 저장소에는 INSERT Query가 저장됩니다.

img10

이 상태에서는 Entity를 영속성 컨텍스트가 관리하는 상태이므로 객체의 상태변경을 자동으로 감지합니다. 이것을 Dirty Checking이라고 합니다.

영속 상태가 되기 위해서는 EntityManager의 persist() 메소드를 사용합니다.

EntityManager.persist()
EntityManagerFactory emf = Persistence.createEntityManagerFactory("practice");
EntityManager em = emf.createEntityManager();

em.persist(board);

persist() 메소드를 통해 Entity가 영속화 되어도 flush() 메소드가 호출되기 전까지는 DB에 쿼리가 전달되지 않고 SQL 저장소에 쌓입니다.

img13

EntityManager의 flush() 메소드가 호출되면 SQL 저장소의 모든 쿼리가 DB에 반영됩니다. flush를 하더라도 Entity는 영속성 컨텍스트에 계속 저장되어 있습니다. 단지 영속성 컨텍스트와 DB를 동기화(Synchronize)할 뿐입니다.

img14

EntityManager의 find() 메소드를 통해서도 DB의 데이터를 통해 Entity 객체를 만들어 영속성 컨텍스트에 저장함으로써 영속 상태가 될 수 있습니다.

EntityManager.find()
em.find(Board.class, 2L);

img11

만약 같은 Entity를 조회한다면 JPA는 1차 캐시 저장소에 있는 Entity를 반환하게 되므로 성능상 큰 이점을 얻을 수 있습니다.

아래 코드를 실행시켜보면 영속화된 board 객체와 find()로 호출한 findBoard 객체는 동일한 객체라는 것을 확인할 수 있습니다.

JPAClient.java
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JPAClient {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("practice");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();

            Board board = Board.builder()
                .title("테이블 제목")
                .writer("Rubisco")
                .content("첫번째 글입니다.")
                .build();
            
            em.persist(board);
            tx.flush();

            Board findBoard = em.find(Board.class, 1L);
            System.out.println("동일성 여부 : " + (board == findBoard)); // true 출력
            
        } catch (Exception e) {
            e.printStackTrace();
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

준영속 상태(Detatched)

EntityManager의 detach() 메소드 또는 clear(), close() 메소드가 호출된 경우 Entity는 준영속 상태가 됩니다.

detach() 메소드는 특정 Entity를 준영속 상태로 전환합니다.

img15

EntityManager.detach()
em.detach(board);

clear() 메소드는 영속성 컨텍스트 안의 모든 Entity를 준영속 상태로 만듭니다.

img16

close() 메소드는 EntityManager를 닫아 영속성 컨텍스트를 종료하고 영속성 컨텍스트 안의 모든 Entity를 준영속 상태로 만듭니다.

img17

준영속 상태가 된 Entity는 EntityManager의 관리를 벗어나기 때문에 Dirty Checking을 하지 않습니다. 하지만 완전히 메모리에서 사라진 것은 아니기 때문에 EntityManager의 merge() 메소드를 통해 다시 영속상태로 전환될 수 있습니다.

EntityManager.merge()
em.merge(board);

여기에서 중요한 것은 board 객체 자체가 다시 영속상태가 되는 것이 아니라는 것입니다. merge라는 말처럼 새로운 Entity에 board 객체의 모든 필드를 합병하고 새로운 객체를 1차 캐시 저장소에 저장합니다. 즉, merge는 UPDATE 기능을 수행합니다.

img18

EntityManager로부터 merge 메소드가 호출되면 우선 DB에서 Id 필드와 같은 동일한 기본키를 조회합니다. 동일한 기본키가 없다면 새로운 Entity에 board Entity를 합병하고 합병된 Entity를 반환합니다. 이때 SQL 저장소에는 INSERT Query가 저장됩니다.

동일한 기본키가 있다면 해당 Entity를 영속화하여 board Entity를 합병합니다. 이때 SQL 저장소에는 UPDATE Query가 저장됩니다.

실제 아래 코드를 통해 1차 캐시 저장소에 저장된 객체는 board가 아닌 mergedBoard라는 것을 확인할 수 있습니다.

JPAClient.java
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JPAClient {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("practice");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();

            Board board = Board.builder()
                .title("테이블 제목")
                .writer("Rubisco")
                .content("첫번째 글입니다.")
                .build();
            
            Board mergedBoard = em.merge(board);
            em.flush();

            Board findBoard = em.find(Board.class, 1L);
            System.out.println("동일성 여부 : " + (findBoard == mergedBoard)); // true 출력
            
        } catch (Exception e) {
            e.printStackTrace();
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

사실 1차 캐시 저장소에는 entity 자체가 저장되는 것이 아니라 Entity의 Reference와 Snapsot을 저장하고 있습니다.

img19

flush가 호출되고 실행되기 전에 EntityManager는 실제 Entity와 Snapshot을 비교하여 변경을 감지할 수 있습니다. 내용이 변경되었다면 UPDATE Query를 생성하여 DB를 update 할 수 있습니다.

주의할 것은 Entity의 모든 필드가 교체되므로 값이 null이 될 가능성이 있다는 것을 숙지해야합니다.

JPAClient.java
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JPAClient {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("practice");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();

            Board board = Board.builder()
                .title("테이블 제목")
                .writer("Rubisco")
                .content("첫번째 글입니다.")
                .build();
            
            em.persist(board);
            em.flush();
            em.detach(board);
            em.merge(board); // board의 cnt는 null이므로 cnt가 null로 갱신
            em.flush();

        } catch (Exception e) {
            e.printStackTrace();
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

위에 코드를 실행하고 DB를 확인하면 cnt 값이 null이라는 것을 확인할 수 있습니다.

삭제 상태(Removed)

EntityManger의 remove() 메소드로 Entity를 삭제하면 해당 Entity는 영속성 컨텍스트에서 제외되고 DELETE Query가 SQL 저장소에 저장됩니다.

img20

EntityManager.remove()
Board removeBoard = em.find(Board.class, 2L);
em.remove(removeBoard);

JPQL(Java Persistence Query Language)

특정 데이터의 조회는 EntityManager의 find() 메소드를 사용하면 되지만, 목록을 조회하려면 JPQL 이라는 JPA에서 제공하는 별도의 쿼리 명령어를 사용해야 합니다. SQL문과 유사한 문법을 사용하므로 해석에는 특별한 어려움이 없지만 중요한 것은 검색 대상이 테이블이 아니라 Entity라는 점입니다.

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JPAClient {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("practice");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        try {

            tx.begin();

            for(int i = 0; i < 10; i++) {
                Board board = Board.builder()
                .title("테이블 제목" + i)
                .writer("Rubisco")
                .content(i + "번째 글입니다.")
                .build();
            
                em.persist(board);
            }
            
            tx.commit();

            String jpql = "select b from Board b order by b.seq desc";
            List<Board> BoardList = em.createQuery(jpql, Board.class).getResultList();

            for(Board brd : BoardList) {
                System.out.println("---> " + brd.getTitle());
            }

        } catch (Exception e) {
            e.printStackTrace();
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

Spring Data JPA

Spring Data JPA는 JPA를 쉽게 사용하기 위해 Spring에서 제공하는 프레임워크 이며, JPA의 인터페이스 입니다. JPA는 아니지만 보통 JPA라고 하면 Spring Data JPA를 뜻합니다.

img03

Spring Data JPA, Spring Data MongoDB, Spring Data Redis등 Spring Data의 하위 프로젝트들은 동일한 인터페이스를 가지고 있어서 Repository를 교체해도 기본적인 기능이 변하지 않습니다.

프로젝트 생성

빌드 도구를 Gradle로 하여 프로젝트를 만들겠습니다. 의존성 패키지로 spring-boot-starter-data-jpa를 추가합니다.

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

전체 코드는 다음과 같습니다.

build.gradle
plugins {
    id 'org.springframework.boot' version '2.7.2'
    id 'io.spring.dependency-management' version '1.0.12.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {

    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'junit:junit'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 오라클DB 접근
    runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
    implementation 'com.oracle.database.security:oraclepki'
    implementation 'com.oracle.database.security:osdt_core'
    implementation 'com.oracle.database.security:osdt_cert'
    
}

tasks.named('test') {
    useJUnitPlatform()
}

프로퍼티 설정

스프링 데이터 JPA는 persistence.xml 파일에 작성한 설정과 동일하게 application.properties 파일을 작성하면 됩니다. 아래와 같이 작성합시다.

# DataSource Setting
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=DB 주소
spring.datasource.username=DB 아이디
spring.datasource.password=DB 암호

# JPA Setting
spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.generate-ddl=false
spring.jpa.show-sql=true
spring.jpa.database=oracle
spring.jpa.database-platform=org.hibernate.dialect.OracleDialect
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=false

# Logging Setting
logging.level.org.hibernate=info

Entity 설정

Entity 매핑을 설정합니다. 위에 작성한 Board 클래스와 동일합니다.

Board.java
import lombok.*;
import javax.persistence.*;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import java.util.Date;

@Getter
@ToString
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicInsert
@DynamicUpdate

public class Board {
    
    @Id @GeneratedValue
    private Long seq;

    @Setter
    @Column(nullable = false)
    private String title;

    @Column(length = 40, nullable = false)
    private String writer;

    @Setter
    @Column(columnDefinition = "varchar(100) default 'No Contents'")
    private String content;

    @Temporal(TemporalType.DATE)
    @Column(updatable = false)
    private Date createDate;

    @ColumnDefault("0")
    private Long cnt;

    @PrePersist
    protected void onCreate() {
        createDate = new Date();
    }
}

spring.jpa.hibernate.ddl-auto 속성값을 create로 설정했기 때문에 Entity를 기준으로 테이블이 자동생성됩니다. 메인 클래스를 하나 만들어 다음과 같은 코드를 작성해주세요.

SpringDataJpaApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringDataJpaApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SpringDataJpaApplication.class);
        application.setWebApplicationType(WebApplicationType.NONE);
        application.run(args);

    }
}

WebApplicationType을 NONE으로 설정하여 내장 톰캣을 껐습니다. 메인 클래스를 실행하면 다음과 같은 내용이 콘솔창에 출력됩니다.

Hibernate: 
    
    drop table board cascade constraints
Hibernate: 
    
    drop sequence hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: 
    
    create table board (
       seq number(19,0) not null,
        cnt number(19,0) default 0,
        content varchar(100) default 'No Contents',
        create_date date,
        title varchar2(255) not null,
        writer varchar2(40) not null,
        primary key (seq)
    )

시퀀스와 테이블이 자동으로 생성되었으며, createData 필드가 스네이크 표기법으로 바뀌었습니다. (create_data)

ddl-auto가 create 이므로 테이블이 삭제되고 다시 생성됨을 확인할 수 있습니다.

Repository 인터페이스 작성

Entity의 CRUD 기능을 처리하기 위한 Repository 인터페이스를 작성하겠습니다. Repository는 위에서 설명했듯이 기존 DAO와 동일한 개념으로, 비즈니스 로직층에서는 Repository를 통해 DB 연동을 처리합니다. 스프링에서 제공하는 Repository 중 하나를 상속하여 작성하면 됩니다.

img21

일반적으로 CrudRepository를 사용하며, 이름에서 알 수 있듯이 CRUD 기능을 제공합니다.

만약 검색 기능이 필요하고 페이징 처리를 하고자 한다면 PagingAndSortingRepository를 사용하고, Spring Data JPA에서 추가한 기능을 사용하고 싶으면 JpaRepository를 사용하면 됩니다.

CrudRepository는 다음과 같이 T, ID 2개의 제네릭 타입을 지정해야 합니다.

CrudRepository<T, ID>

T는 Entity 클래스, ID는 식별자(Id)의 타입을 지정합니다.

BoardRepository.java
import org.springframework.data.repository.CrudRepository;

public interface BoardRepository extends CrudRepository<Board, Long> {
    
}

CRUD 기능 테스트

CrudRepository 인터페이스가 제공하는 메소드는 다음과 같습니다.

메소드 반환형 기능
count() long 모든 Entity 개수 리턴
delete(ID) void 식별키 삭제
delete(Iterable<?Extends T>) void 주어진 모든 Entity 삭제
deleteAll() void 모든 Entity 삭제
exists(ID) boolean 식별키를 가진 Entity 존재 확인
findAllById(ID) Iterable<T> 모든 Entity 목록 리턴
findAll(Iterable<ID>) Iterable<T> 해당 식별키를 가진 Entity 목록 리턴
findById(ID) Optional<T> 해당 식별키에 해당하는 Entity 리턴
saveAll<Iterable> <S extends T>Iterable<S> 여러 Entity들을 한 번에 등록, 수정
save<S entity> <S extends T>S 하나의 Entity를 등록, 수정

CRUD 기능 테스트를 하기 전에 application.properties에서 spring.jpa.hibernate.ddl-autoupdate로 바꿔주세요.

spring.jpa.hibernate.ddl-auto=update

등록(Create)

src/test/java 폴더에 Junit 기반의 BoardRepositoryTest라는 테스트 케이스를 작성합니다.

BoardRepositoryTest.java
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTest {
    @Autowired
    private BoardRepository boardRepo;

    @Test
    public void testInsertBoard() {
        Board board = Board.builder()
                        .title("테이블 제목")
                        .writer("Rubisco")
                        .content("첫번째 글입니다.")
                        .build();
        
        boardRepo.save(board);
    }
}

JPA에서는 persist() 메소드로 INSERT를 했지만, CrudRepository 인터페이스는 save() 메소드를 통해 INSERT를 합니다.

Hibernate: 
    select
        hibernate_sequence.nextval 
    from
        dual
Hibernate: 
    insert 
    into
        board
        (content, create_date, title, writer, seq) 

시퀀스를 하나 증가시킨 후 INSERT 쿼리가 실행됨을 확인할 수 있습니다.

조회(Read)

조회 기능을 테스트 하기 위해 testGetBoard라는 메소드를 작성합니다.

BoardRepositoryTest.java
@Test
public void testGetBoard() {
    Board board = boardRepo.findById(1L).get();
    System.out.println(board.toString());
}

데이터 하나를 조회하기 위해서는 findById() 메소드를 사용합니다. 그러면 Optional 타입의 객체가 리턴되며, get() 메소드를 통해 영속성 컨텍스트에 저장된 Board 객체를 받을 수 있습니다.

Hibernate: 
    select
        board0_.seq as seq1_0_0_,
        board0_.cnt as cnt2_0_0_,
        board0_.content as content3_0_0_,
        board0_.create_date as create_d4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer as writer6_0_0_ 
    from
        board board0_ 
    where
        board0_.seq=?
Board(seq=1, title=제목 수정, writer=Rubisco, content=첫번째 글입니다., createDate=2022-08-08, cnt=0)

테스트 케이스를 실행하면 SELECT 쿼리가 실행되고, 터미널에 seq=1인 Entity의 속성이 출력되는 것을 확인할 수 있습니다.

수정(Update)

수정 기능을 테스트 하기 위해 testUpdateBoard 라는 메소드를 작성합니다.

BoardRepositoryTest.java
@Test
public void testUpdateBoard() {
    Board board = boardRepo.findById(1L).get();
    board.setTitle("제목 수정");
    boardRepo.save(board);
}

JPA에서는 merge() 메소드를 사용했지만, CrudRepository는 INSERT 할때와 동일하게 save() 메소드를 사용합니다.

Hibernate: 
    select
        board0_.seq as seq1_0_0_,
        board0_.cnt as cnt2_0_0_,
        board0_.content as content3_0_0_,
        board0_.create_date as create_d4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer as writer6_0_0_ 
    from
        board board0_ 
where
        board0_.seq=?
Hibernate: 
    select
        board0_.seq as seq1_0_0_,
        board0_.cnt as cnt2_0_0_,
        board0_.content as content3_0_0_,
        board0_.create_date as create_d4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer as writer6_0_0_ 
    from
        board board0_ 
    where
        board0_.seq=?
Hibernate: 
    update
        board 
    set
        title=? 
    where
        seq=?

수정할 Entity를 영속성 컨텍스트에 저장하기 위해 처음 SELECT 구문을 실행했으며, 수정하기 직전 다시 한번 수정할 Entity를 영속성 컨텍스트에 저장하고 수정 작업이 이루어지기 때문에 2번의 SELECT와 1번의 UPDATE 쿼리가 실행되었음을 확인할 수 있습니다.

삭제(Delete)

삭제 기능을 테스트 하기 위해 testDeleteBoard 라는 메소드를 작성합니다.

BoardRepositoryTest.java
@Test
public void testDeleteBoard() {
    boardRepo.deleteById(1L);
}

단 한줄입니다.. JPA도 구문이 간단하다고 느꼈는데 Spring Data JPA는 더욱 단순화 되고 직관적입니다.

Hibernate: 
    select
        board0_.seq as seq1_0_0_,
        board0_.cnt as cnt2_0_0_,
        board0_.content as content3_0_0_,
        board0_.create_date as create_d4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer as writer6_0_0_ 
    from
        board board0_ 
    where
        board0_.seq=?
Hibernate: 
    delete 
    from
        board 
    where
        seq=?

수정과 마찬가지로 SELECT 쿼리를 통해 Entity를 영속성 컨텍스트에 저장한 후 DELETE 쿼리가 실행됨을 확인할 수 있습니다.


Hibernate로 CRUD를 만들때는 EntityManagerFactory를 짓고 EntityManager를 생성하여 EntityManager를 통해 Entity의 CRUD를 처리했습니다.

반면 Spring Data JPA로 CRUD를 만들때는 EntityManager가 등장하지 않았습니다. EntityManager는 Repository에 포함되어있어 직접 작성하지 않아도 내부에서 자동적으로 호출되는 모양새 입니다.

아직 완전히 이해하진 못했지만 Spring Data JPA를 사용하면 효율적인 개발이 될 것임은 확실해 보입니다.