JPA의 가장 중요한 개념 중 하나가 영속성 컨텍스트라는 것이다.
만약 영속성 컨텍스트를 잘 모르겠다면!
아래 글을 먼저 보고 오길 추천한다.
영속성 컨텍스트는 간단하게 말해서 "엔티티를 영구 저장하는 환경"이라는 뜻이다. 지금은 잘 이해가 가지 않더라도 아래 내용을 보다보면 조금은 이해가 될 것이라 확신한다! 이번 글에서는 영속성 컨텍스의 이점에 대해서 상세하게 설명해 보고자 한다.
JPA에서 영속성 컨텍스트가 필요한 이유는 캐싱이나 버퍼링 등을 해서 성능이나 사용상의 이점을 얻을 수 있다는데 있다. JPA는 마이바티스 같은 SQL Mapper와 다르게 바로 쿼리가 실행되지 않는다. DB와 자바 어플리케이션 사이에 있는 가상 데이터베이스 같은 개념이 JPA 라고 보면 이해가 더 쉽다. 이 JPA에서 가장 주요한 개념 중 하나가 영속성 컨텍스트 이다.
영속성 컨텍스트의 이점
- 1차 캐시
- 동일성(Identity) 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지 (Dirty Checking)
- 지연 로딩 (Lazy Loading)
위의 영속성 컨텍스트의 이점에 대한 내용만 보는 것 보다는 실제 코드를 통해서 이해하면 더 쉽게 이해할 수 있다.
1차 캐시
엔티티 매니저를 통해 엔티티가 영속 상태가 되었다면, 이후에 동일 엔티티를 조회를 하는 경우 영속성 컨텍스트 내의 1차 캐시를 뒤져서 해당 데이터를 리턴하게 된다. DB에 쿼리를 다시 날리는 것이 아니라 1차 캐시에서 조회하기 때문에 약간의 조회 성능 향상을 기대할 수 있다. 단, 트랜잭션 단위로 1차 캐시를 쓰기 때문에 성능 이점은 높은 편은 아니다. 여러명의 고객의 요청에서 사용하는 캐시가 아니라 동일 트랙잭션 내에서만 사용하기 때문이다.
1차 캐시 테스트 코드
@SpringBootTest
@Transactional
class JpaTest {
@Autowired
EntityManager em;
@Autowired
MemberRepository memberRepository;
/**
* 1차 캐시 테스트
*/
@Test
void firstCacheTest() {
Long id = 1L;
Member member = new Member();
member.setName("member1");
memberRepository.save(member);
System.out.println("==== 쿼리 조회 ====");
Member findMember1 = memberRepository.findById(id).orElseThrow();
System.out.println("### findMember1 = " + findMember1.getName());
System.out.println("==== 1차 캐시 조회 ====");
Member findMember2 = memberRepository.findById(id).orElseThrow();
System.out.println("### findMember2 = " + findMember2.getName());
}
}
일반적으로 생각하면 조회를 2번 하는 경우 쿼리가 2번 나간다고 생각하기 쉽지만, JPA로 동일한 식별자(id)를 가진 엔티티를 같은 트랙잭션에서 1회 이상 조회하는 경우 아래 로그와 같이 select 쿼리는 1번만 실행 된다.
1차 캐시 테스트 코드 로그
동일성(Identity) 보장
JPA는 영속 엔티티의 동일성을 보장해준다. SQL로 조회하는 경우 데이터가 같더라도 참조값이 달라 다른 객체로 인식하지만, 영속 엔티티의 경우 참조값이 똑같아서 같은 객체 인식한다.
동일성 보장 테스트 코드
@SpringBootTest
@Transactional
class JpaTest {
@Autowired
MemberRepository memberRepository;
/**
* 테스트 코드 실행 전 데이터 등록
*/
@BeforeEach
void init() {
Member member = new Member();
member.setName("member1");
memberRepository.save(member);
}
/**
* 동일성 보장 테스트
*/
@Test
void identityTest() {
Long id = 1L;
Member findMember1 = memberRepository.findById(id).orElseThrow();
Member findMember2 = memberRepository.findById(id).orElseThrow();
System.out.println("### findMember1 = " + findMember1);
System.out.println("### findMember2 = " + findMember2);
System.out.println("findMember1 == findMember2 : " + (findMember1 == findMember2));
// 동일한 객체가 맞는지 비교
assertEquals(findMember1, findMember2);
}
}
조회를 2번 했지만 동일한 참조값을 가지며, == 비교 결과 또한 동일한 객체로 인식한다.
트랜잭션을 지원하는 쓰기 지연
보통은 insert 쿼리를 호출하는 시점에 데이터베이스에 쿼리를 바로 날리게 되지만, 영속성컨텍스는 트랜잭션 커밋 시점에 쿼리를 날린다. 엔티티를 persist 하는 시점이 아니라 트랜잭션 커밋 시점에 "쓰기 지연 SQL 저장소"에 있던 쿼리가 flush 되어 DB에 반영된다.
위와 같은 것을 버퍼링 기능이라고 한다. persist 실행 시점에 쿼리가 DB에 날아가면 최적화를 할 수 있는 여지가 없다. 이러한 기능으로 최적화가 필요한 경우 최적화를 할 수 있다. (참고 : hibernate.batch_size 를 설정하면 설정한 사이즈로 쿼리를 모았다가 나중에 날릴 수 있음)
쓰기 지연 테스트 코드
@SpringBootTest
class JpaTest {
@Autowired
EntityManagerFactory emf;
@Autowired
MemberRepository memberRepository;
/**
* 트랜잭션을 지원하는 쓰기 지연 테스트
*/
@Test
void writeBehind() {
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
System.out.println("====== 트랜잭션 start ======");
transaction.begin();
System.out.println("====== member1 저장 ======");
Member member1 = new Member();
member1.setName("member1");
em.persist(member1);
System.out.println("====== member2 저장 ======");
Member member2 = new Member();
member2.setName("member2");
em.persist(member2);
System.out.println();
System.out.println("====== 트랜잭션 커밋 start ======");
transaction.commit();
System.out.println("====== 트랜잭션 커밋 end ======");
}
}
쓰기지연 테스트 코드 실행 로그
변경 감지 (Dirty Checking)
변경 감지 기능으로 set을 통해서 자바 컬렉션 처럼 엔티티를 수정할 수 있다. set만 했는데, update 쿼리가 날아간다고 생각하면 된다. 단, 트랜잭션 내에서 변경 감지 기능을 사용할 수 있다.
영속성 컨텍스트 내 1차 캐시에 들어온 엔티티를 스냅샷을 떠놓고, 트랜잭션이 커밋되는 시점에 내부적으로 flush가 호출되면서 현재의 엔티티와 스냅샷을 비교하면서 변경된 부분을 "쓰기 지연 SQL 저장소"에 추가하고 DB에 반영한다. persist를 호출하지 않아도 변경된 내용을 감지해서 update 쿼리가 DB에 날아가게 되는 것이다.
변경 감지 테스트 코드
@SpringBootTest
class JpaTest {
@Autowired
EntityManagerFactory emf;
@Autowired
MemberRepository memberRepository;
/**
* 변경 감지 테스트
*/
@Test
void dirtyChecking() {
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
System.out.println("====== 트랜잭션 start ======");
transaction.begin();
System.out.println();
System.out.println("====== 엔티티 조회======");
Member member = em.find(Member.class, 1L);
System.out.println("member.getName() = " + member.getName());
System.out.println();
System.out.println("====== 엔티티 수정======");
member.setName("userA change");
System.out.println("member.getName() = " + member.getName());
System.out.println();
System.out.println("====== 트랜잭션 커밋 start ======");
transaction.commit();
System.out.println("====== 트랜잭션 커밋 end ======");
Member changeMember = em.find(Member.class, 1L);
System.out.println("changeMember.getName() = " + changeMember.getName());
}
}
변경 감지 테스트 코드 실행 로그
지연 로딩 (Lazy Loading)
JPA를 사용하다 보면 엔티티 연관관계를 배우게 된다. 필요한 경우 연관관계를 세팅해서 member를 조회하지만, 관련 있는 team의 데이터도 함께 가져와서 멤버가 속한 팀의 데이터까지 함께 조회 할 수 있다. 하지만 항상 팀의 데이터가 필요한 것은 아니기 때문에 멤버의 데이터만 가지고 오고 싶을 때도 있다. 이럴 때 사용하는 것이 지연 로딩이다.
연관관계가 되어 있는 엔티티를 실제로 사용하는 시점에 조회하는 것을 지연로딩이라고 생각하면 된다. 처음 멤버를 조회할 때는 팀 데이터를 프록시로 가지고 있다가 실제 사용하는 시점에 조회 쿼리를 날리게 되는 것이다. 지연로딩은 실무에서 중요한 부분이기 때문에 추후에 지연 로딩 관련해서 추가로 포스팅 예정이다.
'Java | spring > JPA' 카테고리의 다른 글
JPA 조회 동작 방식 비교 : findById(), getOne(), getReferenceById() (0) | 2024.03.27 |
---|---|
JPA 영속성 컨텍스트 및 내부 동작 방식 (2) | 2023.10.08 |
JPA 사용 하는 이유는? (+ ORM 과 SQL mapper 비교) (0) | 2022.10.17 |
JPA Entity & 영속성 컨텍스트 & 라이프 사이클 알아보자! (0) | 2022.10.14 |
JPA란 무엇인가? Mybatis와 차이점 (+기초 무료 인강 추천) (0) | 2022.10.14 |
댓글