2024.03.12 - [Framework/JPA] - [JPA] 영속성 컨텍스트
[JPA] 영속성 컨텍스트
JPA에서 가장 중요한 두 가지는 객체와 관계형 데이터베이스 매핑과 영속성 컨텍스트이다. 영속성 컨텍스트를 알면 JPA가 내부적으로 어떻게 동작하는지에 대한 이해를 할 수 있게 된다. 엔티티
jh7722.tistory.com
이전 포스팅에서는 영속성 컨텍스트의 기본 개념에 대해 알아봤다. 이번에 특징을 알아보자.
영속성 컨텍스트의 특징
영속성 컨텍스트의 특징은 다음과 같다.
영속성 컨텍스트와 식별자 값
영속성 컨텍스트는 엔티티를 식별자 값(@Id로 매핑된 값)으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.
영속성 컨텍스트와 데이터베이스 저장
JPA는 보통 영속성 컨텍스트에 새로 저장된 엔티티를 DB에 반영하는 시점을 트렌잭션이 커밋되는 순간이라고 한다. 엔티티를 데이터베이스로 반영하는 행위를 플러시(flush)라 한다.
영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다.
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
지금부터 영속성 컨텍스트가 왜 필요하고 어떤 이점이 있는지 알아보도록 하자.
엔티티 조회와 1차 캐시
영속성 컨텍스트는 내부의 캐시를 가지고 있는데 이것을 1차 캐시라고 한다. 영속 상태의 엔티티는 모두 이곳에 저장이 된다.
// 엔티티를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// 엔티티를 영속 -> 1차 캐시에 저장
em.persist(member);
위 코드를 실행하면 1차 캐시에 회원 엔티티를 저장하게 된다. 주의할 점은 회원 엔티티는 아직 데이터베이스에 저장되지 않았다.
1차 캐시는 위 이미지에서 알 수 있듯이 식별자 값(@Id)이다. 식별자 값은 데이터베이스 기본 키와 매핑되어 있다.
이번엔 조회를 해보자.
// 1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
em.find()를 호출하면 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 엔티티가 없으면 데이터베이스를 조회한다.
데이터베이스에서의 조회
만약 em.find() 를 호출 시 엔티티가 1차 캐시에 없다면 엔티티 매니저는 데이터베이스를 조회하게 된다. 이후 해당 엔티티 값을 1차 캐시에 저장한 후(영속상태로 만듬) 엔티티를 반환하게 된다.
// 1차 캐시에 없는 member2 값을 조회한다.
Member findMember2= em.find(Member.class, "member2");
1차 캐시의 이점?
사실 1차 캐시의 이점은 크지 않는다고 한다. 엔티티 매니저는 보통 데이터베이스 트랜잭션 단위로 동작하며 트랜잭션이 끝나면 영속성 컨텍스트도 종료된다.
만약 고객의 요청이 들어온다면 요청의 비즈니스를 끝나면 보통 이 영속성 컨텍스트를 지우게 된다. 그럼 1차 캐시도 다 지워지게 된다. 1차 캐시는 여러 고객(요청자)이 사용(공유)하는 캐시 개념이 아니다. (애플리케이션 전체 공유 캐시는 2차 캐시라 부른다.) 비즈니스 로직 자체가 복잡하면 어느 정도 이점은 있겠지만 보통 생각하는 전체 공유 캐시 개념은 아니면 한 트랜잭션 안에서 사용되는 캐시라고 생각하면 된다.
영속 엔티티의 동일성 보장
다음 코드는 동일 식별자로 조회한 인스턴스를 비교한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // true
a == b의 결괏값을 true가 나오게 된다. 동일 조회 코드를 반복 호출하여도 영속성 컨텍스트에 있는 1차 캐시를 활용하기 때문이다. 1차 캐시에 있는 엔티티 인스턴스를 반환하기 때문에 결과는 참이 나오게 된다.
따라서 영속성 컨텍스트는 엔티티의 동일성을 보장한다.
JPA는 1차 캐시를 통해 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다는 장점이 있다.
엔티티 등록 (트랜잭션을 지원하는 쓰기 지연)
엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 등록해 보자.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지는 Insert SQL를 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 Insert SQL을 보낸다.
transaction.commit(); // 트랜잭션 커밋
위 코드를 실행시켜 보면 트랜잭션 커밋 전 까지는 persist()를 하여도 데이터베이스의 Insert 쿼리가 실행되지 않는다. 내부 쿼리 저장소에 Insert 쿼리를 차곡차곡 모으고 있다 트랜잭션 커밋되면 모아둔 쿼리를 데이터베이스로 보낸다. 이것을 트랜잭션을 지원하는 쓰기 지연이라고 한다.
위 그림을 보면 memberA를 영속화하면 영속성 컨텍스트는 1차 캐시에 회원 엔티티를 저장하면 동시에 회원 엔티티 정보로 등록쿼리를 만든다. 그리고 만들어진 쿼리를 쓰기 지연 SQL 저장소에 보관한다. memberB로 마찬가지의 동작으로 진행된다.
마지막으로 트랜잭션이 커밋되면 엔티티 매니저는 영속성 컨텍스트를 플러시 한다. 플러시는 영속성 컨테이너의 변경 내용을 데이터베이스에 동기화하는 작업을 의미하며 이때, 등록, 수정, 삭제 및 변경사항을 데이터베이스에 반영하게 된다.
이후 데이터베이스에 동기화가 끝나면 실제 데이터베이스의 트랜잭션을 커밋한다.
트랜잭션을 지원하는 쓰기 지연이 가능한 이유?
begin(); // 트랜잭션 시작
save(A);
save(B);
save(C);
commit(); // 트랜잭션 커밋
이런 로직이 있다고 가정하면 두 가지 실행 방식이 생각난다.
첫 번째는 데이터를 저장하는 즉시 등록 쿼리를 데이터베이스로 전달한다. 예제에서는 save() 할 때마다 즉시 데이터베이스에 등록 쿼리를 보내고 마지막에 트랜잭션을 커밋한다.
두 번째는 데이터를 저장하면 등록 쿼리를 보내지 않고 메모리에 모아두며, 트랜잭션을 커밋할 때 모아둔 등록 쿼리를 데이터베이스에 보낸 후에 커밋한다.
같은 트랜잭션 범위 안에서 실행되므로 둘의 결과는 같다. 첫 번째의 경우 바로바로 쿼리를 데이터베이스에 보내지만 커밋되지 않으면 적용되지 않는다. 이것이 의미하는 것은 굳이 바로바로 쿼리를 보내지 않아도 상관없다는 것을 의미하며, 커밋 직전에만 데이터베이스에 SQL을 전달하면 된다.
JPA에서 트랜잭션을 지원하는 쓰기 지원이 가능한 이유이며, 이 기능을 잘 활용하면 모아둔 등록 쿼리를 한 번에 전달해서 성능을 최적화할 수 있다.
엔티티 수정
SQL 수정 쿼리의 문제점
SQL을 직접 작성하는 방식으로 개발을 진행할 경우, 수정 항목에 대한 쿼리를 직접 작성해야한다. 그런데 프로젝트나 요구사항이 커지게 되면 수정 쿼리에 대한 부분도 늘어나게 된다.
다음은 회원의 이름과 나이를 변경하는 SQL이라고 하자.
UPDATE MEMBER
SET
NAME = ?,
AGE = ?
WHERE
ID = ?
회원의 이름과 나이를 변경하는 기능을 개발했는데 회원의 등급을 변경하는 기능을 추가한다고 한다. 그럼 수정 쿼리를 변경하든 추가하든 해야한다.
UPDATE MEMBER
SET
NAME = ?,
AGE = ?,
GRADE = ?
WHERE
ID = ?
UPDATE MEMBER
SET
GRADE = ?
WHERE
ID = ?
만약 합친 쿼리를 만들어서 사용하다면 이름과 나이 변경에 따라 등급 정보를 입력하지 않거나, 등급을 변경할 때 나이나 이름 정보를 입력하지 않는 등의 추가적인 문제를 야기시킬 수 있다.
이런 개발 방식은 수정 쿼리가 많아지는 것 뿐 아니라, 비즈니스 로직을 분석하기 위해선 지속적으로 SQL을 확인해야 되는 문제가 발생된다. 결국 직접적이든 간접적이든 비즈니스 로직이 SQL에 의존하게 된다.
변경감지(Dirty Checking)
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야한다.
transaction.begin(); // 트랜잭션 시작
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 수정
memberA.setUsername("has");
// em.update(memberA); 가 있어야 하지 않을까?
transaction.commit(); // 트랜잭션 커밋
JPA에서 엔티티를 수정하다고 하면 단순히 엔티티만 조회해서 데이터만 변경하면 된다. 트랜잭션 커밋 전 em.update() 같은 메소드가 없어도 변경이 된다. 엔티티 값을 변경하는 것만으로 데이테베이스의 반영시키는 기능을 JPA에서는 변경 감지라고 한다.
JPA 에서 변경감지 기능이 가능한 이유를 알아보자. 위 그림을 살펴보면 스냅샷이는 항목이 존재한다. 스냅샷이란 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 한다. 변경 감지는 이 스냅샷을 이용해 엔티티와 비교하여 변경된 엔티티를 찾게 된다.
주의할 점은 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다. 비영속, 준영속처럼 영속성 컨텍스트의 관리를 받지 못하는 엔티티는 값을 변경해도 데이터베이스에 반영되지 않는다.
엔티티 삭제
// 삭제 대상 조회
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA); //엔티티 삭제
엔티티 삭제도 마찬가지로 조회 대상 엔티티를 조회를 사용한다. em.remove() 에 삭제 대상 엔티티를 넘겨주면 삭제를 진행한다. 물론 엔티티를 즉시 삭제처리를 하는 것은 아니고 등록 엔티티 처럼 쓰기 지연 저장소에 삭제 쿼리를 저장해 트랜잭션 커밋 시점에 반영이 된다.
하지만 주의할 점은 em.remove가 호출되면 memberA는 영속성 컨텍스트에서 제거가 된다. 삭제된 엔티티는 재사용하지 않고 자연스럽게 가비지 컬렉션의 대상이 되도록 두는 것이 좋다.
'Framework > JPA' 카테고리의 다른 글
[JPA] 엔티티 매핑 - 객체와 테이블 매핑 (0) | 2024.03.14 |
---|---|
[JPA] 준영속 상태 (0) | 2024.03.13 |
[JPA] 플러시(flush) (0) | 2024.03.13 |
[JPA] 영속성 컨텍스트 (0) | 2024.03.12 |
[JPA] JPA란? (0) | 2024.03.12 |