15장 고급 주제와 성능 최적화

  • 예외처리
  • 엔티티 비교
  • 프록시 심화 주제
  • 성능 최적화

예외 처리

  • JPA 표준 예외(javax.persistence.PersistenceExcpetion의 자식 클래스)
    • 트랜잭션 롤백을 표시하는 예외
    • 트랜잭션 롤백을 표시 하지 않는 예외
JPA 예외 스프링 변환 예외
javax.persistence.PersistenceException org.springframework.orm.jpa.JpaSystemException
javax.persistence.NoResultException org.springframework.dao.EmptyResultDataAccessException
javax.persistence.EntityNotFoundException org.springframework.orm.jpa.JpaOptimisticLockingException
javax.persistence.RollbackException org.springframework.transaction.TransactionRequiredException
javax.persistence.EntityExistsException org.springframework.dao.DataIntegrityViolationException

스프링 프레임워크의 JPA 예외 변환 서비스 계층에서 데이터 접근 계층의 구현 기술에 직접 의존하는 것은 좋은 설계라 할 수 없다. 서비스 계층에서 JPA의 예외를 직접 사용하면 JPA에 의존 하게 된다. 스프링 프레임 워크는 이런 문제를 해결하려고 데이터 접근 계층에 대한 예외를 추상화 해서 개발자에게 제공한다.

스프링 프레임워크에 JPA 예외 변환기 적용
  • XML
    <bean class="org.springframework.dao.annotaion.PersistenceExceptionTranslationPostProcessor"/>
    
  • Java Config
    @Bean
    public PersistenceExceptionTranslationPostProcessorexceptionTranslation(){
        return new PersistenceExceptionTranslationPostProcessor();
    }
    

    엔티티 비교

    영속성 컨텍스트가 같을 때 엔티티 비교
@Transcational //트랜잭션 안에서 실행
public class MemberServiceTest(){

  @Test
  public void main {
  Member member = new Member("kim");

  Long saveId = memberservice.join(member);

  Member findMember = memberRepository.findOne(saveId);
  assertTrue(member == findMember); // 참조값 비교
    }
 }
  @Transactional
  public class MemberService {
    public Long join(Member member) {
      ...
      memberRepository.save(member);
      return memger.getId();
      }
  }

  @Repository
  public class MemberRepository {

   public void save(Member member){
     em.persist(member);
   }

   public Member findOne(Long id){
     return em.find(Member.class, id);
   }
 }
  • 동일성 ==
  • 동등성 equals
  • 데이터 베이스 동등성 : @Id 데이터 베이스 식별자가 동일
영속성 컨텍스트가 다를 때 엔티티 비교

public class MemberServiceTest(){

  @Test
  public void main {
  Member member = new Member("kim");

  Long saveId = memberservice.join(member);

  Member findMember = memberRepository.findOne(saveId);
  assertTrue(member == findMember); // 참조값 비교
    }
 }
  @Transactional
  public class MemberService {
    public Long join(Member member) {
      ...
      memberRepository.save(member);
      return memger.getId();
      }
  }

  @Repository
  @Transactional
  public class MemberRepository {

   public void save(Member member){
     em.persist(member);
   }

   public Member findOne(Long id){
     return em.find(Member.class, id);
   }
 }
  • 동일성 == (실패)
  • 동등성 equals
  • 데이터 베이스 동등성 : @Id 데이터 베이스 식별자가 동일

프록시 심화 주제

영속성 컨텍스트와 프록시
public void 영속성 컨텍트스_프록시(){
     Member newMember = new Member("member", "회원1");
     em.persist(newMember);
     em.flush();
     em.clear();

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

     log.debug(refMember.getClass());
     log.debug(findMember.getClass());

     Assert.assertTrue(refMember == findMember);
}

     refMember = class jpabook.advanced.Member_$$_jvst843_0
     findMember = class jpabook.advanced.Member_$$_jvst843_0
  • 영속성 컨텍스트는 프록시로 조회한 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면 원본 엔티티가 아닌 처음 조회된 프록시를 반환 한다.
