개요
- DB와 객체를 어떻게 설계하여 매핑(Mapping)할 것인가?
- 내부에서 JPA는 어떻게 동작할까?

어플리케이션의 개발시 하나의 EntityManagerFactory를 통해서 고객의 요청(트랜잭션)이 올때마다 EntityManager가 생성되고, 각 커넥션을 통해 DB와 상호작용이 이루어진다. 이때 EntityManager마다 1대1로 영속성 컨텍스트가 생성된다.
영속성 컨텍스트란?
- 엔티티를 영구 저장하는 환경이라는 의미
- 애플리케이션과 데이터베이스 사이에서 객체를 임시 보관하는 가상의 캐시같은 역할을 한다.
- 엔티티매니저를 통해서 엔티티를 [저장/조회]하면 엔티티매니저는 영속성 컨텍스트를 통해서 엔티티를 보관하고 관리한다 -> 저장 뿐 아니라 조회시에도 DB의 데이터는 영속성 컨텍스트의 1차 캐시에 영속화된다.
- 영속성 컨텍스트 안에는 1차 캐시가 존재한다. 후술하겠지만 (영속성 컨텍스트 == 1차 캐시)라고 이해하면 될 것 같다.
엔티티의 생명주기
- 비영속: 객체가 생성되어 있지만 영속 컨텍스트(EntityManager)와의 연결이 이루어지지 않은 상태(JPA와 관련이 없는 순수한 객체 상태)
- 영속: 객체를 생성하고 EntityManager를 통해 em.persist(member);와 같이 persist가 실행된 상태. 사실 영속상태라고 반드시 DB에 저장되는(쿼리 전달) 것은 아니다.
- 준영속 : em.detach(member)(특정 엔티티만 분리), em.clear()(영속성 콘텍스트 초기화), em.close()(영속성 콘텍스트 종료) 등의 작업으로 엔티티를 영속성 컨텍스트에서 분리한 상태. 더이상 엔티티 매니저가 관리하지 않는 상태를 말한다.
- 삭제 : em.remove(member) -> 객체를 삭제한 상태
트랜잭션
트랜잭션이란 더 이상 쪼갤 수 없는 최소 작업의 단위를 의미한다.
만약 DB의 데이터를 수정하는 도중에 exception이 발생한다던지 하는 경우, DB의 데이터들은 일단 수정이 되기 전의 상태로 다시 되돌아가야 한다. 그렇기에 트랜잭션은 성공하여 commit되거나, 실패하여 rollback되는 두 가지 선택지만 존재하여 데이터의 무결성을 보장한다.
트랜잭션이 시작될 때, JPA는 DB 커넥션 풀에서 HikariCP와 같은 라이브러리를 사용하여 EntityManager로 DB 커넥션을 가져온다. 그리고 트랜잭션이 종료되면, (JPA의 기본 OSIV 설정이 true로 되어있다는 가정 하에) API response가 나가기 전까지 영속성 컨텍스트의 커넥션을 유지한다.
(OSIV: 트랜잭션이 끝나도 영속성 컨텍스트를 유지하는 JPA 전략이다. 기본값은 true.
https://dodeon.gitbook.io/study/kimyounghan-spring-boot-and-jpa-optimization/04-osiv 참고하면 될 것 같다)
영속성 컨텍스트 장점
1차 캐시
- 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데, 이를 1차 캐시라고 한다.
- EntityManager로 조회하거나 변경하는 모든 엔티티는 먼저 1차 캐시에 저장되고, flush를 호출하거나 commit(자동 flush)하면 1차 캐시의 내역이 DB로 동기화된다. (단, 동기화가 1차캐시에서의 삭제를 의미하는것은 아니다!)
- 같은 트랜잭션 내에서 캐시값을 조회할때 db조회 전에 key를 id로, value를 엔티티로 하는 캐시에 있는 값을 반환받아 조회할 수 있다. (DB로 쿼리를 날리지 않고 대신 1차캐시를 조회하기 때문에 여러번 호출되지 않는다). 만약 1차 캐시에 없다면 DB에서 조회해서 1차캐시에 저장후 반환한다. 캐시메모리나 TLB와 유사하게 이해하자.
- EntityManager가 트랜잭션 단위로 존재하기 때문에, 트랜잭션이 끝나면 1차 캐시도 지워진다.
영속 엔티티의 동일성 보장
1차 캐시로 반복가능한 읽기등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 제공한다. 쉽게 말해, 1차캐시의 존재로 인해서 같은 트랜잭션 내에서 같은 객체에 대해 == 비교가 가능하게끔 한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a==b); // true returned
트랜잭션을 지원하는 쓰기 지연 (buffering)
EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(memberA);
em.persist(memberB);
//여기까지 쿼리문은 전달되지 않는다.
transaction.commit(); //트랜잭션을 커밋하는 순간 쿼리문을 DB에 전달
말 그대로 데이터 변경 시 즉시 DB에 쿼리를 날려 반영하는 것이 아니라, 커밋 시 한꺼번에 쿼리를 반영한다.
em.persist를 통해 객체를 영속 상태로 만들면 엔티티를 일단 1차 캐시에 저장하고, 엔티티를 분석해서 필요한 SQL문을 만들어 저장해둔다. 이후 commit()이 이루어져야만 저장소에서 flush되어 DB에 SQL문이 전송된다.
왜 쿼리문을 모아서 전송할까? 만약 persist문을 바로바로 sql을 작성하여 전송하게 되면, 구현체(hibernate)가 네트워크를 여러번 거쳐 DB에 접근해야 하고, 최적화 또한 불가능하게 된다.
하지만 주의할 점이 하나 있는데, 쓰기 지연으로 인해 한꺼번에 쿼리가 반영될 때, 코드를 작성한 순서대로 쿼리가 나가지 않는다.

