Light Blue Pointer
본문 바로가기
Developing/TIL(Develop)

[JPA Core]

by Greedy 2023. 11. 15.

JPA Core - Java 환경에서의 JPA

JPA란 무엇일까?

DB를 직접 다룰 때의 문제점

  1. 애플리케이션에서 아래 형태의 객체 데이터를 DB에 저장해서 관리해야 하는 상황
public class Memo {
    private Long id;
    private String username;
    private String contents;
}
  1. DB 테이블 만들기
create table memo (
id bigint not null auto_increment,
contents varchar(500) not null,
username varchar(255) not null,
primary key (id)
);

이처럼 직접 DB로 접속해서 SQL을 호출해야합니다.

  1. 애플리케이션에서 SQL 작성
String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
String sql = "SELECT * FROM memo";
  1. SQL을 JDBC를 사용해서 직접 실행해줘야 합니다.
jdbcTemplate.update(sql, "Robbie", "오늘 하루도 화이팅!");
jdbcTemplate.query(sql, ...);
  1. SQL 결과를 객체로 직접 만들어줘야 합니다.
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
		// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
		Long id = rs.getLong("id");
		String username = rs.getString("username");
		String contents = rs.getString("contents");
		return new MemoResponseDto(id, username, contents);
}

→ 문제 : SQL 의존적이라 변경에 취약

메모 데이터에 비밀번호를 추가해야 한다면

public class Memo {
    private Long id;
    private String username;
    private String contents;
		
		// 비밀번호 추가
		private String password;
}
  1. SQL을 직접 수정해야 합니다.
String sql = "INSERT INTO memo (username, contents, password) VALUES (?, ?, ?)";
  1. MemoResponseDto 객체에 값을 넣어주는 부분도 당연히 추가해줘야 합니다.
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
		// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
		Long id = rs.getLong("id");
		String username = rs.getString("username");
		String contents = rs.getString("contents");
		String password = rs.getString("password");
		return new MemoResponseDto(id, username, contents, password);
}

ORM 이 없는 환경에서는 백엔드 개발자가 비즈니스 로직 개발보다 SQL 작성 및 수정에 더 많은 노력을 들여야 했었다고 함

반복적이고 번거로운 애플리케이션 단에서의 SQL 작업을 줄여주기 위해서 ORM(객체 관계 매핑)기술들이 등장

ORM : Object-Relational Mapping

Object : **"객체"**지향 언어 (자바, 파이썬) Relational : "관계형" 데이터베이스 (H2, MySQL)

ORM : Object-Relational Mapping 객체와 DB의 관계를 매핑 해주는 도구

객체 즉, 자바의 클래스와 DB의 데이터를 직접 매핑 하려면 앞서 살펴본 것 처럼 매우 번거롭고 많은 작업들이 필요했지만 ORM을 사용하면 이를 자동으로 처리

JPA: Java Persistence API

JPA : Java ORM 기술의 대표적인 표준 명세

JPA는 애플리케이션과 JDBC 사이에서 동작

JPA를 사용하면 DB 연결 과정을 직접 개발하지 않아도 자동으로 처리해줌

객체를 통해 간접적으로 DB 데이터를 다룰 수 있기 때문에 매우 쉽게 DB 작업을 처리할 수 있다.

하이버네이트(Hibernate)란?

JPA 는 표준 명세이고, 이를 실제 구현한 프레임워크 중 (사실상**) 표준 :** 하이버네이트

스프링 부트에서는 기본적으로 ‘하이버네이트’ 구현체를 사용 중입니다.

사실상 표준 (de facto, 디팩토) 보통 기업간 치열한 경쟁을 통해 시장에서 결정되는 비 공식적 표준

Entity

JPA에서 관리되는 클래스 즉, 객체

Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리됨

Spring과 Java가 다르다고 함!

프로젝트 생성(Java 환경 먼저)

IntelliJ → New Project → jpa-core

Language : Java

Build system : Gradle

Gradle DSL : Groovy

src → main → resources → New → Directory → META-INF

META-INF → New file → persistence.xml

persistence.xml


    
        com.sparta.entity.Memo
        
            
            
            
            

            

            
            
            
        
    

File is not configured as JPA facet descriptor

Create JPA facet and add file to its configuration

build.gradle → dependencies에 추가

