서론
JPA강의를 모두 듣고 배운 내용을 정리하고 프로젝트에 적용해주기 위해서 영속성 컨텍스트에 대해서 작성해보고자합니다. 이전에는 그냥 jpaRepository.save로 저장된 객체를 사용하거나 find로 찾아온 엔티티 객체를 사용하면 데이터베이스에 있는 객체에 접근할 수 있겠거니 했었는데 강의를 듣고 그 원리에 대해서 알게 된 것 같습니다. 이번 포스팅을 통해서 트랜젝션과 영속성 컨텍스트가 엔티티 객체를 어떻게 관리하는지 원리를 파악해서 더 효율적으로 엔티티를 관리할 수 있게 되는 계기가 되었으면 좋겠습니다.
영속성 컨텍스트(Persistence Context)란?
영속성 컨텍스트는 JPA(Java Persistence API)에서 엔티티 객체를 관리하는 일종의 저장소입니다. JPA는 애플리케이션과 데이터베이스 간의 상호작용을 관리하며, 영속성 컨텍스트는 데이터베이스에 저장되기 전의 임시 저장소 역할을 해줍니다.
엔티티의 생명 주기를 관리하며, 엔티티를 영속성 관리 상태로 둠으로써 객체를 안전하게 데이터베이스에 저장하거나 수정, 삭제할 수 있습니다. 이를 통해 데이터 일관성을 유지하며, 데이터베이스와의 통신을 최적화할 수 있는 메모리 상의 데이터 저장소 역할을 합니다.
영속성 컨텍스트의 상태와 생명 주기
영속성 컨텍스트는 데이터베이스와 상호작용할 4가지의 상태가 있습니다.
비영속
- 영속성 컨텍스트와 관련이 없는 상태로 새로 생성된 객체가 영속성 컨텍스트에 등록되기 전의 상태입니다.
영속
- 영속성 컨텍스트에 의해 관리되고 있는 상태입니다. 1차 캐시를 제공해서 동일 트랜젝션 내에서 엔티티를 조회 시에 영속성 컨텍스트에 등록된 엔티티를 사용합니다. 자세한 내용은 아래에서 다루겠습니다.
준영속
- 영속성 컨텍스트에서 분리된 상태로, 데이터베이스에 존재하나 영속성 컨텍스트가 관리하지 않는 상태입니다.
삭제
- 영속성 컨텍스트에서 삭제가 예약된 상태로, 커밋 시점에 데이터베이스에서 제거됩니다.
예시코드
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private int age;
// 기본 생성자, getter, setter 생략
}
// 비영속 상태
User user = new User();
user.setName("John");
user.setAge(30);
// 영속 상태 - 영속성 컨텍스트에 추가
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
em.persist(user); // User 엔티티가 영속성 컨텍스트에 등록되어 관리됨
// 커밋 전까지 변경사항 자동 감지 (Dirty Checking)
user.setAge(31); // 나이 변경
em.getTransaction().commit(); // 커밋 시 변경사항이 자동으로 데이터베이스에 반영됨
em.close();
// 준영속 상태 - 영속성 컨텍스트와 분리됨
em.detach(user);
// 삭제 상태
em.getTransaction().begin();
em.remove(user); // 데이터베이스에서 해당 엔티티가 삭제됨
em.getTransaction().commit();
스프링 데이터 JPA를 통한 영속성 컨텍스트
@Transactional 어노테이션 내의 메소드에서 새로 생성한 객체를 save하는 경우 해당 객체는 영속 상태로 전환됩니다. EntityManager를 직접 다루지 않아도, save 메소드를 통해 스프링 데이터 JPA가 내부적으로 EntityManager를 사용해 영속 상태로 관리하기 때문입니다. 즉, 스프링 데이터 JPA는 EntityManager의 역할을 내부적으로 대신 처리하여 영속성 작업을 수행합니다. 즉 스프링 데이터 JPA를 통한 save, find, delete 등의 메소드를 사용하면 객체가 영속성 컨텍스트에서 관리될 수 있습니다.
영속성 컨텍스트의 특징
1. 1차 캐시
영속성 컨텍스트는 1차 캐시를 제공하여 동일 트랜잭션 내에서 조회 시 데이터베이스 쿼리를 줄입니다. 첫 조회 후에는 메모리에 캐시된 값을 사용해 성능을 최적화합니다. 해당 1차 캐시를 사용하기 위해서는 조건이 필요합니다.
1차 캐시의 조건
- 트랜젝션 안에서만 사용 가능하다.
- 동일 트랜젝션 안에서만 같은 1차 캐시를 사용 가능하다. 그래서 트랜잭션 전파 속성이 REQUIRED(기본값)으로 되어 있었다면 트랜젝션 전파가 이뤄진 같은 트랜젝션에서도 1차 캐시를 동일하게 사용가능하다.
- 엔티티의 기본키를 기준으로 조회하고 반환값이 엔티티여야 해당 객체가 1차 캐시에 저장된다.
2. 변경 감지 (Dirty Checking)
영속성 컨텍스트는 엔티티의 상태 변화를 감지해, 트랜젝션 커밋 시점에 마지막으로 변경된 부분을 반영합니다.
영속성 컨텍스트의 함수
flush()
- 영속성 컨텍스트가 현재까지 변경된 엔티티를 데이터베이스에 반영합니다.
- 트랜잭션이 끝나지 않더라도 변경 사항이 데이터베이스에 즉시 적용되지만 1차 캐시는 그대로 유지됩니다.
- JpaRepository에서 flush가 선언되어 있어서 사용 가능합니다.
clear()
- 영속성 컨텍스트가 모든 엔티티를 1차 캐시에서 제거하고 영속성 컨텍스트를 초기화합니다.
- 이후에는 트랜잭션 내에서 동일한 엔티티를 조회하더라도 다시 데이터베이스에서 조회합니다.
- 트랜잭션 중간에 clear()를 사용하면 1차 캐시가 비워지기 때문에 영속성 컨텍스트가 관리하던 엔티티들이 비영속 상태로 전환됩니다.
예시
@PersistenceContext // EntityManager를 주입받음
private EntityManager entityManager;
// 비영속 상태
User user = new User();
user.setName("John");
user.setAge(30);
// JpaRepository에서 flush 사용 예시
scheduleRepository.save(entity);
scheduleRepository.flush(); // 변경 사항을 즉시 데이터베이스에 반영
// 아직 1차캐시에 남아있어서 변경 감지가 됨
entity.setAge(20);
entityManager.clear(); // 1차 캐시 초기화 (트랜잭션 내)
entity.setAge(25); // 더 이상 영속 상태가 아니므로 변경 감지로 하는 변경이 반영되지 않음
// setAge(20); 또한 반영되지 않음
3. 지연 로딩
관계형 엔티티를 필요할 때만 조회하여 메모리와 성능을 최적화합니다. 연관관계 상태일 때만 사용 가능하며 기본 타입이 FetchType.EAGER가 되어 있을 수 있으므로 웬만하면 명시적으로 선언해주는게 좋습니다.
예를 들어, User와 Order 관계에서 Order가 필요할 때만 조회합니다.
import jakarta.persistence.*;
import java.util.List;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 지연 로딩 설정
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders;
// getter, setter
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void loadUserData() {
// User만 조회 (Order는 아직 조회하지 않음)
User user = userRepository.findById(1L).orElseThrow();
System.out.println("User name: " + user.getName());
// 이 시점에서 지연 로딩이 수행되어 Order를 조회
List<Order> orders = user.getOrders();
System.out.println("Number of Orders: " + orders.size());
}
}
4. 트랜잭션 관리
트랜잭션 단위로 엔티티 상태를 관리하며, 데이터베이스에 커밋할 시점까지 변경사항을 보류했다가 한 번에 반영하여 데이터 일관성을 유지합니다.
영속성 컨텍스트를 사용하는 이유
효율적인 데이터베이스 관리
- 객체 상태의 일관성과 성능 최적화를 통해 데이터베이스 자원을 효율적으로 사용하고 데이터베이스와의 통신 비용을 줄일 수 있습니다.
데이터 일관성 유지
- 엔티티 상태를 트랜잭션 범위 내에서 일관되게 유지하여, 트랜잭션 종료 시점에 안전한 데이터 반영이 가능합니다.
코드의 간결화와 유지보수성
- JPA가 제공하는 자동화된 상태 관리 기능을 활용하면 코드가 간결해지고, 복잡한 상태 변환 로직을 작성할 필요가 없어 유지보수가 용이합니다.
'Backend Programming' 카테고리의 다른 글
빈은 어떻게 관리되어서 응답을 하는가? (1) | 2024.10.20 |
---|---|
HTTP란? (0) | 2024.10.05 |
인터넷의 작동 원리 (0) | 2024.10.04 |
HTTPS에 대해서 (0) | 2024.09.29 |
JWT을 사용하여 로그인 서비스 만들기 (2) | 2024.09.22 |