Chapter15. 고급 주제와 성능 최적화

김신영·2023년 2월 8일
0

JPA

목록 보기
12/14
post-thumbnail
post-custom-banner

예외 처리

JPA 표준 예외들은 javax.persistence.PersistenceException 의 자식 클래스이다.
모두 RuntimeException 으로 Unchecked Exception 이다.

Transaction Rollback을 표시하는 예외

트랜잭션 롤백을 표시하는 예외설명
javax.persistence.EntityExistsExceptionEntityManager::persist 호출 시, 이미 같은 엔티티가 있으면 발생
javax.persistence.EntityNotFoundExceptionEntity가 존재하지 않으면 발생.
javax.persistence.OptimisticLockException낙관적 락 충돌 시 발생
javax.persistence.PessimisticLockException비관적 락 충돌 시 발생
javax.persistence.RollbackExceptionEntityManager::commit 실패 시 발생.
롤백이 표시되어 있는 트랜잭션 커밋 시에도 발생한다.
javax.persistence.TransactionRequiredException트랜잭션이 필요할 때 트랜잭션이 없으면 발생.
트랜잭션 없이 Entity를 변경할 때 주로 발생.

Transaction Rollback을 표시하지 않는 예외

트랜잭션 롤백을 표시하는 예외설명
javax.persistence.NoResultExceptionQuery::getSingleResult 호출 시 결과가 하나도 없을 때 발생
javax.persistence.NonUniqueResultExceptionQuery::getSingleResult 호출 시 결과가 둘 이상일 때 발생
javax.persistence.LockTimeoutException비관적 락에서 시간 초과 시 발생
javax.persistence.QueryTiemoutException쿼리 실행 시간 초과 시 발생

Spring Framework의 JPA Exception 변환

JPA 예외Spring 변환 예외
javax.persistence.PersistenceExceptionorg.springframework.orm.jpa.JpaSystemException
javax.persistence.NoResultExceptionorg.springframework.dao.EmptyResultDataAccessException
javax.persistence.NonUniqueResultExceptionorg.springframework.dao.IncorrectResultSizeDataAccessException
javax.persistence.LockTimeoutExceptionorg.springframework.dao.CannotAcquireLockException
javax.persistence.QueryTimeoutExceptionorg.springframework.dao.QueryTimeoutException
javax.persistence.EntityExistsExceptionorg.springframework.dao.DataIntegrityViolationException
javax.persistence.EntityNotFoundExceptionorg.springframework.orm.jpa.JpaObjectRetrievalFailureException
javax.persistence.OptimisticLockExceptionorg.springframework.orm.jpa.JpaOptimisticLockingFailureException
javax.persistence.PessimisticLockExceptionorg.springframework.dao.PessimisticLockingFailureException
javax.persistence.TransactionRequiredExceptionorg.springframework.dao.InvalidDataAccessApiUsageException
javax.persistence.RollbackExceptionorg.springframework.transaction.TransactionSystemException
java.lang.IllegalStateExceptionorg.springframework.dao.InvalidDataAccessApiUsageException
java.lang.IllegalArgumentExceptionorg.springframework.dao.InvalidDataAccessApiUsageException

Spring Framework의 JPA 예외 변환 처리

  • PersistenceExceptionTranslationPostProcessor Bean 등록하면 된다.
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
	return new PersistenceExceptionTranslationPostProcessor();
}
  • xml을 통한 Bean 등록
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

예외 변환 처리 해제 방법

  • 예외를 반환하지 않고 그대로 반환하고 싶다면, 직접 throws 명시하면 된다.

트랜잭션 롤백 시 주의사항

  • Database 반영사항만 롤백한다.
  • 컨텍스트에 존재하는 객체를 원상태로 복구해주지 않는다.
  • 따라서 EntityManager::clear 메서드를 호출해서 영속성 컨텍스트를 초기화 한 다음에 사용해야 한다.

Entity 비교

  • 동일성 / Identical (==)
  • 동등성 / equivalent (equals)
  • 데이터 베이스 동등성 (@Id)

같은 영속성 컨텍스트에서는 모두 적용 가능.
그렇지 않다면 equals 혹은 @Id 를 비교해야한다.