// JPA 구현체인 hibernate
implementation 'org.hibernate:hibernate-core:6.1.7.Final'
// MySQL
implementation 'mysql:mysql-connector-java:8.0.28'

IntelliJ 에 mySQL memo db 전에 만든거 연결

memo table 우클릭 → drop

test → java → EntityTest 클래스 만들어줌

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class EntityTest {

    EntityManagerFactory emf;
    EntityManager em;

    @BeforeEach
    void setUp() {
        emf = Persistence.createEntityManagerFactory("memo");
        em = emf.createEntityManager();
    }

    @Test
    void test1() {

    }
}

org.example → com.sparta로 refactor

com.sparta → Main 지움, entity 패키지 생성 → Memo 클래스 생성

package com.sparta.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
    @Id
    private Long id;

    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용)
    @Column(name = "username", nullable = false, unique = true)
    private String username;

    // length: 컬럼 길이 지정
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;
}

JPA가 Entity 클래스를 인스턴스화 할 때 기본생성자를 사용하기 때문에

현재의 entity 클래스에 기본생성자가 포함이 되었는지 반드시 확인해야 한다

@Entity(name = "Memo")는
@Entity 와 같다
 // length: 컬럼 길이 지정
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;

name = "contents" 안 써도 똑같다고 함

unique 중복 허용(false) 여부

nullable 의 default는 true임

length 길이 default = 255

Java에서 JPA를 사용하려면

persistence.xml을 설정해줘야 한다고 함


    
        com.sparta.entity.Memo
        
            
            
            
            

            

            
            
            
        
    

JPA 부분

<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <property name="jakarta.persistence.jdbc.password" value="58155815"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>
<property name="hibernate.hbm2ddl.auto" value="create" />

<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>

EntityTest 실행

public class EntityTest {

    EntityManagerFactory emf;
    EntityManager em;

    @BeforeEach
    void setUp() {
        emf = Persistence.createEntityManagerFactory("memo");
        em = emf.createEntityManager();
    }

    @Test
    void test1() {

    }
}

Annotation 으로 준 제약조건에 일치하게 테이블 만들어줌

Hibernate:

drop table if exists memo

Nov 10, 2023 5:08:24 PM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@5e50df2e] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode. Hibernate:

create table memo (
   id bigint not null,
    contents varchar(500) not null,
    username varchar(255) not null,
    primary key (id)
) engine=InnoDB

Nov 10, 2023 5:08:24 PM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@44aa2e13] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode. Hibernate:

alter table memo
   add constraint UK_n3qr0n9og6katwb84yqcia5ib unique (username)

Nov 10, 2023 5:08:24 PM org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]

Task :test

autoincrement가 없으면 항상 max id 찾아야 함 primary key라서

이럴때 쉽게 autoincrement 하는 방법

@GeneratedValue(strategy = GenerationType.IDENTITY)

  	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

identity 를 걸고나면 어떻게 옵션이 변하는지 본대

Hibernate:

drop table if exists memo

Nov 10, 2023 10:05:28 PM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@5e1a986c] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode. Hibernate:

여기에 auto increment가 추가됨

create table memo (
   id bigint not null **auto_increment**,
    contents varchar(500) not null,
    username varchar(255) not null,
    primary key (id)
) engine=InnoDB

Nov 10, 2023 10:05:28 PM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@79957f11] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode. Hibernate:

alter table memo
   add constraint UK_n3qr0n9og6katwb84yqcia5ib unique (username)

Nov 10, 2023 10:05:28 PM org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]

Task :test

JPA에서 두가지를 생각해야 한다고 함

테이블이 없는 상태에서 테이블을 만들 때

이미 테이블이 만들어져있고 그 테이블을 Entity랑 매핑할 때

→ 어떤 옵션과 constraints들이 걸려있는지 보고 가래

영속성 컨텍스트 Persistence Context

Persistence : 영속성, 지속성

객체에서 의 Persistence : 객체가 생명이나 공간을 자유롭게 유지하고 이동할 수 있는 객체의 성질

JPA는 영속성 컨텍스트에 Entity 객체들을 저장하여 관리하면서 DB와 소통 → 개발자들은 이제 직접 SQL을 작성하지 않아도 JPA를 사용하여 DB에 데이터를 저장,조회,수정,삭제 가능

영속성 컨텍스트 : Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간

