티스토리 뷰

728x90
반응형

Goals

  • 영속성 컨텍스트란?
  • 엔티티 생명주기
  • 영속성 컨텐스트의 장점
  • 플러시(flush())

영속성 컨텍스트란?


JPA를 이해하는데 가장 중요한 용어는 영속성 컨텐스트(persistence context)다. 우리말로 해석해 보면 ‘엔티티를 영구 저장하는 환경’이라는 뜻이다.

엔티티 매니저(EntityManager)로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리할 수 있게 된다. 일반적으로 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다.

예를 들어 다음과 같은 메서드들을 호출할 때

em.persist(entity);               // entity를 영속성 컨텍스트에 저장(영속 상태로 만듬)
em.find(Entity.class, pk);        // entity를 DB에서 로드(영속 상태로 만듬)

그럼 영속성 컨텍스트는 엔티티 객체들의 상태를 추적해서 변경사항을 반영한다거나, 데이터베이스에 저장하거나 로드할 수도 있다. 이렇게 영속성 컨텍스트 안에서 관리되는 엔티티들을 영속(managed) 상태라고 한다. 영속 상태에 대해 알기 위해서는 먼저 엔티티의 생명주기를 알아야 한다.

 

엔티티 생명주기


엔티티에는 총 4가지 상태가 존재한다.

 

  • 비영속(New/Transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(Managed) : 영속성 컨텍스트에 저장된 상태
  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed) : 삭제된 상태

 

비영속(New/Transient)

엔티티 객체를 막 생성하기만 했을 때로, 아직 영속성 컨텍스트나 데이터베이스와 전혀 관련이 없는 상태다.

// 엔티티 객체 생성
Member.member = Member.builder()
        .name("회원1")
        .age(20)
        .build();

 

영속(Managed)

엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장하거나, DB로부터 조회했을 때 영속성 컨텍스트가 엔티티를 관리하게 된다. 즉, 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 한다.

em.persist(member);
em.find(Member.class, id);

이때 영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다.

  • 식벽자 값 : @Id로 테이블의 기본키와 매핑한 값

 

만약 식별자 값이 없으면 예외가 발생한다.

 

준영속(detached)

영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않게 됐을 때 준영속 상태가 된다.

em.detach(member);  // 회원 엔티티를 영속성 컨텍스트에서 분리
em.clear();         // 영속성 컨텍스트 초기화
em.close();         // 엔티티 매니저(영속성 컨텍스트) 종료

 

삭제(Removed)

엔티티를 영속성 컨텍스트와 데이터베이스에서(flush() 호출 시) 삭제한다.

em.remove(member)

 

영속성 컨텍스트의 장점


엔티티들을 데이터베이스에 바로 반영하지 않고 굳이 영속성 컨텍스트라는 공간에서 관리하는 이유가 있다.

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지(Dirty Checking)
  • 지연 로딩

 

1차 캐시

영속성 컨텍스트 내부에 캐시를 가지고 있는데 이것을 1차 캐시라고 한다. 다음 코드를 실행하면 영속성 컨텍스트의 1차 캐시에 회원 엔티티가 저장된다.

// 엔티티 객체 생성
Member.member = Member.builder()
        .id("member1")
        .name("회원1")
        .build();

em.persist(member);

그림처럼 key는 식별자 값(@Id)이고 값은 엔티티 인스턴스다. 1차 캐시는 엔티티를 조회할 때, DB를 거치지 않고 메모리에서 가져올 수 있어 성능상 이점이 매우 크다.

다음 코드를 실행해 보자.

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

그럼 먼저 1차 캐시에서 Member 클래스 중 @Id 값이 member1인 엔티티가 있는지 찾는다. 위 1차 캐시에 해당하는 엔티티가 있으므로 굳이 DB까지 거치지 않고 메모리에서 조회할 수 있다.

