JPA(Java Persistence API)에서 영속성 컨텍스트(Persistence Context)는 데이터베이스와 애플리케이션 간의 데이터를 관리하는 중요한 개념이다. 이를 이해하기 쉽게 설명하자면, 영속성 컨텍스트는 엔티티 객체들을 담아두는 "가상의 바구니"라고 할 수 있다.
예제 코드와 함께 알아보는 JPA의 핵심 개념
1. Entity 저장 - 1차 캐시
@Test
@DisplayName("1차 캐시: Entity 저장")
void test1() {
EntityTransaction et = em.getTransaction(); // 트랜잭션 시작을 준비
et.begin(); // 트랜잭션 시작
try {
Memo memo = new Memo(); // 새로운 Memo 엔티티 생성
memo.setId(1L); // 엔티티의 ID 설정
memo.setUsername("Robbie"); // 엔티티의 필드 설정
memo.setContents("1차 캐시 Entity 저장"); // 엔티티의 필드 설정
em.persist(memo);
// 새로운 엔티티를 영속성 컨텍스트에 저장한다. 이 시점에 DB에 반영되지 않고,
// 1차 캐시에 저장된다.
et.commit();
// 트랜잭션 커밋 시 영속성 컨텍스트에 있는 엔티티 상태가 DB에 반영된다.
// 여기서 INSERT 쿼리가 실행되어 DB에 데이터가 저장된다.
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // 예외 발생 시 트랜잭션 롤백
} finally {
em.close(); // EntityManager 종료
}
emf.close(); // EntityManagerFactory 종료
}
설명:
em.persist(memo)는 Memo 엔티티를 영속성 컨텍스트에 저장한다. 이 시점에서 데이터베이스에 반영되지 않고, 영속성 컨텍스트의 1차 캐시에서 관리된다. 트랜잭션이 커밋될 때 INSERT 쿼리가 실행되며, 데이터베이스에 저장된다.
2. Entity 조회 - 1차 캐시에 없는 경우
@Test
@DisplayName("Entity 조회: 캐시 저장소에 해당하는 Id가 존재하지 않은 경우")
void test2() {
try {
Memo memo = em.find(Memo.class, 1L);
// ID가 1인 Memo 엔티티를 조회한다.
// 이 엔티티가 1차 캐시에 없으면, DB에서 조회한 후 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(); // EntityManager 종료
}
emf.close(); // EntityManagerFactory 종료
}
설명:
em.find(Memo.class, 1L)는 영속성 컨텍스트의 1차 캐시에서 ID가 1인 Memo 엔티티를 찾는다. 1차 캐시에 해당 엔티티가 없으면, 데이터베이스에서 조회하여 1차 캐시에 저장하고, 이후 애플리케이션에 반환한다.
3. Entity 조회 - 1차 캐시에 있는 경우
@Test
@DisplayName("Entity 조회: 캐시 저장소에 해당하는 Id가 존재하는 경우")
void test3() {
try {
Memo memo1 = em.find(Memo.class, 1L);
// ID가 1인 Memo 엔티티를 조회. 이 엔티티가 1차 캐시에 없으면 DB에서 가져와서 1차 캐시에 저장
Memo memo2 = em.find(Memo.class, 1L);
// 같은 ID로 다시 조회. 이 경우 1차 캐시에서 조회되므로 DB 접근이 없다.
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(); // EntityManager 종료
}
emf.close(); // EntityManagerFactory 종료
}
설명:
em.find(Memo.class, 1L)를 두 번 호출해도 두 번째 호출은 1차 캐시에서 바로 엔티티를 반환한다. 이는 1차 캐시에 엔티티가 있으면 데이터베이스에 다시 조회하지 않고, 캐시된 객체를 반환하기 때문이다.
4. 객체 동일성 보장
@Test
@DisplayName("객체 동일성 보장")
void test4() {
EntityTransaction et = em.getTransaction();
et.begin(); // 트랜잭션 시작
try {
Memo memo1 = em.find(Memo.class, 1L); // ID가 1인 Memo 엔티티 조회
Memo memo2 = em.find(Memo.class, 1L); // 같은 ID로 다시 조회
System.out.println(memo1 == memo2);
// true 출력: 같은 엔티티를 1차 캐시에서 가져왔으므로 동일한 객체
et.commit(); // 트랜잭션 커밋
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // 예외 발생 시 트랜잭션 롤백
} finally {
em.close(); // EntityManager 종료
}
emf.close(); // EntityManagerFactory 종료
}
설명:
영속성 컨텍스트는 동일한 엔티티 ID에 대해 항상 동일한 객체를 반환한다. 이는 같은 트랜잭션 내에서 em.find()를 여러 번 호출해도 같은 객체가 반환되며, 이는 엔티티 객체의 동일성을 보장한다는 의미이다.
5. Entity 삭제
@Test
@DisplayName("Entity 삭제")
void test5() {
EntityTransaction et = em.getTransaction();
et.begin(); // 트랜잭션 시작
try {
Memo memo = em.find(Memo.class, 2L);
// ID가 2인 Memo 엔티티를 조회
em.remove(memo);
// 조회된 엔티티를 영속성 컨텍스트에서 삭제. 이때 영속성 컨텍스트에서만 제거된 상태
et.commit();
// 트랜잭션 커밋 시 DELETE 쿼리가 실행되어 DB에서도 삭제됨
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // 예외 발생 시 트랜잭션 롤백
} finally {
em.close(); // EntityManager 종료
}
emf.close(); // EntityManagerFactory 종료
}
설명:
em.remove(memo)는 영속성 컨텍스트에서 엔티티를 삭제하지만, 트랜잭션이 커밋되기 전까지 데이터베이스에 DELETE 쿼리가 실행되지 않는다. 커밋 시점에 DELETE 쿼리가 실행되어 데이터베이스에서도 삭제가 이루어진다.
6. 쓰기 지연 저장소 확인
@Test
@DisplayName("쓰기 지연 저장소 확인")
void test6() {
EntityTransaction et = em.getTransaction();
et.begin(); // 트랜잭션 시작
try {
Memo memo1 = new Memo();
memo1.setId(2L);
memo1.setUsername("Robbert");
memo1.setContents("쓰기 지연 저장소");
em.persist(memo1);
// 새로운 엔티티가 영속성 컨텍스트에 저장되지만, 실제 DB에는 반영되지 않고 쓰기 지연 저장소에 보관됨
Memo memo2 = new Memo();
memo2.setId(3L);
memo2.setUsername("Bob");
memo2.setContents("과연 저장을 잘 하고 있을까?");
em.persist(memo2);
// 또 다른 새로운 엔티티 추가. 이 역시 쓰기 지연 저장소에 보관됨
System.out.println("트랜잭션 commit 전");
et.commit();
// 트랜잭션 커밋 시점에 쓰기 지연 저장소에 있던 쿼리들이 일괄 실행되어 DB에 반영됨
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // 예외 발생 시 트랜잭션 롤백
} finally {
em.close(); // EntityManager 종료
}
emf.close(); // EntityManagerFactory 종료
}
설명:
JPA는 트랜잭션이 끝나기 전까지 INSERT/UPDATE/DELETE 쿼리를 데이터베이스에 바로 보내지 않고, "쓰기 지연 저장소"라는 내부 버퍼에 모아둔다.
결론
JPA의 영속성 컨텍스트(Persistence Context)는 애플리케이션과 데이터베이스 간의 데이터 관리를 효율적으로 수행하기 위한 핵심 개념이다. 코드 예제를 통해 살펴본 바와 같이, 영속성 컨텍스트는 다음과 같은 주요 기능과 특성을 가진다:
- 가상의 바구니: 영속성 컨텍스트는 데이터베이스와 애플리케이션 간의 엔티티 객체들을 관리하는 가상의 바구니 역할을 한다. 엔티티 객체는 데이터베이스에 실제로 저장되기 전까지 이 바구니 안에서 관리되며, 상태 변경은 영속성 컨텍스트에서만 추적된다.
- 상태 관리:
- 변경 감지: 엔티티의 상태가 변경되면, 이 변경 사항은 영속성 컨텍스트에 의해 자동으로 감지된다. 실제 데이터베이스에 변경 사항이 반영되기 전까지는 영속성 컨텍스트의 1차 캐시에서만 관리된다.
- 추가 및 삭제: 새로 추가된 엔티티는 영속성 컨텍스트에 추가되며, 삭제된 엔티티는 영속성 컨텍스트에서 제거된다. 이 모든 작업은 트랜잭션 커밋 시 데이터베이스에 반영된다.
- 트랜잭션과 커밋:
- 트랜잭션 커밋: 트랜잭션이 커밋되기 전까지 영속성 컨텍스트는 엔티티의 상태 변화를 추적하고, 커밋 시점에 데이터베이스에 최종적으로 반영된다. commit() 호출은 트랜잭션을 종료하며, 영속성 컨텍스트 내의 모든 변경 사항이 데이터베이스에 영구적으로 저장된다. 이 과정에서 flush() 메서드가 자동으로 호출되어, 변경 사항이 데이터베이스에 즉시 반영된다.
- commit(): 트랜잭션을 종료하고, 영속성 컨텍스트의 모든 변경 사항을 데이터베이스에 영구적으로 반영한다.
- flush(): 현재까지의 변경 사항을 데이터베이스에 즉시 반영하지만, 트랜잭션을 종료하지 않는다.
- 트랜잭션 커밋: 트랜잭션이 커밋되기 전까지 영속성 컨텍스트는 엔티티의 상태 변화를 추적하고, 커밋 시점에 데이터베이스에 최종적으로 반영된다. commit() 호출은 트랜잭션을 종료하며, 영속성 컨텍스트 내의 모든 변경 사항이 데이터베이스에 영구적으로 저장된다. 이 과정에서 flush() 메서드가 자동으로 호출되어, 변경 사항이 데이터베이스에 즉시 반영된다.
- 객체 동일성 보장: 영속성 컨텍스트는 동일한 ID를 가진 엔티티에 대해 항상 동일한 객체를 반환한다. 이는 동일한 트랜잭션 내에서 엔티티 객체의 참조가 일관되게 유지됨을 보장한다.
이러한 특성 덕분에 JPA는 데이터베이스 작업을 보다 효율적으로 관리하고, 애플리케이션 개발자는 데이터베이스와의 상호작용을 간편하게 처리할 수 있다. 영속성 컨텍스트를 이해하고 활용하는 것은 JPA를 효과적으로 사용하는 데 있어 필수적이다.
'SPRING&BOOT > JPA' 카테고리의 다른 글
JWT란? (0) | 2024.08.30 |
---|---|
관계형 데이터베이스에서 중간 테이블의 중요성과 활용 방법 (0) | 2024.08.27 |
JPA 지연 로딩(Lazy Loading) (0) | 2024.08.27 |
JPA Entity 연관 관계 (0) | 2024.08.26 |
JPA 엔티티 어노테이션 가이드: 핵심 어노테이션과 예제 코드 정리 (0) | 2024.08.22 |