EntityManager : Entity를 관리하는 관리자, 영속성 컨텍스트에 접근하여 Entity 객체들을 조작

개발자들은 EntityManager를 사용해서 Entity를 저장, 조회, 수정, 삭제

EntityManager는 EntityManagerFactory를 통해 생성하여 사용, DB당 하나씩 생성, 애플리케이션이 동작하는 동안 사용

정보를 전달하기 위해서는 /resources/META-INF/ 위치에 persistence.xml 파일을 만들어 정보를 넣어두면 됨


    ->가지고올때 memo로 가지고 옴
        com.sparta.entity.Memo
        
            
            
            
            

            

            
            
            
        
    

EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo"); -> 메모로 가져옴
EntityManager em = emf.createEntityManager();
  • EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
  • 해당 코드를 호출하면 JPA는 persistence.xml 의 정보를 토대로 EntityManagerFactory를 생성
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class EntityTest {

    EntityManagerFactory emf;
    EntityManager em;

    @BeforeEach
    void setUp() {
        emf = Persistence.createEntityManagerFactory("memo");
        em = emf.createEntityManager();
    }

    @Test
    void test1() {

    }
}

Transaction

트랜잭션은 DB 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적 개념

여러 개의 SQL이 하나의 트랜잭션에 포함될 수 있음

모든 SQL이 성공적으로 수행이 되면 DB에 영구적으로 변경을 반영하지만 SQL 중 단 하나라도 실패한다면 모든 변경을 되돌림

트랜잭션 실습

MySQL 콘솔에 다음 코드를 입력

START TRANSACTION; # 트랜잭션을 시작합니다.

INSERT INTO memo (id, username, contents) VALUES (1, 'Robbie', 'Robbie Memo');
INSERT INTO memo (id, username, contents) VALUES (2, 'Robbert', 'Robbert Memo');
SELECT * FROM memo;

COMMIT; # 트랜잭션을 커밋합니다.

SELECT * FROM memo;

COMMIT 이전에는 intelliJ에서는 볼 수 있지만 MySQL shell에서 조회하면 memo가 비었다고 뜸

COMMIT 이후에는 MySQL shell에서 조회해도 뜸

Entity manager 에 있는 action queue 에 저장했다가 db에 commit

autoincreament 빼고

create로 교체 :<property name="hibernate.hbm2ddl.auto" value="create" />

test1 run 말고 debug 해보라고함

오 코드 안에 한글이 다 깨짐

file encoding → project → utf-8로 해봄 → 성공적

@Test
@DisplayName("EntityTransaction 성공 테스트")
void test1() {
    EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.

    et.begin(); // 트랜잭션을 시작합니다.

    try { // DB 작업을 수행합니다.

        Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
        memo.setId(1L); // 식별자 값을 넣어줍니다.
        memo.setUsername("Robbie");
        memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");

        em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.

        et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
        // commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
    } finally {
        em.close(); // 사용한 EntityManager 를 종료합니다.
    }

    emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}
  • JPA에서 이러한 트랜잭션의 개념을 적용하기 위해서는 EntityManager에서 EntityTransaction을 가져와 트랜잭션을 적용할 수 있다.
    • EntityTransaction et = em.getTransaction();
    • 해당 코드를 호출하여 EntityTransaction을 가져와 트랜잭션을 관리
  • et.begin();
    • 트랜잭션을 시작하는 명령
  • et.commit();
    • 트랜잭션의 작업들을 영구적으로 DB에 반영하는 명령어
  • et.rollback();
    • 오류가 발생했을 때 트랜잭션의 작업을 모두 취소하고, 이전 상태로 되돌리는 명령어
@Test
@DisplayName("EntityTransaction 실패 테스트")
void test2() {
    EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.

    et.begin(); // 트랜잭션을 시작합니다.

    try { // DB 작업을 수행합니다.

        Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
        memo.setUsername("Robbert");
        memo.setContents("실패 케이스");

        em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.

        et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
        // commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
    } catch (Exception ex) {
        System.out.println("식별자 값을 넣어주지 않아 오류가 발생했습니다.");
        ex.printStackTrace();
        et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
    } finally {
        em.close(); // 사용한 EntityManager 를 종료합니다.
    }

    emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}

→ db에 반영이 안 됨

 

영속성 컨텍스트의 기능

