일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 1차원 DP
- 2차원 dp
- 99클럽
- @Builder
- @GeneratedValue
- @GenericGenerator
- @NoargsConstructor
- @Transactional
- Actions
- Amazon EFS
- amazon fsx
- Android Studio
- ANSI SQL
- ApplicationEvent
- assertThat
- async/await
- AVG
- AWS
- Azure
- bind
- builder
- button
- c++
- c++ builder
- c03
- Callback
- case when
- CCW
- chat GPT
- CICD
- Today
- Total
기록
[자바 ORM 표준 JPA 프로그래밍] 예외 처리, 엔티티 비교, 프록시 문제 및 성능 최적화(15장) 본문
JPA 활용과 심화 이해
이 글은 "자바 ORM 표준 JPA 프로그래밍" 책의 주요 내용을 바탕으로 작성되었습니다. 특히 15장에서 다룬 예외 처리, 엔티티 비교, 프록시 문제 및 성능 최적화에 관한 내용을 정리한 기록입니다.
1. 예외 처리
1.1 JPA 예외 개요
JPA를 사용할 때 주로 발생하는 예외는 두 가지로 구분됩니다.
- 트랜잭션 롤백이 필요한 예외
- 데이터베이스 상태를 되돌려야 할 정도로 심각한 문제.
- 예시:
DataIntegrityViolationException
,OptimisticLockingFailureException
- 롤백이 필요한 이유는 데이터 무결성 위반이나 잠금 충돌 같은 경우 데이터의 일관성을 보장해야 하기 때문입니다.
- 트랜잭션 롤백이 필요하지 않은 예외
- 시스템에 큰 영향을 주지 않고 처리 가능한 오류.
- 예시:
QueryTimeoutException
,NonTransientDataAccessException
- 가벼운 문제로 간주되며 작업을 중단하지 않아도 됩니다.
스프링 프레임워크는 JPA 예외를 추상화된 형태로 변환하여 DataAccessException
계층의 예외로 제공함으로써 특정 기술에 종속되지 않는 설계를 지원합니다. 주요 예외 유형은 다음과 같습니다:
DataIntegrityViolationException
: 데이터 무결성 위반 시 발생.OptimisticLockingFailureException
: 낙관적 락 충돌 시 발생.QueryTimeoutException
: 쿼리 시간 초과 시 발생.
이처럼 스프링을 통해 변환된 예외는 비즈니스 로직에서 일관된 방식으로 처리할 수 있어 유지보수가 용이합니다.
1.2 스프링에서 JPA 예외 처리 적용 방법
스프링은 PersistenceExceptionTranslationPostProcessor
라는 컴포넌트를 제공하며, 이를 빈으로 등록하면 @Repository
가 붙은 클래스에서 발생하는 JPA 예외가 자동으로 스프링 예외로 변환됩니다.
1.3 트랜잭션 롤백 처리 시 주의점
트랜잭션 롤백은 데이터베이스 작업을 취소하지만, 영속성 컨텍스트에서 관리 중인 엔티티의 상태는 초기화되지 않습니다. 이를 해결하기 위해 다음 방법 중 하나를 사용해야 합니다:
- 새로운 영속성 컨텍스트 생성: 현재 컨텍스트를 대체합니다.
EntityManager.clear()
** 호출**: 기존 컨텍스트를 수동으로 초기화하여 불필요한 데이터가 남지 않도록 합니다.
2. 엔티티 비교
2.1 같은 영속성 컨텍스트에서의 비교: 동일성 보장
같은 영속성 컨텍스트에서 조회된 엔티티는 동일성을 보장합니다. 즉, 두 엔티티가 메모리 주소가 같은 객체로 간주됩니다:
Member findMember = memberRepository.findOne(saveId);
assertTrue(member == findMember); // true
2.2 다른 영속성 컨텍스트에서의 비교: equals 메서드를 구현
서로 다른 트랜잭션(영속성 컨텍스트)에서 조회된 엔티티는 동일하지 않으며, 비교 결과가 달라질 수 있습니다:
assertTrue(member == findMember); // false
이 문제를 해결하려면 비즈니스 키(예: ID)를 기반으로 equals()
메서드를 적절히 구현해 엔티티를 비교해야 합니다.
3. 프록시 문제와 해결
3.1 프록시와 동일성: 프록시로 조회된 엔티티도 동일성을 보장
JPA는 성능 최적화를 위해 실제 엔티티 대신 프록시 객체를 반환합니다. 프록시로 조회된 엔티티는 동일성을 보장하므로 원본 엔티티와 같은 방식으로 비교할 수 있습니다:
JPA는 성능 최적화를 위해 실제 엔티티 대신 프록시 객체를 반환합니다. 그러나 프록시로 조회된 엔티티도 동일성을 보장하므로 원본 엔티티와 같은 방식으로 비교할 수 있습니다:
Member refMember = em.getReference(Member.class, "member1");
Member findMember = em.find(Member.class, "member1");
assertTrue(refMember == findMember); // true
3.2 프록시와 타입 비교: 프록시 타입은 instanceof로 확인
프록시는 실제 엔티티의 자식 클래스이므로, 타입 비교 시 instanceof
를 사용하는 것이 안전합니다:
프록시는 실제 엔티티의 자식 클래스이므로, 타입 비교 시 instanceof
를 사용하는 것이 안전합니다:
assertTrue(refMember instanceof Member);
3.3 상속 관계와 프록시 문제: 프록시를 부모 타입으로 조회할 때 문제 해결
상속 관계에서 프록시를 부모 타입으로 조회하면 예상치 못한 문제가 발생할 수 있습니다. 예를 들어, 부모 클래스 Item
과 이를 상속받는 Book
클래스가 있을 때, Item
타입으로 프록시를 조회하면 구체 클래스인 Book
의 메서드나 필드에 접근할 수 없습니다:
Item proxyItem = em.getReference(Item.class, bookId);
proxyItem.getAuthor(); // 컴파일 오류 또는 런타임 오류 발생
이 문제를 해결하려면 다음과 같은 방법을 고려해야 합니다:
- JPQL을 사용해 자식 엔티티를 명시적으로 조회이렇게 하면 정확히 자식 엔티티 타입을 조회할 수 있습니다.
Book book = em.createQuery("SELECT b FROM Book b WHERE b.id = :id", Book.class)
.setParameter("id", bookId)
.getSingleResult();
- 프록시 초기화
Hibernate에서는 프록시를 초기화하여 실제 엔티티로 변환할 수 있습니다:
Item proxyItem = em.getReference(Item.class, bookId);
if (proxyItem instanceof HibernateProxy) {
proxyItem = ((HibernateProxy) proxyItem)
.getHibernateLazyInitializer()
.getImplementation();
}
- 인터페이스 설계를 통해 다형성을 활용
공통 기능을 정의한 인터페이스를 설계하고 이를 구현함으로써 구체 클래스에 의존하지 않고도 원하는 동작을 수행할 수 있습니다.
public interface Item {
String getDetails();
}
public class Book implements Item {
@Override
public String getDetails() {
return "Author: " + this.author;
}
}
- 비지터 패턴 적용으로 코드의 유연성을 높이기
비지터 패턴을 사용하면 객체의 구체적인 타입에 따라 다르게 동작하도록 설계할 수 있습니다.
public interface ItemVisitor {
void visit(Book book);
}
public class ItemPrinter implements ItemVisitor {
@Override
public void visit(Book book) {
System.out.println("Book Author: " + book.getAuthor());
}
}
4. 성능 최적화
4.1 N+1 문제
4.1.1 N+1 문제란?
즉시 로딩(EAGER)이나 지연 로딩(LAZY) 설정으로 인해 연관된 데이터를 여러 번 추가 조회하는 문제가 발생합니다. 예를 들어, 회원과 주문 데이터 관계에서 모든 회원의 주문 데이터를 조회하려고 하면:
- 회원 데이터 조회
- 각 회원의 주문 데이터를 개별적으로 조회 (회원 수만큼 반복)
4.1.2 해결 방법
페치 조인 사용:
List<Member> members = em.createQuery( "SELECT m FROM Member m JOIN FETCH m.orders", Member.class ).getResultList();
// SELECT m.id, m.name, o.id, o.order_date
// FROM members m
// INNER JOIN orders o ON m.id = o.member_id;
@BatchSize
설정
// SELECT o.id, o.order_date FROM orders o WHERE o.member_id IN (?, ?, ?, ...); -- 최대 100개의 ID
@BatchSize(size = 100)
private List<Order> orders;
@Fetch(FetchMode.SUBSELECT)
활용
// SELECT o.id, o.order_date FROM orders o
// WHERE o.member_id IN ( SELECT m.id FROM members m );
@Fetch(FetchMode.SUBSELECT)
private List<Order> orders;
4.2 읽기 전용 쿼리 최적화
읽기 전용 데이터를 처리할 때는 다음 방법으로 메모리 사용량을 줄일 수 있습니다:
스칼라 타입 조회:
List<Object[]> results = em.createQuery( "SELECT o.id, o.name FROM Order o" ).getResultList();
읽기 전용 힌트 사용:
query.setHint("org.hibernate.readOnly", true);
읽기 전용 트랜잭션 사용:
@Transactional(readOnly = true)
트랜잭션 외부 조회:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
4.3 배치 처리
많은 데이터를 한 번에 처리할 때는 메모리 부족을 방지하기 위해 주기적으로 플러시(flush
)와 초기화(clear
)를 수행해야 합니다:
for (int i = 0; i < totalEntities; i += batchSize) {
List<Product> products = em.createQuery("SELECT p FROM Product p")
.setFirstResult(i)
.setMaxResults(batchSize)
.getResultList();
for (Product product : products) {
product.setPrice(product.getPrice() + 100);
}
em.flush();
em.clear();
}
4.4 SQL 힌트 사용
SQL 힌트를 사용하려면 하이버네이트 세션을 직접 이용해야 합니다. SQL 힌트는 데이터베이스에서 쿼리의 실행 계획을 제어하기 위해 제공되는 기능으로, 특정 상황에서 성능을 최적화하거나 원하는 방식으로 데이터를 조회하도록 설정할 수 있습니다. 예를 들어, 힌트를 사용해 특정 인덱스를 강제로 사용하거나 병렬 쿼리 실행을 활성화할 수 있습니다.
SQL 힌트의 실제 적용 예시
- 쿼리 힌트 적용:
아래는 특정 인덱스를 강제로 사용하도록 SQL 힌트를 설정하는 예입니다.여기서/*+ INDEX(tbl_name idx_name) */
는 데이터베이스가idx_name
인덱스를 사용하도록 지시합니다.
SELECT /*+ INDEX(tbl_name idx_name) */ col1, col2 FROM tbl_name WHERE col3 = 'value';
- 병렬 쿼리 실행:
대량 데이터 조회 시 성능을 높이기 위해 병렬 처리를 요청할 수 있습니다.이 힌트는 쿼리가 병렬로 실행되도록 설정하며, 병렬 실행 스레드 수를4
로 지정합니다.
SELECT /*+ PARALLEL(4) */ col1, col2 FROM tbl_name;
- 하이버네이트와의 통합:
하이버네이트를 통해 SQL 힌트를 적용하려면 다음과 같이addQueryHint
메서드를 사용할 수 있습니다:
Session session = em.unwrap(Session.class);
List<Member> list = session.createQuery("SELECT m FROM Member m") .addQueryHint("FULL(Member)") .list();
이러한 힌트를 적절히 사용하면 쿼리 성능을 높이고 데이터베이스 리소스를 효율적으로 활용할 수 있습니다.
4.5 쓰기 지연과 성능 최적화
JPA의 쓰기 지연 기능은 데이터베이스 락을 최소화하면서 성능을 높입니다. hibernate.jdbc.batch_size
를 설정하면 배치 쿼리를 실행할 수 있습니다:
hibernate.jdbc.batch_size=50
이 설정은 여러 SQL 작업을 한 번의 플러시로 묶어서 처리합니다.
@BatchSize와의 차이점
hibernate.jdbc.batch_size
:- 전역 설정으로, 특정 엔티티에 국한되지 않고 모든 배치 작업에 적용됩니다.
- JDBC 레벨에서 쿼리를 모아서 실행합니다.
- 주로 대량 INSERT, UPDATE, DELETE 작업에서 성능을 최적화합니다.
@BatchSize
:- 특정 엔티티나 컬렉션 필드에 적용되는 설정입니다.
- SELECT 쿼리에서 IN 절을 활용하여 여러 데이터를 한 번에 가져옵니다.
- 연관된 엔티티의 조회 성능을 높이는 데 유용합니다.
5. 정리
JPA는 객체 지향 프로그래밍과 관계형 데이터베이스 간의 격차를 해소해주는 강력한 도구입니다. 그러나 이를 효율적으로 사용하려면 몇 가지 중요한 개념을 숙지해야 합니다.
- 예외 처리: JPA 예외는 스프링의 예외 추상화를 통해 관리할 수 있으며, 롤백 여부에 따라 적절히 처리해야 합니다.
- 엔티티 비교: 동일성과 동등성 비교의 차이를 이해하고, 비즈니스 키를 활용하여
equals()
를 올바르게 구현해야 합니다. - 프록시: 프록시는 성능 최적화를 위해 제공되며, 타입 비교와 상속 구조에서 발생하는 문제를 해결하는 방법을 알아야 합니다.
- 성능 최적화: N+1 문제를 방지하기 위해 페치 조인과 배치 설정을 활용하며, 쓰기 지연 및 SQL 힌트를 통해 쿼리 성능을 극대화할 수 있습니다.
'교육 > 책' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] JPA 트랜잭션 관리와 캐싱(16장) (0) | 2024.12.23 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] JPA 컬렉션과 부가 기능: 컬렉션, 컨버터, 리스너, 엔티티 그래프(14장) (0) | 2024.12.14 |
[자바 ORM 표준 JPA 프로그래밍] 일반적인 트랜잭션 관리와 뷰에서 필요한 데이터에 접근(13장) (0) | 2024.12.07 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA/성능 최적화 (12장-2) (0) | 2024.12.07 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA 기능/쿼리 메서드, 페이징과 정렬, 벌크 연산 (12장-1) (0) | 2024.12.07 |