본문 바로가기

JPA

[JPA] 영속성 컨텍스트란(Persistence Context)?

영속성 컨텍스트란?

영속성 컨텍스트(Persistence Context)란 해석하면 엔티티를 영구 저장하는 환경이라는 뜻으로 엔티티 매니저를 사용해 데이터를 저장하거나 조회하면 엔티티 매니저는 영속성 컨테스트에 엔티티를 보관하고 관리한다.

 

em.persist(member);

 

위 코드에서 persist() 메소드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다. 조회 기능 메소드인 find()도 마찬가지이다. 동일한 memberId로 Member 객체를 두번 조회할 때 sql문으로 조회했을때는 select 쿼리가 두번 나갔지만 JPA의 find() 메소드를 이용하면 첫 번째 조회때 select 쿼리를 보내 데이터를 조회한 후 영속성 컨텍스트에 저장해놨다가 두 번째 조회부터는 데이터베이스를 조회하기 전 영속성 컨텍스트를 먼저 조회하기 대문에 select 쿼리가 한 번만 나갔다.

엔티티의 생명주기

- 비영속 (new / transient)

영속성 컨텍스트와 전혀 관계가 없는 순수한 객체 상태로 아래와 같이 Member 객체를 생성만 하고 아직 persist() 메소드를 호출하지 않아 영속성 컨텍스트에 저장되지 않았다. 

// 객체를 생성한 상태(비영속)
String id = "100";
Member member = new Member();
member.setId(id);
member.setUsername("key");

- 영속 (managed)

영속 상태는 위에서 Member 객체를 생성한 후 persist() 메소드를 호출하여 데이터가 영속성 컨텍스트에 저장된 상태로 지금부터는 영속성 컨텍스트에 의해 관리된다.

// 객체를 저장한 상태(영속)
...    // Member 객체 생성 코드
em.persist(member);

- 준영속 (detached)

영속성 컨텍스트에 저장되었다가 분리된 상태로 detach() 메소드를 호출하여 준영속 상태로 만든다. close() 메소드를 호출하여 영속성 컨텍스트를 닫거나 clear() 메소드를 영속성 컨텍스트를 초기화 해도 준영속 상태가 된다.

준영속 상태는 거의 비영속 상태에 가깝다. 그렇기 때문에 1차 캐시, 쓰기 지연, 변경 감지와 같은 영속성 컨텍스트가 제공하는 어떤 기능들도 동작하지 않는다.

// 회원 엔티티를 영속성 컨텍스트에서 분리(준영속)
...
em.detach();

em.close();    // 영속성 컨텍스트 닫기

em.clear();    // 영속성 컨텍스트 초기화

- 삭제 (removed)

remove() 메소드를 호출하여 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.

// 객체를 삭제한 상태(삭제)
...
em.remove(member);

영속성 컨텍스트의 특징

- 영속성 컨텍스트와 식별자 값

영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분하기 때문에 영속 상태는 반드시 식별자 값이 있어야 한다. 만약 식별자 값이 없으면 예외가 발생

- 영속성 컨텍스트와 데이터베이스 저장

em.persist()를 통해 데이터를 저장하면 바로 데이터베이스에 저장되지 않고 영속성 컨텍스트에 저장되었다가 트랜잭션을 커밋하는 순간 데이터베이스에 반영한다. 

- 영속성 컨텍스트의 장점

1차 캐시

영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라고 한다. em.find()를 호출하면 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 엔티티가 1차 캐시에 없으면 데이터베이스에서 조회한다. 글 처음에 소개했던 같은 객체를 두번 조회했을때 select 쿼리가 한번만 나가는 이유!!

 

동일성 보장

Member member1 = em.find(Member.class, "member");
Member member2 = em.find(Member.class, "member");

System.out.println(member1 == member2);

 

위 코드와 같이 같은 식별자로 두 번 조회한 뒤 동일성 비교를 하면 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환하기 때문에 true가 출력된다. 영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장한다.

 

트랜잭션을 지원하는 쓰기 지연

