3 분 소요

스터디를 진행하며 JPA와 영속성 컨텍스트에 대해 학습한 내용을 정리해봤습니다.

영속성 컨텍스트

영속성을 영구 저장하는 환경

EntityManager.persist(Entity entity);

EntityManagerpersist 메서드로 엔티티를 영속성 컨텍스트에 저장할 수 있습니다.

영속성 컨텍스트란 무엇일까요?

출처: Baeldung

영속성 컨텍스트는 애플리케이션과 DB 사이에 존재하는 개념적인 영역입니다.

EntityManagerEntityManagerFactory에 의해 생성되며 영속성 컨텍스트를 관리하고 접근할 수 있습니다.

출처: 자바 ORM 표준 JPA 프로그래밍

엔티티의 생명주기는 비영속, 영속, 준영속, 삭제로 분류됩니다.

이 중 영속 상태는 영속성 컨텍스트에 의해 관리되며 1차 캐시, 변경 감지, 동일성 보장, 지연 로딩 등의 도움을 받을 수 있습니다.

영속 상태인 엔티티는 바로 DB에 저장되지 않고 영속성 컨텍스트에 의해 관리됩니다.

예제 코드

@Getter
@Setter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@EqualsAndHashCode(of = "id")
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String username;
    private String role;

    public Member(String username, String role) {
        this.username = username;
        this.role = role;
    }
}

변경감지

Member member = new Member("시안", "백엔드");

Member가 개명을 했다면 어떻게 해야 할까요?

UPDATE MEMBER SET (username = ?, role = ?) WHERE id = 1;

JDBC 또는 MyBatis 등을 사용한다면 위와 같은 쿼리문을 작성해야 합니다.

JPA를 쓰게 된다면 이 과정이 매우 편해집니다.

@Test
@DisplayName("변경 감지")
void dirtyCheck() {
    // given
    Member member = new Member("시안", "백엔드");
    entityManager.persist(member);
    entityManager.flush();

    // when
    Member findMember = memberRepository.findById(1L).get();
    findMember.setRole("잡부");
    entityManager.flush();

    // then
    assertThat(member.getRole()).isEqualTo(findMember.getRole());
}

실행된 쿼리를 확인해보면

엔티티의 값을 변경했을 뿐인데 flush() 가 호출된 순간 update 쿼리가 날라간 것을 확인할 수 있습니다.

JPA를 사용하면 영속 상태인 엔티티의 데이터를 변경하면 변경 감지는 언제 일어날까요?

출처: 자바 ORM 표준 JPA 프로그래밍

변경 감지는 flush()가 호출될 때 일어나며 1차 캐시에서 엔티티와 최초 영속화되었을 때의

스냅샷을 비교합니다.

이 때 엔티티가 스냅샷과 다르다면, 쓰기 지연 SQL 저장소에 UPDATE 쿼리를 저장하고 flush가 발생하여 DB에 반영됩니다.

@DynamicUpdate 어노테이션을 사용하면 수정된 데이터만 업데이트하는 쿼리를 생성하게 설정할 수 있습니다.

이 덕분에 개발자는 도메인 계층에서 비즈니스 로직을 작성할 수 있고, 객체 지향적으로 설계할 수 있게 됩니다.


1차 캐시와 동일성

JPA가 제공하는 유용한 기능이며 성능상 이점과 함께 엔티티의 동일성을 보장하게 됩니다.

@Test
@DisplayName("1차 캐시와 동일성")
void findById() {
    // given
    Member member = new Member("시안", "백엔드");
    memberRepository.save(member);

    // when
    Member findMember1 = memberRepository.findById(member.getId()).get();
    Member findMember2 = memberRepository.findById(member.getId()).get();

    // then
    assertThat(findMember1).isEqualTo(findMember2);
}

위와 같이 findById() 를 두 번 조회했을 때 어떻게 될까요?

  1. 두 객체는 동일할까?
  2. 전체 쿼리는 몇 번 실행될까?
