public class Memo {
private Long id;
private String username;
private String contents;
}
이와 같은 객체를 다루게된다면,
1. DB에 테이블을 같은 형식으로 만들고
2. 앱에서 SQL을 작성하고,
3. 그 SQL을 JDBC를 사용하여 직접 실행해주어야한다.
4. 또한, 그 SQL의 결과를 객체로 직접 만들어주는
번거로운 작업을 거치게된다.
또한, SQL 의존적이라 변경에 취약하다.
만약 위 Memo 객체에 password 필드 하나를 더 추가한다고 한다면,
sql도 직접 수정해야하고 MemoResponseDto객체에 값을 넣어주는 부분도 당연히추가해주어야한다.
이런식으로 단순하게 필드 하나 추가하는 것조차 해야하는 일들이 많아진다.
이처럼 ORM이 없는 환경에서는 백엔드 개발자가 비즈니스 로직 개발보다 SQL 작성 및 수정에 더 많은 노력을 기울였어야했는데 이를 해결한 것이 ORM이다.
Object-Relational Maaping의 약자로, 객체 관계 매핑이라고 보면된다.

이름 그대로 객체와 DB의 관계를 매핑해주는 도구를 말한다.
ORM 기술에 대한 대표적인 표준 명세를 JPA라고 부른다.
이는 Java Persistence API의 약자이다.

JPA는 애플리케이션과 JDBC 사이에서 동작되고 있으며, DB 연결 과정을 직접 개발하지 않아도 자동으로 처리해준다.
또한, 객체를 통해 간접적으로 DB데이터를 다룰 수 있기 때문에 매우 쉽게 DB 작업을 처리할 수 있다.

JPA는 표준 명세이고, 이를 실제 구현한 프레임워크 중 사실상 표준이 하이버네이트이다.
스프링 부트에서는 기본적으로 하이버네이트 구현체를 사요한다.
(사실상 표준 - de facto / 보통 기업간 치열한 경쟁을 통해 시장에서 결정되는 비 공식적 표준)

JPA에서 관리되는 클래스 즉, 객체를 의미한다.
Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리된다.
우리가 메모장 프로젝트에서 처음에 JDBC로 만들지만 데이터를 담는 패키지의 이름을 entity로 지었었는데 그 이유가 바로 이것이다.
또한, JPA의 entity클래스로 바꾸기 위해 해당 부분 코드를 바꾸며 일부분은 같이 정리할 예정이니 다음 포스트 참고바란다.
메모장 프로젝트 - JPA

Persistence를 한글로 번역하면 영속성, 지속성이라는 뜻이된다.
JPA의 P가 영속성이라는 뜻이다!
이를 객체의 관점으로 해석해보자면, 객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)를 자유롭게 유지하고 이동할 수 있는 객체의 성질을 의미한다.
쉽게 말해, Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이라고 생각하면 된다.

개발자들은 이제 직접 SQL을 작성하지 않아도 JPA를 사용하여 DB에 데이터를 저장하거나 조회할수 있으며, 수정,삭제도 가능해진다.

이러한 과정을 효율적으로 처리하기 위해 JPA는 영속성 컨텍스트에 Entity 객체들을 저장하여 관리하면서 DB와 소통한다.


EntityManagerFactory는 일반적으로 DB 하나에 하나만 생성되어 앱이 동작하는 동안 사용된다.
이를 만들기 위해서는 DB에 대한 정보를 전달해야한다.
-> 정보 전달을 위해서는 /resources/META-INF 위치에 persistence.xml 파일을 만들어 정보를 넣어두면된다.
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="memo">
<class>com.sparta.entity.Memo</class>
<properties>
<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="{비밀번호}"/>
<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"/>
</properties>
</persistence-unit>
</persistence>
EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
//JPA는 persistence.xml의 정보를 토대로 EntityManagerFactory를 생성한다.
EntityManager em = emf.createEntityManager();
//EntityManagerFactory를 사용하여 EntityManager를 생성할 수 있다.

