3일 차 TIL
JPA는 영속성 컨텍스트에 엔티티를 등록하여 관리한다.
이때 1차 캐시 형식으로 key-value 형태로 저장을 하는데
key는 엔티티의 pk값, value는 엔티티 객체가 들어간다.
그리고 실행할 쿼리문들은 쓰기 지연 저장소에 저장해 뒀다가
커밋 시에 한 번에 실행하게 된다.
이로써 네트워킹 횟수를 줄일 수 있다.
또한 JpaRepository를 상속받아 사용하더라도
내부적으로는 EntityManager를 사용한다.
마찬가지로 @Transactional 설정 시
EntityTransaction을 불러와 사용하고
메서드 실행 시 정상 종료 시 커밋,
예외 발생/실패 시 롤백을 해준다.
JPA는 영속성 컨텍스트에서 엔티티의 생명주기를 영속/비영속/준영속/삭제로 나눠 관리한다.
영속성 컨텍스트
JPA는 영속성 컨텍스트에 엔티티 객체를 저장하여 관리하면서 DB와 소통한다.

EntityManager는 EntityManagerFactory를 통해 생성할 수 있다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();
그리고 EntityManager가 DB작업을 수행하기 위해 DB정보를 persistence.xml을 통해 얻어온다.
여기서 createEntityManagerFactory 메서드에 파라미터로 넘겨주는 "memo"는 xml에 persistence-unit name에 해당한다.
즉 DB의 정보를 기록해 둔 유닛이름이라고 생각하면 쉽다.

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>
JPA와 트랜잭션의 관계
트랜잭션은 DB 데이터의 무결성과 정확성을 유지하기 위한 논리적 개념이다.
트랜잭션 내에 포함된 sql 중 하나라도 실패하면 전체를 다 rollback 해서 취소한다.

SQL에서
START TRANCSACTION; 으로 트랜잭션을 시작하고
COMMIT; 을 통해 성공 시 DB에 결과를 반영한다.
START TRANSACTION; # 트랜잭션을 시작합니다.
# ... CRUD 쿼리들
COMMIT; # 트랜잭션을 커밋합니다.
JPA도 마찬가지로 SQL의 트랜잭션 개념을 사용한다.
트랜잭션 내에서 영속성 컨텍스트에 엔티티 객체를 생성하고
실행할 쿼리를 저장해 뒀다가
마지막에 SQL을 DB에 요청해 변경을 반영한다.
영속성 컨텍스트의 엔티티 관리하는 방법
1차 캐시를 사용해서 엔티티를 PK를 기준으로 구분하고 저장한다.

key에는 pk값
value에는 Entity 클래스의 객체를 저장
테스트 코드
public class EntityTest {
EntityManagerFactory emf;
EntityManager em;
@BeforeEach
void setUp() {
emf = Persistence.createEntityManagerFactory("memo");
em = emf.createEntityManager();
}
}
EntityManagerFactory 줄여서 emf로
EntityManager 줄여서 em
emf로 em을 생성한다.
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();
}
조회를 제외하고 (조회도 readOnly 같은 경우는 트랜잭션이 사용되지만)
생성/수정/삭제는 트랜잭션이 필요하기 때문에 트랜잭션 환경 구성을 위한 EntityTransaction et = em.getTransaction();
영속성 컨텍스트에 저장하는 메서드는 em.persist()이다.
파라미터로 저장할 Entity 객체를 넘겨주면 된다.
그리고 결과 반영을 위한 et.commit() 실행
이때 commit 메서드에는 flush가 포함된다.
flush에서 ActionQueue에 저장된 sql들을 DB에 요청하는 역할을 한다.

위 저장소(ActionQueue)에 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();
}
커밋 전에는 sql이 실행되지 않고
커밋 시 저장된 sql들을 한 번에 실행한다.
위 코드에서는 persist()를 2번 실행했으니
2번의 insert문이 한 번에 실행된다.
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();
}
의도적으로 flush() 메서드를 실행하면 commit() 시 아무 작업도 하지 않는다.
객체의 동일성 보장 / Entity 조회 테스트 코드
@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); // true
System.out.println(memo1 == memo); // false
et.commit();
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
}
조회 메서드는 em.find()이다.
첫 번째 파라미터로 조회할 Entity클래스
두 번째로 pk값을 전달하면 된다.
em.find(Memo.class, 1);
1차 캐시를 먼저 확인하고
없다면 쿼리문을 실행해서 가져온다.
같은 데이터를 다시 조회한다면
쿼리문 없이 바로 1차 캐시를 조회해서 가져온다.
위 조회 테스트 코드에서
memo2의 경우 memo1과 같은 pk가 1인 데이터를 가져오기 때문에 비교 시 true
memo1과 memo는 pk가 1과 2로 다르기에 비교시 false
즉 1차 캐시를 통해서 데이터를 가져오더라도
객체의 동일성이 보장된다는 것이다. ( 별도의 주소 생성 x )
Entity 삭제
엔티티의 삭제는 em.remove()를 사용한다.
먼저 em.find()를 통해 조회하고