영속성 컨텍스트 : Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간

영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있다.

캐시 저장소(1차 캐시)

우리가 저장하는 Entity 객체들이 저장됨

Map 자료구조 형태로 되어 있다

key에는 @Id로 매핑한 기본 키 즉, 식별자 값을 저장

value에는 해당 Entity 클래스의 객체를 저장

영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자값을 사용하여 Entity 객체를 구분하고 관리

em.persist(memo); 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장

EntityManagerFactory emf;
    EntityManager em;

    @BeforeEach
    void setUp() {
        emf = Persistence.createEntityManagerFactory("memo");
        em = emf.createEntityManager();
    }

 

Entity 저장

@Test
@DisplayName("1차 캐시 : Entity 저장")
void test1() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo();
        memo.setId(1L);
        memo.setUsername("Robbie");
        memo.setContents("1차 캐시 Entity 저장");

        em.persist(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

 

em > persistenceContext > 비어있음

 

실행 이후

em > persistenceContext > entitiesBykey를 확인해 보시면key-value 형태로 정보가 저장되어있음을 확인

 

Entity 조회

  1. 캐시 저장소에 조회하는 Id가 존재하지 않은 경우
  1. 캐시 저장소 조회
  2. DB SELECT 조회 후 캐시 저장소에 저장
  3. em.find(Memo.class, 1); 호출 → 캐시 저장소를 확인 한 후 해당 값이 없으면
  4. DB에 SELECT 조회 후 해당 값을 캐시 저장소에 저장하고 반환

memo 조회 Id가 존재하지 않는 예제)

@Test
@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하지 않은 경우")
void test2() {
    try {

        Memo memo = em.find(Memo.class, 1);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        em.close();
    }

    emf.close();
}

DB에서 데이터를 조회만 하는 경우에는 데이터의 변경이 발생하는 것이 아니기 때문에 트랜잭션이 없어도 조회가 가능

Memo memo = em.find(Memo.class, 1); 호출 시 캐시 저장소에 해당 값이 존재하지 않기 때문에 DB에 SELECT 조회하여 캐시 저장소에 저장한 후 반환

  1. 캐시 저장소에 조회하는 Id가 존재하는 경우

em.find(Memo.class, 1); 호출 시 캐시 저장소에 식별자 값이 1이면서 Memo Entity 타입인 값이 있는지 조회

값이 있음 → 해당 Entity 객체를 반환

memo 조회 Id가 존재하는 예제)

@Test
@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하는 경우")
void test3() {
    try {

        Memo memo1 = em.find(Memo.class, 1);
        System.out.println("memo1 조회 후 캐시 저장소에 저장\\n");

        Memo memo2 = em.find(Memo.class, 1);
        System.out.println("memo2.getId() = " + memo2.getId());
        System.out.println("memo2.getUsername() = " + memo2.getUsername());
        System.out.println("memo2.getContents() = " + memo2.getContents());

    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        em.close();
    }

    emf.close();
}

Memo memo1 = em.find(Memo.class, 1); 호출 때는 캐시 저장소에 존재하지 않기 때문에 DB에 SELECT 조회하여 캐시 저장소에 저장합니다.

Memo memo2 = em.find(Memo.class, 1); 를 호출 했을 때는 이미 캐시 저장소에 해당 값이 존재하기 때문에 DB에 조회하지 않고 캐시 저장소에서 해당 값을 반환합니다.

1차 캐시(First level cache) 사용의 장점

  1. DB 조회 횟수를 줄임
  2. **'1차 캐시'**를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장 (객체 동일성 보장)

객체 동일성 보장

@Test
@DisplayName("객체 동일성 보장")
void test4() {
    EntityTransaction et = em.getTransaction();

    et.begin();
    
    try {
        Memo memo3 = new Memo();
        memo3.setId(2L);
        memo3.setUsername("Robbert");
        memo3.setContents("객체 동일성 보장");
        em.persist(memo3);

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 1);
        Memo memo  = em.find(Memo.class, 2);

        System.out.println(memo1 == memo2);
        System.out.println(memo1 == memo);

        et.commit();
    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

같은 값을 조회하는 memo1과 memo2는 == 결과 true를 반환

memo1과 다른 값을 조회하는 memo는 == 결과 false를 반환

Entity 삭제

  1. 삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB를 조회한 후 저장
  2. em.remove(entity);
  3. em.remove(memo); 호출 시 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit 후 Delete SQL이 DB에 요청됨

ex) memo 삭제