정답 정답은 두 객체는 동일하며 쿼리는 실행되지 않습니다. (만약 미리 저장된 객체를 조회한 경우라면 1번 실행됩니다.)


save() 메서드로 엔티티를 저장했을 때 바로 DB에 저장되는 것이 아니라 영속성 컨텍스트에 저장됩니다. 영속성 컨텍스트는 엔티티의 키를 ID로, 값을 엔티티 인스턴스로 하는 Map이 존재합니다.

Member 엔티티는 @GeneratedValue 어노테이션에 의해 영속화될 때 ID를 부여받게 되고

findById() 를 이용하여 조회할 시 우선적으로 영속성 컨텍스트를 확인합니다.

만약 ID 생성 전략이 IDENTITY라면 DB에 저장이 되어야만 기본 키를 알 수 있으므로 flush()를 강제로 호출하지 않아도 내부적으로 INSERT 쿼리가 나가게 됩니다.

영속성 컨텍스트 내부의 Map에는 key = 1, value = Member(”시안”, “백엔드) 가 이미 존재하기 때문에 쿼리가 발생하지 않는 것입니다.

@Test
@DisplayName("1차 캐시와 동일성")
void findByUsername() {
    // given
    Member member = new Member("시안", "백엔드");
    memberRepository.save(member);

    // when
    Member findMember1 = memberRepository.findByUsername("시안").get();
    Member findMember2 = memberRepository.findById(member.getId()).get();
    Member findMember3 = memberRepository.findByUsername("시안").get();

    // then
    assertThat(findMember1).isEqualTo(findMember2);
    assertThat(findMember2).isEqualTo(findMember3);
}

위와 같이 조회하는 경우에는 어떻게 될까요?

  1. 세 객체는 동일할까?
  2. 조회 쿼리는 몇 번 실행될까?
정답 역시 세 객체는 동일하며 조회 쿼리는 총 2번 날라가게 됩니다. 왜 그럴까요?


위에 적은대로 영속성 컨텍스트 내부의 Map은 식별자를 key로 사용합니다.

때문에 ID를 통한 조회가 아니라면 매번 DB를 통해 조회하게 됩니다.

트랜잭션

트랜잭션은 DB 작업의 최소 단위로 원자성, 일관성, 격리성, 지속성을 보장해야 하며 영속성 컨텍스트는 트랜잭션과 생명주기를 같이 합니다.

@Inherited
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Transactional {...}

@Transactional 어노테이션은 클래스, 메서드에 붙일 수 있으며 해당 범위가 트랜잭션에 의해 관리됩니다.

출처: 자바 ORM 표준 JPA 프로그래밍

해당 어노테이션이 붙은 메서드가 실행되면 스프링 트랜잭션 AOP가 동작하게 되고, 메서드를 동작하며 모든 작업이 마무리되면 COMMIT 또는 예외 발생 시 ROLLBACK을 수행하게 됩니다.

영속성 컨텍스트의 flush()는 COMMIT이 발생할 때 호출되며 이 때 쓰기 지연 저장소에 저장된 쿼리들이 DB로 날라가게 됩니다.

@Inherited 어노테이션을 보면 알 수 있듯이 트랜잭션은 상속 → 전파되는 특징이 있습니다. 때문에 고수준 메서드에 어노테이션을 붙이면 고수준 메서드에 의해 호출되는 저수준 메서드들도 트랜잭션 범위에 속하게 됩니다.

@Transactional 의 속성인 isolation, propagtion으로 격리 수준과 전파 옵션을 설정할 수 있습니다.

또한 트랜잭션이 같으면 같은 영속성 컨텍스트를, 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다는 특징이 있습니다. 이 특징 덕분에 멀티 스레드 환경에서 안전할 수 있으며 개발자는 싱글 스레드를 사용하듯이 단순하게 개발에 전념할 수 있습니다.

태그:

카테고리:

업데이트:

댓글남기기