영속성 컨텍스트에 데이터를 저장할 때 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 내부 쿼리 저장소에 insert sql문을 저장해 두었다가 커밋할 때 모아둔 쿼리를 한번에 데이터베이스에 보낸다. 이렇게 하면 데이터를 저장할때마다 insert 쿼리를 보내지 않고 모아둔 insert 쿼리를 데이터베이스에 한 번에 전달 함으로써 성능을 최적화 할 수 있다.

 

변경 감지

JPA의 엔티티매니저는 따로 update 메소드가 존재하지 않는다. 대신 조회한 객체의 정보를 수정하는 것만으로 변경사항을 데이터베이스에 반영할 수 있는데 이는 영속성 컨텍스트의 변경 감지 기능 덕분에 가능하다.

JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라고 한다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 데이터베이스에 반영한다.

 

변경 감지 기능 순서

  1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시(flush())가 호출된다.
  2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
  3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
  4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
  5. 데이터베이스 트랜잭션을 커밋한다.

변경 감지 기능을 통해 얻는 이점

  1. 모든 필드를 사용하면 수정 쿼리가 항상 같다. 따라서 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다.
  2. 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.

지연 로딩

@Entity
@Table(name="MEMBER")
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "USERNAME")
    private String username;
    
    @Column(name = "TEAM")
    Team team;
    
}

@Entity
@Table(name="TEAM")
@Getter
@Setter
@NoArgsConstructor
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    Long id;
    
    @Column(name = "TEAMNAME")
    String name;

}

 

위 코드와 같이 Member 클래스와 Team 클래스가 서로 연관되어 있을 때 SQL문을 사용해 조회할 경우 SQL문을 작성할 때 아래 SQL 문 처럼 연관된 객체를 어디까지 탐색할 수 있는지 정해줘야 한다. 

 

SELECT M.*, T.* FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

 

하지만 JPA는 연관된 객체를 사용하는 시점에 즉 아래와 같이 조회한 Member 객체를 통해 Team 객체를 사용할때 적절한 SELECT SQL을 실행한다. 이런 기능을 지연로딩이라고 하며 지연 로딩 덕분에 SQL과 논리적으로 완전히 독립되어 개발자 입장에서 SQL문이 어떻게 작성됐는지 알 필요 없이 마음껏 연관된 객체를 사용할 수 있다.

 

// 처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);

Team team = member.getTeam();

플러시(flush())

플러시(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.

flush() 메소드를 호출하면 다음과 같은 일들이 일어난다.

  1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교한 후 수정된 엔티티를 찾고 수정 쿼리를 만들어 쓰기 지연 SQL저장소에 저장한다.
  2. 쓰기 지연 SQL 저장소의 쿼리를 데이터 베이스에 전송한다.

영속성 컨택스트를 플러시하는 방법

- 직접 호출

직접 엔티티매니저의 flush() 메소드를 호출한다. ex) em.flush()

 

- 트랜잭션 커밋 시 플러시 자동 호출

데이터베이스에 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 데이터베이스에 반영되지 않는다. 그렇기 때문에 JPA는 트랜잭션을 커밋할 때 flush()를 자동으로 호출한다.

 

- JPQL 쿼리 실행 시 플러시 자동 호출

em.persist(mamberA);
em.persist(mamberB);
em.persist(mamberC);

// 중간에 JPQL 실행
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();

 

위와 같이 memberA, memberB, memberC 엔티티를 엔티티매니저의 persist() 메소드를 이용해 영속 상태로 만들었다. 이 멤버 엔티티들은 아직 데이터베이스에는 반영되지 않은 상태이다. 이때 JPQL을 이용하여 Member 데이터베이스를 검색하면 아직 데이터베이스에 반영되지 않은 상태이기 때문에 쿼리 결과로 조회되지 않는다. 이런 문제점을 해결하고자 JPA는 JQPL을 실행할 때 flush()를 자동 호출한다. 

'JPA' 카테고리의 다른 글

[JPA] 영속성 전이 (CASCADE), 고아객체 제거 (ORPHAN)  (0) 2022.10.05
[JPA] 프록시, 즉시로딩, 지연로딩  (0) 2022.10.04
[JPA] 연관관계 매핑  (0) 2022.10.02
[JPA] JPA 란?  (0) 2022.10.01
[JPA] LazyInitializationException  (0) 2022.05.26