@Test
@DisplayName("Entity 삭제")
void test5() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 2);

        em.remove(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.remove(memo); 호출되면서 memo Entity 객체를 DELETED 상태로 만들고 트랜잭션 commit 후 Delete SQL이 DB에 요청됨

em.find(Memo.class, 2); 호출, memo 객체를 캐시 저장소에 저장

후 memo Entity 객체: MANAGED 상태(entityEntry에서 확인)

em.remove(memo); 호출 후 memo Entity 객체: DELETED 상태

트랜잭션 commit 후 DB 데이터를 확인 → 해당 데이터가 삭제됨

ActionQueue(쓰기 지연 저장소)

JPA는 ActionQueue를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영

Ex) ActionQueue

@Test
@DisplayName("쓰기 지연 저장소 확인")
void test6() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        Memo memo = new Memo();
        memo.setId(2L);
        memo.setUsername("Robbert");
        memo.setContents("쓰기 지연 저장소");
        em.persist(memo);

        Memo memo2 = new Memo();
        memo2.setId(3L);
        memo2.setUsername("Bob");
        memo2.setContents("과연 저장을 잘 하고 있을까?");
        em.persist(memo2);

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em > actionQueue를 확인해보면 insertions > executables에 Insert할 memo#2, memo#3 Entity 객체 2개가 들어가 있는 것을 확인할 수 있다.

트랜잭션 commit 후 )

actionQueue에 있던 insertions 데이터가 사라짐

실제로 기록을 확인해보면 트랜잭션 commit 호출 전까지는 SQL 요청이 없다가 트랜잭션 commit 후 한번에 Insert SQL 2개가 순서대로 요청된 것을 확인할 수 있다.

flush()

트랜잭션 commit 후 쓰기 지연 저장소의 SQL들이 한번에 요청됨을 확인

트랜잭션 commit 후 추가적인 동작이 있는데 바로 em.flush(); 메서드의 호출

flush 메서드는 영속성 컨텍스트의 변경 내용들을 DB에 반영 = Action Queue의 SQL들을 DB에 요청

ex ) flush() 동작 확인을 위해 직접 호출

@Test
@DisplayName("flush() 메서드 확인")
void test7() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        Memo memo = new Memo();
        memo.setId(4L);
        memo.setUsername("Flush");
        memo.setContents("Flush() 메서드 호출");
        em.persist(memo);

        System.out.println("flush() 전");
        em.flush(); // flush() 직접 호출
        System.out.println("flush() 후\\n");
        

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.flush(); 메서드가 호출되자 바로 DB에 쓰기 지연 저장소의 SQL이 요청됨

→ 이미 쓰기 지연 저장소의 SQL이 요청 되었기 때문에 더 이상 요청할 SQL이 없어 트랜잭션이 commit 된 후에 SQL 기록이 보이지 않음

추가) 트랜잭션을 설정하지 않고 플러시 메서드를 호출하면 no transaction is in progress 메시지와 함께 TransactionRequiredException 오류가 발생

Insert, Update, Delete 즉, 데이터 변경 SQL을 DB에 요청 및 반영하기 위해서는 트랜잭션이 필요

변경 감지(Dirty Checking)

영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL이 쓰기 지연 저장소에 저장된다면?

하나의 Update SQL로 처리할 수 있는 상황을 여러번 Update SQL을 요청하게 되기 때문에 비효율적입니다.

그렇다면 JPA는 어떻게 Update를 처리할까요?

em.update(entity); 같은 메서드를 지원할 것 같지만 찾아볼 수 없습니다.

JPA는 영속성 컨텍스트에 Entity를 저장할 때 최초 상태(LoadedState)를 저장

트랜잭션이 commit되고 em.flush(); 가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교

변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 모든 쓰기지연 저장소의 SQL을 DB에 요청

마지막으로 DB의 트랜잭션이 commit 되면서 반영됨

변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고 해당 Entity 객체의 데이터를 변경하면 자동으로 Update SQL이 생성되고 DB에 반영됨

ex) memo 데이터를 조회한 후 변경

