영속성 컨텍스트
영속성 컨텍스는 엔티티 객체를 관리하는 가상의 공간이라고 보면됩니다.
영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행합니다. 엔티티 객체가 영속성 컨텍스트에 들어오면 JPA는 엔티티 객체의 매핑 정보를 데이터베이스에 반영하는 작업을 수행합니다. 이처럼 엔티티 객체가 영속성 컨텍스트에 들어와 JPA의 관리 대상이 되는 시점부터는 해당 객체를 영속 객체(Persistence Object)라고 부릅니다. 간단하게 애플리케이션과 데이터베이스와의 관계를 표현하면 아래와 같습니다.
영속성 컨텍스트는 세션 단위의 생명주기를 가집니다. 데이터베이스에 접근하기 위한 세션이 생성되면 영속성 컨텍스트가 만들어지고, 세션이 종료되면 영속성 컨텍스트도 없어집니다. 엔티티 매니저는 이러한 일련의 과정에서 영속성 컨텍스트에 접근하기 위한 수단으로 사용됩니다.
엔티티 매니저
엔티티 매니저는 이름 그대로 엔티티를 관리하는 객체입니다. 엔티티 매니저는 데이터베이스에 접근해서 CRUD 작업을 수행합니다. Spring Data JPA를 사용하면 리포지토리를 사용해서 데이터 베이스에 접근하는데, 실제 내부 구현체인 SimpleJpaRepository가 아래 예제와 같이 리포지토리에서 엔티티 매니저를 사용하는 것을 알 수 있습니다.
public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManger entityManager) {
Assert.notNull(entityInformation, "JpaEntityInformation must not be null!");
Assert.notNull(entityManager, "EntityManager must not be null!");
this.entityInformation = entityInformation;
this.em = entityManger;
this.provider = PersistenceProvider.fromEntityManager(entityManager);
}
엔티티 매니저는 엔티티 매니저 팩토리(EntityManagerFactory)가 만듭니다. 엔티티 매니저 팩토리는 데이터베이스에 대응하는 객체로서 스프링 부트에서는 자동 설정 기능이 있기 때문에 application.properties에서 작성한 최소한의 설정만으로도 동작하지만 JPA의 구현체 중 하나인 하이버네이트에서는 persistence.xml 이라는 설정 파일을 구성하고 사용해야 하는 객체입니다. 아래 예제는 persistence.xml 파일의 예를 보여줍니다.
<?xml version="1.0" encoding="UTF-8" ?>
<persistence 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/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="entity_manager_factory" transaction-type="RESOURCE_LOCAL">
<properties>
<property name="javax.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="password"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mariadb://localhost:3306/springboot"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MariaDB103Dialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
8번 줄과 같이 persistence-unit을 설정하면 해당 유닛의 이름을 가진 엔티티 매니저 팩토리가 생성됩니다. 엔티티 매니저 팩토리는 애플리케이션에서 단 하나만 생성되며, 모든 엔티티가 공유해서 사용합니다.
인티티 매니저 팩토리로 생성된 엔티티 매니저는 엔티티를 영속성 컨텍스트에 추가해서 영속 객체로 만드는 작업을 수행하고, 영속성 컨텍스트와 데이터베이스를 비교하면서 실제 데이터베이스를 대상으로 작업을 수행합니다.
엔티티의 생명주기
엔티티 객체는 영속성 컨텍스트에서 다음과 같은 4가지 상태로 구분됩니다.
비영속(New)
영속성 컨텍스트에 추가되지 않은 엔티티 객체의 상태를 의미합니다.
영속(Managed)
영속성 컨텍스트에 의해 엔티티 객체가 관리되는 상태입니다.
준영속(Detached)
영속성 컨텍스트에 의해 관리되던 엔티티 객체가 컨텍스트와 분리된 상태입니다.
삭제(Removed)
데이터베이스에서 레코드를 삭제하기 위해 영속성 컨텍스트에 삭제 요청을 한 상태입니다.
1차캐시와 쓰기 지연
1차 캐시
- EntityManager를 통해 데이터베이스에서 조회하거나 새로 생성한 엔티티 객체는 영속성 컨텍스트에 저장됩니다.
- 같은 EntityManager 안에서 동일한 엔티티를 다시 조회하면 데이터베이스에 접근하지 않고 1차 캐시에서 데이터를 가져옵니다.
- 이를 통해 불필요한 데이터베이스 호출을 줄이고 성능을 최적화합니다.
쓰기 지연
- EntityManager.persist()를 호출하면 바로 데이터베이스에 반영되지 않고, 영속성 컨텍스트 내부의 쓰기 지연 저장소에 쌓입니다.
- 트랜잭션이 커밋될 때 한꺼번에 INSERT, UPDATE, DELETE SQL을 실행합니다.
- 이를 통해 데이터베이스와의 통신횟수를 줄이고 배치 작업을 효율적으로 처리할 수 있습니다.
예제
트랜잭션 내에서 여러 작업을 할 때
예를 들어, 하나의 트랜잭션에서 여러 엔티티를 생성하거나 수정한다고 가정해봅시다. JPA는 모든 변경을 1차 캐시에 반영하고, 트랜잭션이 커밋되기 전에 쓰기 지연 저장소에 저장된 SQL을 한꺼번에 실행합니다.
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
Member member = new Member();
member.setName("John");
em.persist(member); // INSERT 쿼리가 즉시 실행되지 않음
member.setName("John Doe"); // 1차 캐시에 반영됨
em.getTransaction().commit(); // 여기서 한꺼번에 INSERT 쿼리가 실행됨
em.close();
동일한 엔티티를 반복적으로 조회할 때
영속성 컨텍스트에 이미 존재하는 엔티티를 가져오면 데이터베이스 조회 쿼리가 발생하지 않습니다.
Member member1 = em.find(Member.class, 1L); // DB에서 조회
Member member2 = em.find(Member.class, 1L); // 1차 캐시에서 조회
위와 같이 JPA를 사용할때 EntityManager를 직접 다룰 수도 있지만
Spring Data JPA를 사용하면 EntityManager를 직접 쓰지 않아도 복잡한 설정과 코드를 알아서 처리해줍니다.
Spring Data JPA에서 영속성 컨텍스트가 동작하는 방식
참고로 엔티티 = 하나의 행이라고 보면됩니다.
Spring Data JPA를 사용할 때는 보통 @Repository 로 정의된 Repository 인터페이스와 @Transactional 이 함께 쓰여진 트랜잭션 영속성 컨텍스트가 자동으로 관리됩니다. 그래서 EntityManager를 명시적으로 호출하지 않고도 JPA의 모든 기능을 사용할 수 있습니다. 예를들어
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void createMember(String name) {
Member member = new Member();
member.setName(name);
memberRepository.save(member); // 여기서 영속성 컨텍스트와 1차 캐시가 동작
}
@Transactional
public Member findMember(Long id) {
return memberRepository.findById(id).orElseThrow(); // 1차 캐시가 적용됨
}
}
그렇다면 Spring Data JPA 내부에서 어떻게 Entity Manager를 다룰까요?
1. Sptring이 자동으로 처리
- @Transactional 이 선언된 메서드 내에서 영속성 컨텍스트가 자동으로 생성되고 관리됩니다.
- save() 메서드는 내부적으로 EntityManager의 persist() 또는 merge()를 호출하지만, 이것이 숨겨져 있어서 직점 em을 다룰 필요가 없던것입니다.
2. 트랜잭션과 쓰기 지연 처리
- 트랜잭션이 커밋될 때, save()로 추가된 엔티티는 실제로 데이터베이스에 반영됩니다.
- 이 과정에서 쓰기 지연(Write-Behind)이 자연스럽게 발생하지만, 사용자는 이를 신경 쓰지 않아도 됩니다.
3. Spring Data JPA의 간편화
- CrudRepository 또는 JPARepository 인터페이스를 확장해서 사용하는 방식으로 JPA를 단순화했기 때문에, EntityManager를 직접 사용할 일이 줄어들었습니다.
Spring Data JPA에서의 1차 캐시
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void processMember(Long memberId) {
// 첫 번째 조회 (DB 접근 발생)
Member member1 = memberRepository.findById(memberId).orElseThrow();
// 두 번째 조회 (1차 캐시에서 가져옴, DB 접근 없음)
Member member2 = memberRepository.findById(memberId).orElseThrow();
// 동일 객체 확인 (True)
System.out.println(member1 == member2);
}
}
첫 번째 findById() 호출은 데이터베이스를 조회해서 영속성 컨텍스트에 엔티티를 저장
두 번째 findById() 호출은 이미 영속성 컨텍스트에 있으므로 DB를 조회하지 않고 캐시된 엔티티를 반환
동일 트랜잭션 내에서 항상 같은 객체를 반환하기 때문에 member1 == member2는 true
Spring Data JPA에서의 지연 쓰기
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void createMember(String name) {
Member member = new Member();
member.setName(name);
// save() 호출 (쓰기 지연 저장소에 INSERT SQL 준비)
memberRepository.save(member);
// 트랜잭션 커밋 시점에 INSERT SQL 실행
}
}
memberRepository.save(member)를 호출하면 INSERT SQL은 즉시 실행되지 않고, 쓰기 지연 저장소에 저장됨
트랜잭션이 커밋(@Transactional)될 때, 즉 @Transactional 어노테이션이 적용된 메서드가 정상적으로 종료되는 시점에 INSERT SQL이 실행돼 데이터베이스에 반영
만약 @Transactional 어노테이션이 없으면 Hibernate는 트랜잭션을 별도로 관리하지 않습니다. 결과적으로, memberRepository.save(member) 를 호출하면 쓰기지연(Hibernate의 1차 캐시)이 작동하지 않고, INSERT SQL이 데이터베이스에 즉시 실행됩니다.
public void saveMember(Member member) {
memberRepository.save(member); // INSERT SQL이 즉시 실행됨
}
만약 여러개의 SQL 작업이 있을때 중간에 예외가 발생하면 이전 작업은 롤백되지 않고 이미 데이터베이스에 반영된 상태로 남게 됩니다.
그럼에도 불구하고 EntityManager를 써야 할 때
Spring Data JPA만으로 해결하기 어려운 고급 기능이 필요할 때는 EntityManager를 직접 사용하는 경우가 있습니다.
- 복잡한 쿼리를 실행해야 할 때 (em.createQuery)
- 영속성 컨텍스트를 직접 제어해야 할 때 (detach, clear, refresh 등)
- 배치 처리나 벌크 연산이 필요할 때
@PersistenceContext
private EntityManager em;
@Transactional
public void batchInsert(List<Member> members) {
for (int i = 0; i < members.size(); i++) {
em.persist(members.get(i));
if (i % 50 == 0) { // 50개 단위로 flush
em.flush();
em.clear();
}
}
}
'웹 개발 > 🍃 SpringBoot' 카테고리의 다른 글
JPA | 엔티티 설계 (0) | 2025.01.10 |
---|---|
Hibernate hibernate.ddl-auto 속성 완벽 가이드 (0) | 2025.01.09 |
JPA | 하이버네이트 (0) | 2025.01.07 |
JPA | JPA (0) | 2025.01.07 |
JPA | ORM (1) | 2025.01.07 |