영속성 컨텍스트가 같을 때, 엔티티 비교

@SpringBootTest
@Transactional // 트랜잭션 안에서 테스트를 실행하고, Rollback 처리 해준다.
class MemberServiceTest {

    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberRepository memberRepository;

    @DisplayName("회원가입이 정상적으로 작동한다.")
    @Test
    void 회원가입() {
        // Given
        String givenMemberName = "rolroralra";
        Member member = MemberDataSet.testData(givenMemberName);

        // When
        Long savedId = memberService.insertMember(member);

        // Then
        assertThat(memberRepository.findById(savedId)).isPresent()
            .get()
            .isEqualTo(member)
            .hasFieldOrPropertyWithValue("name", givenMemberName);
    }
}

https://blog.kakaocdn.net/dn/pnicA/btrrDdyRr7O/vacYVBy6ig6UUPrFsnTJrK/img.png

영속성 컨텍스트가 다를 때, 엔티티 비교

@SpringBootTest
//@Transactional
class MemberServiceTest {

    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberRepository memberRepository;

    @DisplayName("회원가입이 정상적으로 작동한다.")
    @Test
    void 회원가입() {
        // Given
        String givenMemberName = "rolroralra";
        Member member = MemberDataSet.testData(givenMemberName);

        // When
        Long savedId = memberService.insertMember(member);

        // Then
        assertThat(memberRepository.findById(savedId)).isPresent()
            .get()
            .isEqualTo(member) // Test 실패
            .hasFieldOrPropertyWithValue("name", givenMemberName);
    }
}

// org.opentest4j.AssertionFailedError: 
// expected: com.example.springbootwithjpa.domain.Member@686ee555
//  but was: com.example.springbootwithjpa.domain.Member@2bcaec2e
// Expected :com.example.springbootwithjpa.domain.Member@686ee555
// Actual   :com.example.springbootwithjpa.domain.Member@2bcaec2e

https://blog.kakaocdn.net/dn/pnicA/btrrDdyRr7O/vacYVBy6ig6UUPrFsnTJrK/img.png

Untitled

프록시 객체 이슈

영속성 컨텍스트와 프록시

  1. 영속성 컨텍스트는 프록시로 조회된 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면, 원본 엔티티가 아닌 처음 조회된 프록시를 반환한다.

    Member refMember = em.getRefrence(Member.class, 1L);
    Member findMember = em.find(Member.class, 1L);
    
    System.out.println("refMember Type = " + refMember.getClass());
    System.out.println("findMember Type = " + findMember.getClass());
    
    assertThat(refMember == findMember).isTrue();
    assertThat(refMember).isInstanceOf(Member.class);
    
    // refMember Type = class com.example.Member_$$_jvst843_0
    // findMember Type = class com.example.Member_$$_jvst843_0
  2. 원본 엔티티를 먼저 조회하면 영속성 컨텍스트는 원본 엔티티를 이미 데이터베이스에서 조회했으므로 프록시를 반환할 이유가 없다.

    Member findMember = em.find(Member.class, 1L);
    Member refMember = em.getRefrence(Member.class, 1L);
    
    System.out.println("findMember Type = " + findMember.getClass());
    System.out.println("refMember Type = " + refMember.getClass());
    
    assertThat(refMember == findMember).isTrue();
    
    // findMember Type = class com.example.Member
    // refMember Type = class com.example.Member

프록시 타입 비교

  • 프록시는 원본 엔티티를 상속 받아서 프록시 패턴으로 만들어짐
  • 프록시로 조회한 엔티티의 타입을 비교할때는,
    • == 를 사용하면 안됨
    • instanceof 를 사용해야 한다.
em.clear();

Member refMember = em.getReference(Member.class, 1L);

assertThat(refMember.getClass() == Member.class).isFalse();
assertThat(refMember instanceof Member).isTrue();

프록시 동등성 비교 (equals)

  • ❌  프록시의 멤버변수에 직접 접근하면 안됨.
  • ✅  getter 메서드를 통해 사용해야한다.

상관관계와 프록시