그리고 em.remove()를 통해 삭제할 Entity의 상태를 DELETED로 바꾸고
커밋 시 삭제 sql이 DB에 요청된다.

Entity 삭제 테스트 코드
@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.find() 실행 시
해당 엔티티의 상태를 MANAGED로 변경한다.

그리고 em.remove() 실행 시
DELETED로 상태를 변경한다.

et.commit() 시 DB에 결과 반영
변경 감지(Dirty Checking)
위에서 생성, 조회, 삭제 메서드를 확인했다.
그렇다면 수정은 어떻게 진행될까?

EntityManager에는 별도의 수정 메서드가 없다.
em.flush() 호출 시
기존에 저장된 값(LoadedState)과 현재값을 비교해서
다르면 수정쿼리문을 생성해서 ActionQueue에 저장한다.
마지막으로 commit시 결과를 DB에 반영한다.
Entity 수정 테스트 코드
@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();
}

memo.set~~(); 를 통해 변경
commit 전 실행되는 flush를 통해 수정 sql 생성
commit으로 트랜잭션을 DB에 반영
오른쪽 사진은 실제 실행된 sql문이다.
엔티티의 상태
엔티티의 상태는 4가지로 분류된다.

비영속 -> 아직 영속성 컨텍스트에 저장되지 않은 즉, JPA에 관리되지 않는 상태
ex) 객체의 인스턴스만 생성
영속 -> 영속성 컨텍스트에 저장된 상태, JPA에 관리되는 상태
ex) em.persist()로 저장
준영속 -> 영속성 컨텍스트에서 저장돼 관리되다가 분리된 상태
ex) em.detach()로 분리참고로 em.contains()로 영속성 컨텍스트에 포함되어 있는지 확인할 수 있다.
em.clear()는 영속성 컨텍스트의 내용을 비워준다.
즉 find() 같은 걸로 저장되어 있던 캐시를 초기화한다고 보면 된다.
em.merge()는 준영속 엔티티의 상태를 영속성 컨텍스트에 복사하여
기존 영속 엔티티를 수정하거나,
없으면 DB 조회 후 수정하고,
DB에도 없으면 새 영속 엔티티를 생성하는 메서드이다.
삭제 ->영속성 컨텍스트에서 삭제로 표시 후 커밋 시 삭제
em.remove() 사용
Spring vs Spring boot
스프링에서는 properties.xml에 데이터베이스 연결 설정이나
hibernate 설정등을 했고 그리고
emf와 em을 직접 설정해줘야 했다.
하지만 Spring boot에서는 @PersistenceContext로 자동으로 em을 주입받아 사용할 수 있다.
참고로 테스트 시 @SpringBootTest를 클래스에 붙여줘야 한다.
@PersistenceContext
EntityManager em;
트랜잭션 설정 또한
@Transactional으로 환경을 구성할 수 있다.
생성/수정/삭제 작업엔 위 @Transactional 어노테이션이 꼭 필요하다.
앞서 봤듯이 영속성 컨텍스트를 조회하는 작업은 꼭 트랜잭션이 필요하지 않았다.
하지만 조회 작업 기능만 하는 메서드에도 @Transactional(readOnly = true)를 통해 트랜잭셔널을 적용하면 좋다.
기본값은 readOnly = false이다.
트랜잭션 어노테이션은 전파가 가능해다.

부모 클래스에서 자식 클래스의 메서드를 호출 시
부모와 자식 둘 다에 트랜잭션 어노테이션이 있다면
두 트랜잭션이 연결돼서 부모가 종료될 때 커밋된다.
즉 같은 트랜잭션이 공유된다.
이 또한 기본값으로 propagation = Propagation.REQUIRED로 설정되어 있기에 전파가 가능하다.
Spring Data JPA란?
그동안은 순수 JPA를 사용했다. 하지만 Spring Data JPA를 통해 더 간편하게 사용할 수 있다.

JpaRepository

서버 실행 시, JpaRepository를 상속받은 인터페이스가 스캔되면
자동으로 상위 클래스들을 Bean으로 등록해 준다.
그래서 구현클래스들을 직접 작성하지 않아도 JPA 기능들을 사용가능하다.
'내배캠' 카테고리의 다른 글
| [내일배움캠프 본캠프] 5일 차 TIL Spring Security + JWT (0) | 2026.02.14 |
|---|---|
| [내일배움캠프 본캠프] 4일 차 TIL Bean 수동 등록, 쿠키/세션 (0) | 2026.02.14 |
| [내일배움캠프 본캠프] 2일 차 TIL - SQL, JdbcTemplate, IoC, DI, Bean (0) | 2026.02.11 |
| [내일배움캠프 본캠프] 1일 차 TIL 상품-주문 미니프로젝트 피드백 (0) | 2026.02.10 |
| [내일배움캠프 사전캠프] 3일 차 TIL Jackson (0) | 2026.02.06 |
