ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA의 쓰기지연 기능 확인해보기 (transactional write-behind)
    개발/JPA & Hibernate 2021. 7. 16. 19:00

    JPA의 특징중 하나는 영속성 컨텍스트 (Persistence Context)내에서 쓰기지연(transactional write-behind)이 발생한다는 것입니다.

    이 글에서는 JPA와 Mybatis를 이용해 쓰기지연을 했을때와 안했을 때를 비교해보려고 합니다. (Mybatis는 쓰기지연을 하지 않습니다.)

    쓰기지연이란? (transactional write-behind) 영속성 컨텍스트에 변경이 발생했을 때, 바로 데이터베이스로 쿼리를 보내지 않고 SQL 쿼리를 버퍼에 모아놨다가, 영속성 컨텍스트가 flush 하는 시점에 모아둔 SQL 쿼리를 데이터베이스로 보내는 기능입니다.

    사전 지식 - 영속성 컨텍스트는 동일성(Identity)를 보장한다. - 영속성 컨텍스트는 1차 캐시의 역할을 한다. 즉 객체를 저장하고 있다. - flush가 발생하는 경우는 3가지다. 1. 직접 flush() 호출 2. jpql 실행시 3. 트랜잭션 commit() 시

    (ID AUTO_INCREMENT) 동일한 객체를 insert 하는 경우

    1. JPA - 트랜잭션 내에서 같은 객체를 persist - flush 반복하는 경우

    트랜잭션 내에서는 객체를 한번 persist하면 flush하더라도 영속성 컨텍스트에 남아있습니다. 그래서 같은 객체를 다시 persist해도 이미 '동일한 객체'가 영속성 컨텍스트에 snapshot으로 존재하고, 변경이 발생하지 않았으므로 한번의 insert만 발생합니다.

    // Address Id : @GeneratedValue(strategy = GenerationType.IDENTITY)
    Address addr = new Address("seoul");
    em.persist(addr);
    em.flush();
    System.out.println("flush");
    
    em.persist(addr);
    tx.commit();
    System.out.println("commit");
    
    /**
     * 결과:
     *
     * insert  into  Address (ADDRESS_ID, dong)  values  (null, ?)
     * flush
     * flush
     * commit
     */

    만약 flush 이후 원본 객체를 수정한 뒤 flush(commit)하면 영속성 컨텍스트가 dirty checking을 하고 update 쿼리를 발생시킵니다.

    Address city = new Address("city");
    em.persist(city);
    em.flush();
    
    city.setDong("dong"); // 변경 감지 (dirty checking)
    em.persist(city);
    em.flush();
    
    /**
     * 결과: 
     *
     * into  Address  (ADDRESS_ID, dong)  values  (null, ?)
     * update  Address  set  dong=?  where  ADDRESS_ID=?
     */

    데이터베이스를 조회해보면 하나의 데이터만 저장되었습니다.

    2. Mybatis (쓰기지연 X)

    Address address = new Address("city");
    sqlSession.insert("mybatis.AddressMapper.insertAddress", address);
    System.out.println("insert");
    
    sqlSession.insert("mybatis.AddressMapper.insertAddress", address);
    System.out.println("insert");
    
    sqlSession.commit();
    System.out.println("commit");
    
    /**
     * 결과:
     *
     * 1. insert into address (dong) values ('city') {executed in 0 msec}
     * insert
     * 1. insert into address (dong) values ('city') {executed in 0 msec}
     * insert
     * commit
     */

    mybatis는 쓰기지연이 없기때문에 바로 바로 쿼리를 데이터베이스로 보냅니다. Address의 ID는 AUTO_INCREMENT 이므로 아무 문제없이 id를 증가시키며 2개의 insert를 실행합니다.

    고정된 ID를 사용하는 경우

    JPA

    동일한 결과가 나타납니다. 영속성 컨텍스트는 동일성을 보장하니까 당연할 수 있습니다.

    • (하나의 객체를 여러번 persist 해도 결국 동일한 객체다)

    물론 새로운 객체를 만들어서 두개의 객체가 동일한 ID를 사용하면 예외가 발생합니다. 영속성 컨텍스트가 동일성을 보장하는 것이지, 동등성 기준으로 판단하지 않기 때문입니다.

    Mybatis

    Mybatis에 '캐시'는 존재하지만 JPA처럼 쓰기버퍼 개념이 존재하진 않는다. 그래서 동일한 객체여도 동일한 객체인지 알 방법이 없습니다.

    동일한 객체를 여러번 insert 시도하면 다음과 같은 예외가 발생합니다.

    Cause: org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.ADDRESS(ADDRESS_ID)"; SQL statement: insert into address (ADDRESS_ID, DONG) values (?, ?) [23505-197]

    결론

    JPA의 영속성 컨텍스트는 동일성을 보장하기 때문에 같은 객체에 대해선 ID 할당을 어떤 방식으로 하던 한번의 INSERT만 발생합니다.

    그에반해 Mybatis는 영속성 컨텍스트라는 개념이 없고, 쓰기지연을 위한 공간등이 없기 때문에 동일한 객체라도 각각 INSERT가 발생합니다.

    JPA를 제대로 쓰려면 영속성 컨텍스트의 특징을 잘 이해하고, 쓰기지연은 그 하나이다. 화이팅

    댓글

Designed by Tistory.