@Test
@DisplayName("변경 감지 확인")
void test8() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        System.out.println("변경할 데이터를 조회합니다.");
        Memo memo = em.find(Memo.class, 4);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        System.out.println("\\n수정을 진행합니다.");
        memo.setUsername("Update");
        memo.setContents("변경 감지 확인");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

entityInstance는 Entity 객체의 현재 상태입니다.

entityEntry > loadedState는 조회했을 때 즉, 해당 Entity의 최초 상태입니다.

트랜잭션 commit 후 em.flush(); 메서드가 호출되면 현재 상태와 최초 상태를 비교하고 변경이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장한 후 DB에 요청

 

Entity의 상태

비영속(Transient)

Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("비영속과 영속 상태");

new 연산자를 통해 인스턴스화 된 Entity 객체를 의미합니다.

아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA가 관리 X

영속(Managed)

em.persist(memo);

persist(entity) : 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만듭니다.

ex) 비영속과 영속 상태

@Test
@DisplayName("비영속과 영속 상태")
void test1() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo(); // 비영속 상태
        memo.setId(1L);
        memo.setUsername("Robbie");
        memo.setContents("비영속과 영속 상태");

        em.persist(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

비영속 상태이기 때문에 entitiesByKey=null 입니다.

비영속 상태는 JPA가 관리하지 못하기 때문에 해당 객체의 데이터를 변경해도 변경 감지가 이루어지지 않습니다.

em.persist(memo); 메서드 호출 후 영속성 컨텍스트에 저장되었고 MANAGED 상태 즉, JPA가 관리하는 영속 상태의 Entity가 되었습니다.

준영속(Detached)

준영속 상태는 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미

영속 상태에서 준영속 상태로 바꾸는 방법

detach(entity)

em.detach(memo);

detach(entity) : 특정 Entity만 준영속 상태로 전환

영속성 컨텍스트에서 관리되다(Managed)가 ****분리된 상태(Detached)로 전환됨

@Test
@DisplayName("준영속 상태 : detach()")
void test2() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 1);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("detach() 호출");
        em.detach(memo);
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("memo Entity 객체 수정 시도");
        memo.setUsername("Update");
        memo.setContents("memo Entity Update");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.find(Memo.class, 1); 메서드 호출 후 MANAGED 상태

em.detach(memo); 메서드를 호출하여 특정 Entity 객체 Memo#1를 영속성 컨텍스트에서 제거

준영속 상태로 전환되면 1차 캐시 즉, 캐시 저장소에서 제거되기 때문에 JPA의 관리를 받지 못해 영속성 컨텍스트의 어떠한 기능도 사용할 수 없다

따라서 memo Entity 객체의 데이터를 수정해도 변경감지 기능을 사용할 수 없어 Update SQL이 수행되지 않는다.

em.contains(memo); 는 해당 객체가 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드로 em.detach(memo); 이후 확인했을 때 false가 출력된 것을 확인할 수 있다.

clear()

em.clear();

clear() : 영속성 컨텍스트를 완전히 초기화합니다.

영속성 컨텍스트의 모든 Entity를 준영속 상태로 전환합니다.

영속성 컨텍스트 틀은 유지하지만 내용은 비워 새로 만든 것과 같은 상태가 됩니다.

따라서 계속해서 영속성 컨텍스트를 이용할 수 있습니다.

@Test
@DisplayName("준영속 상태 : clear()")
void test3() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 2);

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("clear() 호출");
        em.clear();
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("memo#1 Entity 다시 조회");
        Memo memo = em.find(Memo.class, 1);
        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("\\n memo Entity 수정 시도");
        memo.setUsername("Update");
        memo.setContents("memo Entity Update");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.clear(); 메서드 호출 후 완전히 비워진 영속성 컨텍스트를 확인

다시 memo#1 Entity를 조회하여 영속성 컨텍스트에 저장된 것을 확인

em.clear(); 메서드 호출 후 em.contains(memo1,2); 확인했을 때 false가 출력된 것을 확인할 수 있다.

다시 memo#1 Entity를 조회한 후 em.contains(memo); 확인했을 때 true가 출력된 것을 확인할 수 있다.

또한 memo Entity 객체의 데이터를 수정하자 트랜잭션 commit 후 Update SQL이 수행된 것을 확인할 수 있다.

close()

em.close();

close() : 영속성 컨텍스트를 종료합니다.