만약 1차 캐시에 없는 엔티티를 조회하면 어떻게 될까?

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

  1. @Id 값이 member2Member 엔티티가 1차 캐시에 있는지 조회한다.
  2. 1차 캐시에 없으므로 데이터베이스에서 조회한다.
  3. 조회한 데이터로 엔티티를 생성해 영속성 컨텍스트의 1차 캐시에 저장한다.
  4. 조회한 엔티티를 반환한다.

즉, 1차 캐시에 없는 엔티티는 데이터베이스에서 조회한 후, 새롭게 영속 상태로 만들어 관리한다.

 

동일성 보장

동일성(identity)은 실제 인스턴스(인스턴스 주소)가 같다는 의미로 참조 값을 비교하는 ==를 사용한다.

동등성(equality)은 실제 인스턴스가 가지고 있는 값이 같다는 의미로 equals() 메서드를 사용한다.

영속성 컨텍스트에서 관리되는 엔티티를 비교할 때, @Id 값이 같다면 1차 캐시에 있는 동일한 엔티티 인스턴스를 반환한다.

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

따라서 위 코드를 실행한 후, a == b를 해보면 true가 나온다. 만약 영속성 컨텍스트를 사용하지 않고 DB를 바로 거쳐 인스턴스를 생성했다면, 매번 새로운 인스턴스가 생성되어 동일성이 보장되지 않았을 것이다.

 

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

엔티티 매니저가 관리하는 엔티티의 모든 변경은 트랜잭션 안에서 이루어 져야 한다.

EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

em.persist(memberA);
em.persist(memberB);

tx.commit();

위 코드가 실행됐다고 했을 때, em.persist()를 호출한다고 해서 DB에 INSERT 쿼리를 날리지 않는다. 엔티티 매니저는 commit()이 호출되기 전까지는 내부 쿼리 저장소에 실행된 메서드에 해당하는 SQL들을 모아 둔다.

이것을 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)이라 한다.

이렇게 쿼리 저장소에 모아둔 SQL들은 commit()이 호출되면 데이터베이스에 날린다. 이 과정이 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업이다. 이때 데이터베이스에서도 커밋이 된다.

이 기능을 잘 활용하면 모아둔 쿼리를 데이터베이스에 한 번에 전달할 수 있으므로 성능을 최적화할 수 있다.

 

변경 감지(Dirty Checking)

JPA에서는 엔티티를 수정할 때 따로 update()같은 메서드가 필요 없다. 단지 영속성 컨텍스트 안의 엔티티를 수정하기만 하면, 엔티티의 변경사항을 데이터베이스에 자동으로 반영해 준다.

JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 스냅샷으로 저장한다. 그러다 플러시(flush())가 호출되는 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.

예를 들어, 다음과 같은 코드를 실행해 본다.

entity/Guestbook.java

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
public class Guestbook extends BaseEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gno;
 
    @Column(length = 100, nullable = false)
    private String title;

    @Column(length = 1500, nullable = false)
    private String content;
    
    @Column(length = 50, nullable = false)
    private String writer;

    public void updateTitle(String title) {

        this.title = title;
    }

    public void updateContent(String content) {

        this.content = content;
    }
}

test/…/repository/GuestbookRepositoryTests.java

@SpringBootTest
@Transactional
public class GuestbookRepositoryTests {

        ...