public void 영속성 컨텍트스_프록시(){

     Member newMember = new Member("member", "회원1");
     em.persist(newMember);
     em.flush();
     em.clear();

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

     log.debug(refMember.getClass());
     log.debug(findMember.getClass());

     Assert.assertTrue(refMember == findMember);
}
     refMember = class jpabook.advanced.Member
     findMember = class jpabook.advanced.Member
  • 원본 엔티티를 먼저 조회하면 영속성 컨텍스트는 원본 엔티티를 이미 데이터베이스에서 조회 했으므로 프록시를 반환할 이유가 없다. 따라서 em.getReference()를 호출해도 프록시가 아닌 원본을 반환한다.
프록시 타입 비교

프록시는 원본 엔티티를 상속 받아서 만들어지므로 프록시로 조회한 엔티티의 타입을 비교 할 때는 == 비교를 하면 안되고 대신에 instanceof를 사용해야 한다.

@Test
public void 프록시_타입비교(){
  Member newMember = new Member("member1", "회원1");
  em.persist(newMember);
  em.flush();
  em.clear();

  Member refMember = em.getRefernce(Member.class, "member1");

  System.out.println("refMember Type = " + refMember.getClass());

  Assert.assertFalse(Member.class == refMember.getClass()); //false
  Assert.assertTrue(refMember instanceof Member); //false

  //출력 결과
  refMember Type = class jpabook.advanced.Member_$$_jvsteXXX
프록시 동등성 비교

IDE나 외부 라이브러리를 사용해서 구현한 equals() 메소드로 엔티티를 비교 할 때, 비교 대상이 원본 엔티티이면 문제가 없지만 프록시면 문제가 발생 할 수 있다.

@Entity
@Table(name="zt_country")
@Getter
@Setter
public class Country extends BaseTraceableEntity<String>
implements BaseSynchronizableEntity {
    private static final long serialVersionUID = -2397565525342569487L;

    @Id
    @Column(name="country_code", length = ColumnSizeConstants.COUNTRY_CODE)
    @JsonProperty
    private String countryCode;

    @Column(name = "name", length = ColumnSizeConstants.NAME)
    @JsonProperty
    private String name;

    @Override
    public boolean equals(Object obj) {        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Country other = (Country)obj;
        if (countryCode == null) {
            if (other.countryCode != null)
                return false;
        } else if (!countryCode.equals(other.countryCode))
            return false;
        return true;
    }
}
@Test
public void 프록시와_동등성비교(){
  Member saveMember = new Member("member1", "회원1");
  em.persist(saveMember);
  em.flush();
  em.clear();

  Member newMember = new Member("member1", "회원1");
  Member refMember = em.getRefence(Member.class, "member1");

  Assert.assertTrue(newMember.equals(refMember)); //false
if (getClass() != obj.getClass())
            return false;

if(!(obj instanceof Member)) return false;

프록시 멤버 변수에 직접 접근 할 경우 , 프록시는 실제 데이터를 가지고 있지 않기 때문에 프록시의 멤버 변수에 직접 접근 하면 아무값도 조회 할 수 없다. 프록시의 데이터를 조회할 때는 접근자 Getter를 사용해야 한다.

Country other = (Country)obj;
if (countryCode == null) {
    if (other.countryCode != null)
            return false;
     } else if (!countryCode.equals(other.countryCode))
          return false;

Country other = (Country)obj;
if (countryCode == null) {
    if (other.getCountryCode != null)
            return false;
     } else if (!countryCode.equals(other.getCountryCode))
          return false;

상속 관계와 프록시

Item (부모) Book (자식)

@Test
public void 부모타입으로_프록시조회(){

  Book saveBook = new Book;
  saveBook.setName("jpaBook");
  saveBook.setAuthor("kim");
  em.persist(saveBook);

  em.flush();
  em.clear();

  Item proxyItem = em.getRefence(Item.class, saveBook.getId());

  if(proxyItem instaceof Book){
    System.out.println("proxyItem instanceof Book");
    Book book = (Book) proxyItem; //java.lang.ClassCastException
    System.out.println("책 저자 =" + book.getAuthor);
  }

  Assert.assertFalse(proxyItem.getClass == Book.class);
  Assert.assertFalse(proxyItem instanceof Book); //false
  Assert.assertTrue(proxyItem instanceof Item);

프록시를 부모 타입으로 조회하면 부모 타입을 기반으로 프록시가 생성되는 문제가 있다.

  • instanceof 연산을 사용할 수 없다.
  • 하위타입으로 다운캐스팅을 할 수 없다.
JPQL로 대상 직접 조회
Book jpqlBook = em.createQuery("select b from Book b where b.id=:bookId", Book.class)
                 .setParameter("bookId", item.getId())
                 .getSingleResult();
HibernateProxy 사용
package org.hibernate.proxy;
import java.io.Serializable;

/**
 * Marker interface for entity proxies
 *
 * @author Gavin King
 */
public interface HibernateProxy extends Serializable {
    /**
     * Perform serialization-time write-replacement of this proxy.
     *
     * @return The serializable proxy replacement.
     */
    public Object writeReplace();

    /**
     * Get the underlying lazy initialization handler.
     *
     * @return The lazy initializer.
     */
    public LazyInitializer getHibernateLazyInitializer();
}

Item item = orderItem.getItem();
Item unProxyItem = unProxy(item);

if(unProxyItem instanceof Book) {
  Book book = (Book)unProxyItem;
}

public static <T> unProxy(Object entity){
  if(entity instanceof HibernateProxy) {
      entity = ((HibernateProxy) entity).getHibernateLazyIntializer()
                                      .getImplementation();
  }

  return (T) entity;
}
  • 주의 사항 item == unProxyItem // false

성능 최적화

엔티티가 영속성 컨텍스트에 관리되면 1차 캐시부터 변경 감지까지 얻을 수 있는 혜택이 많다. 하지만 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다. 이때는 읽기 전용으로 엔티티를 조회하면 메모리 사용량을 최적화 할 수 있다.

성능 최적화 기법
스칼라 타입 조회 읽기 전용 엔티티 사용
읽기 전용 쿼리 힌트 읽기 전용 엔티티 사용
읽기 전용 트랜잭션 읽기 전용 트랜잭션 사용
트랜잭션 밖에서 읽기 읽기 전용 트랜잭션 사용
스칼라 타입으로 조회

스칼라 타입은 영속성 컨텍스트가 결과를 관리 하지 않는다.

select o from Order o (변경 전)
select o.id, o.name, o.price from Order p(변경 후)
읽기 전용 쿼리 힌트 사용

하이버네이트 전용 힌트인 org.hibernate.readOnly를 사용하면 엔티티를 읽기 전용으로 조회 할 수 있다.

TypedQuery<Order> query = em.createQuery(“select o from Order o”, Order.class);
query.setHint(“org.hibernate.readOnly”, true);
읽기 전용 트랜잭션 사용

트랜잭션에 readOnly=true 옵션을 주면 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않는다. 영속성 컨텍스트를 플러시 하지 않으니 엔티티의 등록, 수정, 삭제 할 때 일어나는 스냅샵 비교와 같은 무거운 로직들을 수행 하지 않으므로 성능이 향상 된다.

@Transactional(readOnly=true)
트랜잭션 밖에서 읽기

트랜잭션 밖에서 읽는다는 것은 트랜잭션 없이 엔티티를 조회 한다는 뜻이다. 트랜잭션을 사용하지 않으면 플러시가 일어나지 않으므로 조회 성능이 향상된다.

@Transactional(propagation=Propagation.NOT_SUPPORTED)
읽기 전용 트랜잭션과 읽기 전용 쿼리 힌트 사용
@Transactional(readOnly=true) //읽기 전용 트랜잭션
public List<DataEntity> findDatas() {
    return em.createQuery(“select d from DataEntity d”, DataEntity.class)
    .setHint(“org.hibernate.readOnly”,true); //읽기 전용 쿼리 힌트
    .getResultList();
}
  • 읽기 전용 트랜잭션 : 플러시를 작동 하지 않도록 해서 성능 향상

  • 읽기 전용 엔티티 사용:엔티티를 읽기 전용으로 조회해서 메모리 절약

배치

수천에서 수만 건 이상의 엔티티를 한번에 등록 할 때 주의 할 점은 영속성 컨텍스트에 엔티티가 계속 쌓이지 않도록 일정 단위마다 영속성 컨텍스트의 엔티티를 데이터 베이스에 플러시 하고 영속성 컨텍스트를 초기화 해야 한다.

등록 배치
Entitymanager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

for(int i=0; i< 10000; i++){
    Product product = new Product(“item” + i, 10000);
    em.persist(product);

    if(i % 100 == 0){
      em.flush();
    em.clear();
}}
tx.commit();
em.close();
수정 배치
  • 페이징 처리
Entitymanager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
int pageSize = 100;
for(int i=0; i< 10; i++){
    List<Product> resultList = em.createQuery(“select p from Product p”, Product.class)
        .setFirstResult(i * pageSize)
        .setMaxResults(pageSize)
        .getResultList();

    for(Product product : resultList){
        product.setProduct(product.getPrice() + 100);
    }
    em.flush();
    em.clear();
}}
tx.commit();
em.close();
  • 하이버네이트 scroll 사용

JPA 는 JDBC 커서를 지원 하지 않는다. 따라서 커서를 사용 하려면 하이버네이트 세션을(Session)을 사용 해야 한다.

Entitymanager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
Session session = em.unwrap(Session.class)
tx.begin();
ScrollableResults scroll = session.createQuery(“select p from Product p”)
                .setCachMode(CacheMode.IGNORE)
                .scroll(ScrollMode.FORWARD_ONLY);

int count = 0;

whild(scroll.net()){
    Product p = (Product) scroll.get(0);
    p.setPrice(p.getPrice() + 100);

    count++;

    if(count % 100 == 0) {
      session.flush();
      session.clear();
    }
 }
 tx.commit();
 session.close();
  • 하이버네이트 무상태 세션 사용
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
ScrollableResults scroll = session.createQuery("select p from Product p").scroll();

while(scroll.next()) {
    Product p = (Product) scroll.get(0);
    p.setPrice(p.getPrice() + 100);
    session.update(p) //직접 update를 호출 
}
tx.commit();
session.close();
트랜잭션을 지원하는 쓰기 지연과 성능 최적화
insert(member1);
insert(member1);
insert(member1);
insert(member1);
insert(member1);

commit();

5번의 insert sql과 1번의 커밋으로 총 6번 데이터 베이스와 통신

최적화를 위해 5번의 insert SQL을 모아서 한번에 데이터베이스로 보내면 된다.

<property name="hibnernate.jdbc.batch_size" value = "50" />

SQL 배치는 같은 SQL 일 때만 유효 하다. 중간에 다른 처리가 들어가면 SQL 배치를 다시 시작한다.

em.persist(new Member()); //1
em.persist(new Member()); //2
em.persist(new Member()); //3
em.persist(new Member()); //4
em.persist(new Child());  //5 , 다른 연산
em.persist(new Member()); //6
em.persist(new Member()); //7

1,2,3,4 를 모아서 하나의 SQL 배치를 실행 하고 5를 한번 실행하고 6,7를 모아서 실행한다.

results matching ""

    No results matching ""