https://leejaedoo.github.io/assets/img/proxy2.jpeg

  • 프록시를 부모 타입으로 조회하면 부모 타입 기반으로 프록시가 생성되는 문제가 있다.
    • instanceof 연산이 false 리턴
    • 자식 타입으로 캐스팅이 불가능 (ClassCastException 발생)

해결 방법

  1. JPQL로 자식 타입 직접 조회
    • 단점: 다형성을 활용할 수 없다.
  2. 프록시 벗기기 (HibernateProxy::getHibernateLazyInitializer::getImplmentation)
    • 단점: 원본 엔티티와 프록시가 동일성 실패
  3. 부모 타입에 별도의 인터페이스 적용
  4. Visitor 패턴 (Double Dispatch)
@Test
void inheritanceProxyTest() {
    Book book = Book.createBook("book1", 10000L, 20L, "rolroralra", "isbn1");
    em.persist(book);

    OrderItem orderItem = OrderItem.createOrderItem(book, 10000L, 5L);
    em.persist(orderItem);

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

    OrderItem findOrderItem = em.find(OrderItem.class, orderItem.getId());

    Item item = findOrderItem.getItem();
    
    // 2. HibernateProxy 클래스를 활용하여 프록시 벗기기
    Item unProxyItem = unProxy(item);

    assertThat(item)
        .isNotInstanceOf(Book.class)  // 상관관계 프록시 객체 문제점 발생
        .isInstanceOf(Item.class);

    assertThat(unProxyItem)
        .isInstanceOf(Book.class)  // proxy를 벗김으로써 문제 해결
        .isNotSameAs(item);

    item.accept(new PrintVisitor());  // 4. Visitor 패턴을 활용한 해결
}

/**
 * Hibernate가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메서드
 * @param entity proxy entity
 * @return original entity
 * @param <T>
 */
@SuppressWarnings("unchecked")
private static <T> T unProxy(Object entity) {
    if (entity instanceof HibernateProxy) {
        entity = ((HibernateProxy) entity)
            .getHibernateLazyInitializer()
            .getImplementation();
    }

    return (T) entity;
}

성능 최적화

1+N 문제

1. Fetch Join

2. @BatchSize(size = 100)

  • Annotation
import org.hibernate.annotations.BatchSize;

@BatchSize(size = 100)
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
  • Global Setting
spring.jps.properties.hibernate.default_batch_fetch_size: 500

3. @Fetch(FetchMode.SUBSELECT)

  • 즉시 로딩일 경우
    • 조회 시점에 SubQuery 수행
  • 지연 로딩일 경우
    • 지연 로딩된 엔티티 사용 시점에 SubQuery 수행
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;

@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();

읽기 전용 쿼리 성능 최적화

1. 스칼라 타입으로 조회

2. 읽기 전용 쿼리 힌트 사용

  • 영속성 컨텍스트를 flush 하지 않는다.

3. @Transactional(readOnly = true)

  • 영속성 컨텍스트를 flush 하지 않는다.

4. @Transactional(propagation = Propagation.NOT_SUPPORTED)

  • Transaction 없이 조회

쓰기 지연과 성능 최적화

1. 쓰기 지연과 JDBC 배치

  • spring.jpa.properties.hibernate.jdbc.batch_size: 1000
  • ⚠️ 하지만, Identity 식별자 생성 전략은 쓰기지연이 지원되지 않는다.
    • 데이터베이스에 저장해야 id를 구할 수 있으므로…
  • ⚠️  대량의 엔티티를 배치 처리하려면, 적절한 시점에 꼭 영속성 컨텍스트를 flush하고 clear 해줘야 한다.
spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 1000
  • 예제
em.persiste(new Member()); // 1
em.persiste(new Member()); // 2
em.persiste(new Member()); // 3
em.persiste(new Member()); // 4
em.persiste(new Child()); // 5, 다른 연산
em.persiste(new Member()); // 6
em.persiste(new Member()); // 7

// 1,2,3,4 를 모아서 하나의 SQL 실행
// 5를 한번 SQL 실행
// 6, 7을 모아서 하나의 SQL 실행

// 총 3번의 SQL 실행

2. 쓰기 지연과 Application 확장성

  • 데이터베이스 테이블 레코드에 Lock이 걸리는 시간을 최소화
profile
Hello velog!
post-custom-banner

0개의 댓글