[스프링] 예외처리, 성능 최적화
1. 예외 처리
JPA 예외들은 모두 RuntimeException 의 자식이다. 따라서, 언체크 예외
1) 트랜잭션 롤백을 표시하는 예외
이런 예외는 심각한 예외이므로, 복구해서는 안 된다
2) 트랜잭션 롤백을 표시하지 않는 예외
이런 예외는 심각한 예외가 아니므로, 개발자가 커밋할지 롤백할 지 정하면 된다.
// 예시
NoResultException // query.getSingleResult() 결과가 하나도 없을때 발생
NonUniqueResultException // query.getSingleResult() 결과가 둘이상일때 발생
LockTimeoutException // 비관적 락 시간 초과
QueryTimeoutException // 쿼리 실행 시간 초과
2-1) 트랜잭션 롤백시 주의 사항.
롤백하면 DB는 복구가 되지만, 객체는 수정된 상태로 영속성 컨텍스트에 남아있다.
그러므로, EntityManager.clear() 를 호출해서 영속성 컨텍스트를 초기화한 다음에 사용해야 한다.
스프링은 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료되므로 문제가 되지 않지만, 문제는 OSIV처럼 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해서 여러 태랜잭션이 하나의 영속성 컨택스트를 사용할 때 발생한다.
-> 스프링은 영속성 컨텍스트 범위를 트랜잭션 범위보다 넓게 설정하면 트랜잭션 롤백시 영속성 컨텍스트를 초기화( EntityManager.clear() )해서 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다.
2. 엔티티 비교
1차 캐시는 영속성 컨텍스트와 생명주기를 같이 한다.
같은 트랜재션 범위 -> 같은 영속성 컨텍스트 사용 -> 동일성(==), 동등성(equals() ), 데이터베이스 동등성(식별자가 같다) 보장
* 테스트, 서비스 계층 모두 @Transactional 이 있다면, 기본 전략은 먼저 시작된 트랜잭션 있으면 이어서하고 없으면 새로 시작하는 것인데, 전략을 변경하고 싶다면, propagation 속성 사용하기
* 테스트에서 @Transactional 사용하면, 테스트 끝나면 트랜 잭션 커밋안하고 강제로 롤백 ( 플러시 시점 어떤 sql이 실행됬는지 체크할 수 없다. 하고 싶으면 테스트 마지막에 em.flush() 날려보기 )
엔티티를 비교할 때는 동등성 비교 권장
결론, 같은 영속성 컨텍스트(동일 트랜잭션)의 관리를 받는 영속상태 엔티티는 동일성 비교, 그 외는 비즈니스 키를 사용하는 동등성 비교를!
3. 프록시 심화
프록시 조회후 -> 영속성 컨텍스트의 원본 영속 엔티티 조회하면 동일성 보장
원본 영속 엔티티 조회 -> 프록시 조회 : 동일성은 보장하지만, 이미 디비 조회해서 프록시를 반환할 이유가 없어서 원본을 반환함.
1) 프록시 타입 비교
1) 프록시와 엔티티 타입 비교
프록시에는 프록시라는 의미의 _$$_jvsteXXX 가 붙는다.
프록시와 엔티티를 비교하는 것은 부모 클래스와 자식 클래스의 비교와 같으므로 == 이 아니라 instanceof 를 사용하기
2) 프록시 동등성 비교
equals()를 오버라이딩하고 비교하면 되는데, 비교 대상이 원본 엔티티라면 상관이 없는데, 프록시면 문제 발생한다.
프록시와 equals() 비교시 주의할 점
1. 프록시는 원본을 상속 받은 자식타입이므로 프록시의 타입을 비교할 때는 ==이 아닌 instanceof 사용하기
2. 프록시는 실제 데이터를 가지고 있지 않기에, 멤버 변수에 직접 접근하면 아무 값도 조회할 수 없다 (null 반환) -> 프록시의 데이터를 조회할 때에는 Getter(접근자 메소드) 사용해야한다
3) 상속 관계와 프록시
-> 다형성을 다룰때 발생할 수 있다.
상속 관계에서 나타나는 프록시 문제
1. instanceof 사용 불가능하다
2. 하위 타입으로 다운 캐스팅을 할 수 없다.
상속 관계에서 나타나는 프록시 문제 해결 방법
1. JPQL로 대상 직접 조회
2. 프록시 벗기기
3. 기능을 위한 별도의 인터페이스 제공
4. 비지터 패턴 사용
1. JPQL 로 대상 직접 조회
-> 다형성 사용 불가능
2. 프록시 벗기기
-> 하이버 네이트가 제공하는 프록시에서 원본 엔티티 가져오기 BUT, 동일성 비교 실패 ( 이 방법은 원본 엔티티가 꼭 필요한 곳에서 잠깐 사용하고, 다른 곳에서 사용하지 않도록 해야한다 )
public static <T> T unProxy(Object entity) {
if(entity instanceof HibernateProxy) {
entity = ((HibernateProxy) entity)
.getHibernateLazyInitializer()
.getImplementtation();
}
return (T) entity;
}
3. 기능을 위한 별도의 인터페이스 제공
-> 다형성에 좋음
비지터 패턴
정리
1. 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근 가능
2. instanceof 와 타입캐스팅 없이 코드 구현 가능
3. 알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작 추가 가능
단점
1. 복잡, 더블 디스패치 사용으로 이해하기 힘듬
2. 객체 구조 변경시 모든 Visitor 수정
4. 성능 최적화
1. N+1
N+1 : 처음 실행한 sql 의 결과 수만크 추가로 SQL 문 실행 하는 것
즉시 로딩은 N+1 을 야기하고, 지연 로딩도 연관된 컬렉션을 사용할때 발생한다
해결방법
1. fetch join
-> 일반적으로 많이 사용, DISTINCT 를 사용해서 중복도 제거하기
2. @BatchSize
-> size만큼 SQL의 IN 절 사용해서 조회
3. 하이버네이트 @Fetch
-> FetchMode를 SUBSELECT를 사용해서 해결
결론, 지연 로딩 사용하고 성능 최적화 필요한 부분은 패치조인 사용
2. 읽기 전용 쿼리의 성능 최적화
수정할 일 없고 한번만 읽을거면 변경 감지를 사용하지 않는 등으로 메모리 낭비를 막을 수 있다
1. 스칼라 타입으로 조회
2. 읽기 전용 쿼리 힌트 사용
3. 읽기 전용 트랜잭션 사용
4. 트랜잭션 밖에서 읽기
메모리 최적화 -> 스칼라 타입으로 조회 or 읽기 전용 쿼리 힌트 사용
플러시 호출을 막아 속도 최적화 -> 읽기 전용 트랜잭션 사용 or 트랜잭션 밖에서 읽기
결론, 읽기 전용 쿼리 힌트 사용 + 읽기 전용 트랜잭션 사용 하기
@Transactional(readOnly = true) // 읽기 전용 트랜잭션
public List<DataEntity> findDatas() {
return em.createQuery("select d from DataEntity d",
DataEntity.class)
.setHint("org.hibernate.readOnly", true) // 읽기 전용 쿼리 힌트
.getResultList();
}
3. 배치 처리
-> 수백만 건의 데이터를 한번에 처리할때 사용 ( 메모리 부족 오류를 막음 )
1) 등록 배치
일정 단위마다 영속성 컨텍스트의 엔티티를 DB에 플러쉬 하고 영속성 컨텍스트를 초기화 해야한다
em.persist(..);
...
em.flush();
em.clear();
1) 수정 배치
페이징 처리와 커서를 사용해서 처리를 한다
페이지 단위마다 플러쉬하고 초기화 한다
4. 트랜잭션을 지원하는 쓰기 지연과 성능 최적화
쓰기 지연과 더티 체킹은 디비 테이블 로우에 락이 걸리는 시간을 최소화한다 .
-----(DB row에 락이 안걸림 X) ------> | TX commit ( commit() )------> | flush 를 통해 DB에 반영
예를들어, sql 직접 사용하면 update() 이후에, 비즈니스 로직이 있으면 다 끝나고 commit 되면서 락이 걸리는 시간이 길어지지만, 쓰기 지연을 통해서 락이 걸리는 시간이 최소화면서 더 많은 트랜잭션을 처리할 수 있게 된다.