트랜잭션은 DB 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적인 개념이다.
쉽게 표현하자면 DB의 데이터들을 안전하게 관리하기 위해서 생겨난 개념이다.
가장 큰 특징은 여러개의 SQL이 하나의 트랜잭션에 포함될 수 있다는 점이다.
모든 SQL이 성공적으로 수행되면 DB에 영구적으로 변경을 반영하지만 SQL 중 단 하나라도 실패한다면 모든 변경을 되돌린다.
영속성 컨텍스트에 Entity 객체들을 저장했다고 해서 DB에 바로 반영되지는 않는다.

DB에서 하나의 트랜잭션에 여러 개의 SQL을 포함하고 있다가 마지막에 영구적으로 변경을 반영하는 것 처럼 JPA에서도 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 한번에 DB에 요청해 변경을 반영한다.
@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 종료
}

영속성 컨텍스트가 이 캐시 저장소를 어떻게 활용하고 있는지 살펴보자!

em.persist(memo); 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장한다.캐시저장소에 조회하는 id가 존재하지않는 경우

db에서 select 하여 캐시저장소에 저장하고 반환한다.
캐시 저장소에 조회하는 id가 존재하는 경우

캐시 저장소에 식별자 값이 1이면서 Memo Entity 타입인 값이 있는지 조회하여 해당 객체를 반환한다.
@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();
}
위 예제에서 볼수 있듯 1차캐시 사용의 다음과 같은 장점을 볼 수 있다.
1. DB 조회 횟수를 줄일 수 있다.
2. 1차캐시를 사용해 DB row 1개당 객체 1개가 사용되는 것을 보장 (객체 동일성 보장)

em.remove(memo); 호출 시 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit이 되면 DELETE SQL이 DB에 요청된다.
@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();
}
위 테스트 예제에서 볼때 debugging을 걸어놓고 본다면 commit이 되기전에 쓰기지연저장소에 담기는지 확인할 수 있다.

이처럼 em의 actionQueue-> insertions에서 객체가 몇개가 들어가는지 볼 수 있으며,

커밋이 끝나면 해당 객체가 사라진 것을 확인할 수 있다.
실제로 Run console을 보면 트랜잭션 commit 호출 전까지는 SQL 요청이 없다가 트랜잭션 commit 후 한번에 Insert SQL 2개가 순서대로 요청된 것을 확인할 수 있다.

사실 트랜잭션 commit 후 추가적인 동작이 있는데 바로 em.flush(); 메서드의 호출이다.
해당 메서드는 영속성 컨텍스의 변경 내용들을 DB에 반영하는 역할을 수행한다.
즉, 쓰기 지연저장소의 SQL들을 DB에 요청하는 역할을 수행한다.
영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL이 쓰기 지연 저장소에 저장된다면 하나의 Update SQL로 처리할 수 있는 상황을 여러번 Update SQL을 요청하게 되기 때문에 비효율적이게 된다.
또한, JPA는 update 메소드를 지원할 것 같지만 찾아볼수 없다! 이는 비효율적이기 때문이라고 볼 수 있을것이다.

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

위 그림에서 보는 것과 같이 4가지의 상태가 있다.
Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("비영속과 영속 상태");
쉽게 말하면 new 연산자를 통해 인스턴스화 된 Entity 객체를 의미한다.
아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA 관리를 받지않는다.
em.persist(memo);
persist(entity) : 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만드는 메서드이다.
위 비영속인 상태에서는 persistenceContext의 entitiesByKey를 보면 null값인것을 확인할 수 있는데 persist 메서드를 실행한 후 확인하면 해당 부분에 key값이 생성되어있는 것을 볼 수 있으며, nonEnhancedEntityXref의 value값도 MANAGED 상태로 관리되고 있다는 것을 확인할 수 있다.

해당 상태는 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미한다.
contains(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();
}
이처럼 조회하였다 clear 호출 한다음 다시 조회가 가능하다는 뜻이다.
해당 메서드 호출 이후 EntityManager를 사용하려고하면 오류가 발생한다.
-> 따라서 merge(entity)메서드는 비영속, 준영속 모두 파라미터로 받을 수있으며 상황에 따라 저장을 할 수도 수정을 할수도 있다.
remove(entity) : 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환한다.