JPA - 영속성 & 영속성 컨텍스트에 대해

3 분 소요

JPA에서 가장 중요한 영속성(Persistence)에 대해 정리해보았다.


JPA의 영속성(Persistence)

JPA 에서는 Java의 Class Entity를 DB의 Entity로 매핑한다. 이때 매핑 방법은 DTO와 같은 Mapper 클래스를 만들어 주거나 어노테이션을 이용한 매핑 방법 등이 있다. 이러한 트랜잭션들은 JPA의 영속성을 통해 이루어진다.

JDBC와의 차이점

먼저 JDBC로 구현된 insert SQL문을 보면 아래와 같다.

String sql = "INSERT INTO customer(id, name) VALUES(?, ?)";

try {
    // 드라이버 로딩
    Class.forName("com.mysql.jdbc.Driver");
    
    // Connection 생성
    con = DriverManager.getConnection("url", "username", "pw");
    
    // pstmt 생성
    pstmt = con.prepareStatement(SQL);
    pstmt.setString(1, "001");
    pstmt.setString(2, "user1");

    // SQL문 execute
    pstmt.executeUpdate();

} catch (SQLException e) {
    // ERROR
}

이처럼 JDBC는 직접 DB에서 SQL문을 작성하는 것과 같은 느낌을 받을 수 있다.

똑같은 쿼리를 JPA로 작성하면 다음과 같다.

Customer customer = new Customer("001", "user1");

// emf : EntityManager 생성을 위한 Singleton 객체
EntityManagerFactory emf = Persistence.createEntityManagerFactory("mydb");
EntityManager em = emf.createEntityManager();
EntityTransaction tr = em.getTransaction();

// 변경 감지 begin
tr.begin();

// customer 객체 영속 상태로 설정
em.persist(customer);

// 변경 정보 commit
tr.commit();

insert를 위한 SQL문을 작성하지 않아도 JPA에서는 영속 상태인 엔티티들의 변경점을 확인하고 적절한 쿼리를 생성해준다. 이러한 SQL문들은 트랜잭션이 commit 될 때 DB로 실제 쿼리가 전송되는 것이다.

영속성 관리 : 영속성 컨텍스트

이러한 영속 상태인 객체들은 영속성 컨텍스트에서 관리가 된다. JPA에서 쿼리를 생성할 엔티티들을 보관하는 곳이라 생각하자.

transition-persistence-context

영속성 컨텍스트를 통해 엔티티를 관리하면 아래와 같은 장점들이 있다.

  1. 1차 캐시

    JPA에서는 DB의 엔티티 데이터들을 조회하기 이전에 영속성 컨텍스트에 조회하려고 하는 엔티티가 있는지 먼저 확인한다. 이를 통해 DB에 직접 접근하지 않고 캐시를 통해 엔티티에 접근이 가능하다.

    하지만 1차 캐시는 다른 스레드들과 공유하지 않으므로 멀티 스레드에서의 1차 캐시의 이점은 없다!

  2. 쓰기 지연

    트랜잭션을 커밋하기 전까지는 실제로 DB에 쿼리가 전송되지 않는다는 점이다. 이를 통해 앞에서 생성된 쿼리들은 일괄적으로 한꺼번에 전송이 가능하다.

  3. 동일성 보장

    영속성 컨텍스트에서 관리되는 엔티티들은 동일성을 보장한다. 따라서 동일한 엔티티를 조회시에 엔티티의 값만 복제되는 것이 아닌, 동일한 엔티티가 조회되는 것이다.

  4. 변경 감지

    앞에서 JDBC와 비교했을 때의 장점으로 엔티티에 대한 SQL문을 직접 작성하지 않아도 JPA에서 변경을 감지하여 알맞은 쿼리문을 자동으로 생성해주는 것이다. persist, merge, remove와 같은 메소드를 사용하여 간단히 트랜잭션을 완료할 수 있다.

  5. 지연 로딩

    클래스 A, B가 One-to-One 관계로 매핑되어 있다고 가정할 때 클래스 A의 엔티티를 조회 하는 경우를 생각해보자.

    클래스 A의 어떤 데이터 값만 필요한 경우에도 A의 엔티티를 조회하게 되면 연관관계를 가진 클래스 B의 엔티티가 함께 조회될 것이다. 이는 시스템이 복잡해질 수록 불필요한 쿼리문이 생기고 성능 하락을 가져오는 위험이 있다.

    따라서 JPA에서는 지연 로딩을 제공하는데 엔티티 A를 조회해도 A의 연관관계를 가진 B의 값에 접근하기 전까지는 B를 조회하지 않는 것이다. 필요에 따라 EAGER, LAZY 로딩을 사용가능하다.

Data JPA의 경우

기존 JPA에서는 Create를 수행할 때 persist(), Update를 수행할 때 merge()를 호출하여 인스턴스의 영속성 관리를 해주었다. Data JPA를 사용할 때는 조금 다른데, 먼저 생성할 Repository에 대한 인터페이스를 생성하고 JpaRepository와 같이 Data JPA에서 제공하는 인터페이스를 상속받는다.

import org.springframework.data.jpa.repository.JpaRepository;

@Repository // 생략 가능
public interface MyRepository extends JpaRepository<Customer, Integer> {
}
@Autowired
MyRepository myRepository;

Customer customer = new Customer("001", "user1");

myRepository.save(customer);

그 다음 생성한 Repository를 DI(dependency injection) 해준뒤 사용하는데 이 때 Create, Update 모두 save() 메소드를 통해 수행한다. 나는 save() 메소드의 구조가 궁금해서 JpaRepository 인터페이스의 구현체인 SimpleJpaRepository 클래스를 확인해보았다.

image

save() 메소드는 Data JPA만의 특이한 점이 있는건 아니었다.

  • entity가 새롭게 생성된 경우(Create) em.persist()

  • 기존의 entity를 수정하는 경우(Update) em.merge()

이처럼 두가지 메소드를 save()로 묶어서 처리하는 것을 확인할 수 있었다. 더 궁금해서 찾아본 결과 stackoverflow에서 관련 내용을 찾아볼 수 있었다.

영속성 API 설계에는 두가지 주요 접근 방식이 있다.

  • insert/update approach insert 또는 update 메소드를 호출하여 오브젝트를 DB에 입력,수정
  • Unit of Work approach 영속성 라이브러리에 객체들을 추가한 후 작업이 종료 시(트랜잭션이 종료 시) DB에 자동으로 flush 하는 접근 방식. 이 때 영속성 라이브러리를 통해 관리되는 객체는 기본 키로 식별된다.

JPA는 두번째 접근 방식을 따른다. Data JPA의 save()는 일반적인 JPA의 merge() 방식을 사용하므로 위의 접근 방식처럼 엔티티의 영속성을 관리하게 된다. 따라서 오브젝트를 save() 하는 것은 오브젝트의 미리 정의된 id 값의 여부에 따라 insert 또는 update를 의미하며 save()create()라고 부르지 않는 이유를 설명한다.

출처: https://stackoverflow.com/a/11881628

References

댓글남기기