해당 영속성 컨텍스트가 관리하던 영속성 상태의 Entity들은 모두 준영속 상태로 변경된다.

영속성 컨텍스트가 종료되었기 때문에 계속해서 영속성 컨텍스트를 사용할 수 없다.

@Test
@DisplayName("준영속 상태 : close()")
void test4() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 2);

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("close() 호출");
        em.close();
        Memo memo = em.find(Memo.class, 2); // Session/EntityManager is closed 메시지와 함께 오류 발생
        System.out.println("memo.getId() = " + memo.getId());

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.close(); 메서드 호출 이후 EntityManager를 사용하려고 하자 오류가 발생

영속성 컨텍스트가 종료되면 계속해서 영속성 컨텍스트를 사용할 수 없다

merge(entity)

em.merge(memo);

merge(entity) : 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환

merge(entity) 동작

파라미터로 전달된 Entity의 식별자 값으로 영속성 컨텍스트를 조회합니다.

해당 Entity가 영속성 컨텍스트에 없다면?

  1. DB에서 새롭게 조회합니다.
  2. 조회한 Entity를 영속성 컨텍스트에 저장합니다.
  3. 전달 받은 Entity의 값을 사용하여 병합합니다.
  4. Update SQL이 수행됩니다. (수정)

만약 DB에서도 없다면 ?

  1. 새롭게 생성한 Entity를 영속성 컨텍스트에 저장합니다.
  2. Insert SQL이 수행됩니다. (저장)

따라서 merge(entity) 메서드는 비영속, 준영속 모두 파라미터로 받을 수 있으며 상황에 따라 ‘저장’을 할 수도 ‘수정’을 할 수도 있다.

  1. merge(entity) 저장
@Test
@DisplayName("merge() : 저장")
void test5() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo();
        memo.setId(3L);
        memo.setUsername("merge()");
        memo.setContents("merge() 저장");

        System.out.println("merge() 호출");
        Memo mergedMemo = em.merge(memo);

        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo));

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

em.merge(memo); 호출 후 영속성 컨텍스트에 Memo#3 객체가 저장되고 Insert SQL이 추가된 것을 확인

em.merge(memo); 호출 후 영속성 컨텍스트에 해당 값이 없어 DB에 조회 했는데도 해당 값이 없기 때문에 새롭게 생성하여 영속성 컨텍스트에 저장하고 Insert SQL이 수행됨

merge(entity) 수정

@Test
@DisplayName("merge() : 수정")
void test6() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 3);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("detach() 호출");
        em.detach(memo); // 준영속 상태로 전환
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("준영속 memo 값 수정");
        memo.setContents("merge() 수정");

        System.out.println("\\n merge() 호출");
        Memo mergedMemo = em.merge(memo);
        System.out.println("mergedMemo.getContents() = " + mergedMemo.getContents());

        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo));

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

detach() 메서드 호출로 조회해온 영속 상태의 memo 객체를 준영속 상태로 전환

준영속 상태의 memo의 값을 수정한 후 memo 객체를 사용해서 merge() 메서드를 호출

memo 객체는 준영속 상태이기 때문에 현재 영속성 컨텍스트에는 해당 객체가 존재하지 않음

따라서 DB에서 식별자 값을 사용하여 조회한 후 영속성 컨텍스트에 저장하고 파라미터로 받아온 준영속 상태의 memo 객체의 값을 새롭게 저장한 영속 상태의 객체에 병합하고 반환

그 결과 반환된 mergedMemo의 contents를 출력하였을 때 변경되었던 내용인 “merge() 수정”이 출력됨

트랜잭션 commit 후 Update SQL이 수행됨

준영속 상태의 Entity memo는 merge() 호출 후에도 영속성 컨텍스트에 저장되어 있지 않기 때문에 false가 반환되었고

새롭게 저장된 영속 상태의 객체를 반환받은 mergedMemo는 true가 반환됨

삭제(Removed)

em.remove(memo);

remove(entity) : 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환

'Developing > TIL(Develop)' 카테고리의 다른 글

Spring Bean  (0) 2023.11.20
JPA in Spring Boot  (0) 2023.11.15
IoC와 DI  (5) 2023.11.09
3 Layer Architecture 역할 분리  (1) 2023.11.09
Database&SQL&JDBC  (0) 2023.11.07