Spring

금쪽 같은 트러블 슈팅 (최적화)

sehunbang 2024. 3. 13. 15:03

1. DB 조회 쿼리 최적화

1-1. Projection 을 통해 필요한 값만 가져오기

  • 전체 필드가 아닌 필요한 필드만 받아오는 방법
    • 적용 전 : SELCT * FROM User;
    • 적용 후 : SELECT username, profileImageUrl FROM User;
List<UserProfile> findById(Long userId);
public interface UserProfile {

  String getUsername();

  String getProfileImageUrl();

}

1-2. 하이버네이트 @BatchSize

하이버네이트에서 제공하는 어노테이션을 사용하면 연관된 엔티티를 조회할때 지정한 사이즈만큼만 조회한다. (SQL IN절 사용)
 
    @Entity
    public class Member {

        @Id @GeneratedValue
        @Column(name = "MEMBER_ID")
        private Long id;

        private String name;

        @Embedded
        private Address address;

        @org.hibernate.annotations.BatchSize(size = 5)
        @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
        private List<Order> orders = new ArrayList<Order>();
...

1-3. 읽기 전용 쿼리 힌트 적용

하이버네이트 전용 힌트인 readOnly를 적용한다. (영속성 컨텍스트 관리에서 제외됨)

TypedQuery<Order> query = em.createQuery("select o from Order o", Order.class);

query.setHint("org.hibernate.readOnly", true);

1-4. 읽기 전용 트랜젝션 사용

스프링 프레임워크를 사용하면 트랜잭션을 읽기 전용 모드로 설정할 수 있다.

@Transactional(readOnly = true)

트랜잭션을 읽기모드로 적용하면 강제로 플러시를 호출하지 않는 이상 플러시가 발생하지 않는다.

그러므로 자동으로 플러시 되면서 수행되는 무거운 로직들이 실행되지 않아 성능이 향상된다.

1-5. 트랜잭션 밖에서 읽기

말 그대로 트랜잭션 밖에서 엔티티를 조회하는 것임으로 커밋, 플러시 어떤 것도 적용되지 않는다.

그러므로 반드시 조회목적인 경우에만 사용해야 됨.

@Trnasactional(readOnly = true)
public proccessA() {
	Entity entity = search();
	entity.getA(); // A 가 LazyLoading 연관 객체일 경우
}

public proccessB() {
	Entity entity = search();
	entity.getB(); // B 가 LazyLoading 연관 객체가 아닐 경우 (EAGER , Column 필드)
}

@Trnasactional(readOnly = true)
public search() {
	//.. 조회하는 쿼리
	return entity;
}

결과적으로 읽기 전용 데이터를 조회할때 메모리를 최적화하려면 1) Projection 조회, 2) 하이버네이트가 제공하는 읽기 전용 쿼리 힌트

플러시 호출을 막아서 속도를 최적화하려면 1) 읽기 전용 트랜잭션 사용, 2) 트랜잭션 밖에서 읽기를 적용하면 된다.
두가지 방법을 합쳐서 사용하면 가장 효과가 크다.
@Transactional(readOnly = true)
@Override
public Page<Thread> search(ThreadSearchCond cond, Pageable pageable) {
    var query = query(thread, cond)
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize());

    query.orderBy(thread.mentions.any().createdAt.desc());
    query.setHint(AvailableHints.HINT_READ_ONLY, true);

    var threads = query.fetch();
    long totalSize = countQuery(cond).fetch().get(0);

    threads.stream()
        .map(Thread::getComments)
        .forEach(comments -> comments
            .forEach(comment -> Hibernate.initialize(comment.getEmotions())));

    return PageableExecutionUtils.getPage(threads, pageable, () -> totalSize);
}

 

출처 : https://jinjinyang.tistory.com/42

 

JPA 성능 최적화

N+1 문제 JPA 성능상 가장 주의해야 되는 문제이다. 먼저, N +1 문제는 지연로딩, 즉시로딩 모든 경우에 발생할 수 있다. 1) 즉시로딩 N+1문제 즉시로딩의 경우 엔티티 매니저를 통해 조회할 경우 즉

jinjinyang.tistory.com

6. 하이버네이트 @Fetch(FetchMode.SUBSELECT) 주의하기!

FetchMode.SUBSELECT 을 사용할 경우 연관된 데이터를 서브 쿼리를 통해 N + 1문제를 해결할 수 있다.

하지만 부모 쿼리 row 개수만큼 실행되기 때문에 성능이 최악이다. (즉, 부모 쿼리 ROW가 몇개 안될때만 사용해야한다.)

select *
from orders
where member_id in (
										select id
										from member
										); // orders Row 갯수만큼 수행됨

하지만!  서브셀렉트는 매우 느립니다.

출처 : https://happy-coding-day.tistory.com/entry/FetchFetchModeSUBSELECT-%EA%B3%BC-IN-subquery-%EB%8A%94-%EC%99%9C-%EB%8A%90%EB%A6%B4%EA%B9%8C

 

@Fetch(FetchMode.SUBSELECT) 과 IN subquery 는 왜 느릴까?

들어가기 JPA 를 활용하다가 문제를 하나 맞딱뜨렸습니다. 퇴근하기 10분전, 동료분의 호출로 위 그림처럼 시간이 오래걸리는 경우가 발생했습니다. 처음 보는 코드에서 어디서 발생했는지 알 수

happy-coding-day.tistory.com