특히 delete가 맨 마지막에 반영되기에, delete 후 update하는 동작을 기대했다면 다르게 동작할 수 있다.
변경 감지(Dirty Checking)
JPA 첫 포스트의 마지막에서 이야기했던 '객체에 setter만 호출해주어도 알아서 db를 업데이트해주어 마치 컬렉션을 다루듯 객체와 DB를 다루게 해주는' 기능에 관한 내용이다.
transaction.begin();
Member memberA = em.find(Member.class, "MemberA"); //DB에서 1차캐시로 영속화
memberA.setUsername("hi");
memberA.setAge("10");
//em.update(memberA); 가 필요하지 않을까..?
transaction.commit(); //commit 직전시점에 이전의 entity와 커밋 entity를 비교하여 변경감지(Dirty checking)
setter로 영속 상태 엔티티의 값을 변경했으면, 커밋 이전에 update를 통해 갱신해주어야 하지 않을까? 라는 생각을 자연스럽게 하게 된다. 하지만 jpa는 마치 컬렉션에서 list.add나 list.set의 호출만을 통해서 값을 추가하고 업데이트하듯, setter를 통해서 자연스럽게 DB의 정보까지도 바뀌도록 해준다(자동으로 업데이트 쿼리를 추가해준다!).
이것은 em.find()로 EntityManager를 통해 DB에서 데이터를 가져올 때 그 데이터가 1차 캐시에 영속화된 이후, JPA가 트랜잭션을 커밋하기 직전 시점에 1차 캐시에 있는 엔티티 스냅샷과 커밋 시점의 엔티티 스냅샷을 비교해서 변경된 부분이 있다면 UPDATE sql을 자동으로 생성해서 DB로 전송해주기 때문에 가능한 것이다.
변경 감지를 이용하여 다음과 같이 보다 객체 친화적인 코드를 짤 수 있다.
//UserService.java
userRepository.save(new UserInfo(user.getId(), user.getEmail(), bCryptPasswordEncoder.encode(newPw), user.getUsername(), RoleType.USER, null, user.getPhoneNumber(), user.getIsGuardian()));
예를 들어, 고객의 비밀번호를 변경하는 로직이 있다고 할 때 영속계층에서 직접 save함으로서 쿼리문을 날리게 해줄수도 있다.
//UserInfo.java
public void updatePassword(String newPassword) {
this.password = newPassword;
}
하지만, setter를 통해 엔티티의 값을 변경시켜주기만 하면 커밋 시점에 자동으로 변경 감지가 이루어져서 업데이트 쿼리가 날아가게 된다.
플러시
트랜잭션 커밋시 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 것을 말한다.
JPA는 커밋시에도 해당 트랜잭션을 완료하고 모든 변경 사항들을 DB에 영구적으로 반영한다. 이와 조금 다르게 flush는 영속성 컨텍스트의 변경 사항들을 DB와 동기화하는 것이다. 커밋이 발생하지 않았기에 flush된 내용도 롤백될 수 있으며, 프로그래밍 언어에서 사용되는 flush같이 영속성 컨텍스트가 내보내고 비워지는 것이 아니라, 영속성 컨텍스트의 변경내용을 DB에 동기화해주는 것이다.
em.flush(); //직접호출
tx.commit(); //커밋시 자동호출
query = em.createQuery("select m from Member m", Member.class); //JPQL 쿼리 실행시
List<Member> members = query.getResultList();
1. em.flush();를 이용하면 커밋이 이루어지기 전 강제로 플러시가 호출되어 쿼리문이 DB로 반영된다.
(엔티티와 스냅샷 비교 후 변경된 것에 대한 SQL 생성 > 생성된 SQL을 쓰기 지연 SQL 저장소에 등록 > 쓰기 지연 SQL 저장소에 등록된 쿼리를 DB로 전송) // em.flush(); 실행시 이 모든 단계가 이루어지며, 마지막 단계를 flush라고 한다
2. commit시점에 자동호출된다.
3. jpql쿼리 실행시에 문제를 방지하기 위해서 자동으로 쿼리를 실행하기 전에 플러시가 호출된다.
4. jpa를 이용하는 것이 아니라 DB에 바로 접근시에는 flush();를 수동으로 호출해주어야 한다.
@Transactional 어노테이션의 범위 안의 메소드가 종료될 때 자동으로 commit(), flush()가 일어난다.
준영속 상태
em.persist나 em.find를 통해 영속성 컨텍스트에 올라간(1차 캐시에 올라간상태, JPA가 관리하는 상태) 엔티티가 영속성 컨텍스트에서 다시 분리된 상태를 말한다. 직접 사용하는 경우는 흔하지 않다.
'[ Backend ] > Spring DB, JPA' 카테고리의 다른 글
| [JPA] 기본 키 매핑-SEQUENCE 전략, 에러 해결 (0) | 2022.01.02 |
|---|---|
| [JPA] 엔티티 매핑 (0) | 2021.12.31 |
| [JPA] JPQL 예시 (0) | 2021.12.28 |
| [JPA] JPA 설정, 기본 실습 (0) | 2021.12.28 |
| [JPA] JPA 공부를 시작하며 + JPA, Spring Data JPA, Hibernate (0) | 2021.12.27 |