        @Test
        @Commit
        public void updateTest() {

            // Given
            Long gno = 313L;
            String title = "Updated";
            String content = "Updated";

            // EntityTransaction tx = em.getTransaction();
            // tx.begin();

            Guestbook guestbook = guestbookRepository.findById(gno).get();
            System.out.println("----------------업데이트 전 엔티티-----------------");
            System.out.println(guestbook);

            // When
            guestbook.updateTitle(title);
            guestbook.updateContent(content);

            // tx.commit();

            // Then
            guestbookRepository.findById(gno).ifPresent((updatedGuestbook) -> {
                Assertions.assertThat(updatedGuestbook.getTitle()).isEqualTo(title);
                Assertions.assertThat(updatedGuestbook.getContent()).isEqualTo(content);
                Assertions.assertThat(updatedGuestbook.getModDate()).isAfter(updatedGuestbook.getRegDate());
                System.out.println("----------------업데이트 후 엔티티-----------------");
                System.out.println(updatedGuestbook);
            });
        }

Guestbook이라는 엔티티를 업데이트하는 테스트다. 참고로 Spring Boot에서 관리해주는 엔티티 매니저를 사용하기 때문에 트랜잭션을 생성하는 부분은 주석 처리한 후 @Transaction 어노테이션을 사용했다.

코드를 보면 어디에도 UPDATE 쿼리를 호출할 만한 메서드는 보이지 않는다. 단지 조회한 엔티티를 수정하는 코드만 있지만, 데이터베이스를 조회해 보면 변경된 엔티티를 확인할 수 있다.

변경 감지는 아래와 같은 과정으로 일어난다.

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

중요한 점은, 변경 감지 기능은 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다는 것을 잊지 말아야 한다.

그런데 이상한 점이 하나 있다. 위 코드에서는 titlecontent만 수정했는데, 생성된 JPQL에서는 모든 필드 값을 수정하는 것처럼 보인다. 이는 JPA의 수정 기본 전략이 엔티티의 모든 필드를 업데이트하는 것이기 때문이다.

이렇게 함으로써 얻는 장점이 있다.

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

 

단, 데이터베이스에 보내는 데이터 전송량이 증가한다는 단점은 명확하다. 특히나 필드가 많은 엔티티거나 저장되는 내용이 너무 크다면 오히려 장점을 상쇄할 수 있다. 이때는 수정된 데이터만 사용해서 동적으로 UPDATE 쿼리를 생성하는 전략을 선택할 수 있다.

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@DynamicUpdate
public class Guestbook extends BaseEntity {
  • @org.hibernate.annotaions.DynamicUpdate : 수정된 데이터만 사용해서 동적으로 UPDATE 쿼리 생성

참고로 modData 컬럼은 BaseEntity에 있는 필드로, JPA Auditing에 의해 업데이트 시간이 자동으로 기록된다. 그래서 생성된 UPDATE 쿼리에 포함되어 있다.

상황에 따라 다르지만 컬럼이 대략 30개 이상이 되면 기본 방법인 정적 수정 쿼리보다 동적 수정 쿼리가 더 빠르다고 한다. 단, 컬럼이 30개 이상 된다는 것은 테이블 설계 상 책임이 적절히 분리되지 않았을 가능성이 높다. - 김영한

 

( +지연 로딩에 대해서는 프록시 기능을 알아야 하고, 즉시 로딩과 비교가 필요하기 때문에 추후 따로 포스팅 하겠다.)

 

플러시(flush())


플러시(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 기능이다. 즉, 영속성 컨텍스트를 데이터베이스와 동기화하는 것이라고 생각하면 된다.

플러시가 실행되면 아래와 같은 과정이 일어난다.

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

이 플러시를 호출하는 방법은 3가지다.

 

  • 직접 호출

엔티티 매니저의 flush() 메서드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시한다.

 

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

트랜잭션이 커밋되기 전에 JPA가 플러시를 자동으로 호출해서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.

 

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

데이터베이스에 쿼리를 날려야 할 때도 플러시가 자동 실행된다. 예를 들어 보겠다.

em.persist(memberA);
em.persist(memberb);

query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();

query.getResultList()가 호출되면 JPQL이 실행되는데, 그 전에 먼저 flush()가 호출된다. 그렇지 않으면 결과 값에 memberAmemberB가 포함되지 않아 무결성에 큰 문제가 생기기 때문에 이를 방지하기 위함이다.

 

References

 

728x90
반응형

'JPA' 카테고리의 다른 글

[JPA] QueryDSL 간단 (with Gradle, VSCode)  (0) 2023.05.08
[JPA] EntityManagerFactory & EntityManager  (0) 2023.05.07
댓글