본문 바로가기
새로운 학습

[Hibnernate] Query Cache Plan

by 나는후니 2022. 12. 24.

흔히 JPA를 사용하다보면 ~~in 쿼리를 종종 사용하게 된다.
만약 in 쿼리를 많이 사용한다면 하이버네이트의 옵션 중 query.in_clause_parameter_padding 을 꼭 true로 설정해두어야 한다.
그 이유를 하이버네이트의 QueryCachePlan과 함께 학습해보았다.

하이버네이트는 최적화를 위해 파라미터와 응답값을 캐시한다.

하이버네이트 5.2.18 버전부터 JPQL을 실행할 때 쿼리 컴파일 시간을 줄이기 위해 QueryCachePlan을 제공하고 있다. 또한 ParameterMetadata에는 native query의 namedParameter와 return type을 캐싱한다. 따라서 쿼리가 실행될 때 hibernate은 먼저 parameter와 resultType에 대해 plan cache를 확인하고 존재하면 해당 값을 사용, 존재하지 않으면 새로운 쿼리에 대해 LIRS 알고리즘이 적용된 BoundedConcurrentHashMap에 파라미터와 resultType을 포함한 쿼리를 캐시해둔다. QueryCachePlan은 default로 2048MB 만큼 저장 가능하고, ParameterMetadata는 128MB까지 가능하다.

그럼 왜 padding 옵션을 켜야하는지?

하이버네이트가 query를 캐싱하는 기준은 타입이다. 응답같은 경우에는 한 쿼리당 하나의 타입을 갖고 있기 때문에 추가적으로 캐시하지 않고 계속 가져다 쓴다. 하지만 in 쿼리에서 넘겨주는 파라미터의 개수가 달라진다면 캐시에 파라미터 개수 + 타입이 처음이라면 새로 쿼리를 캐시하게 된다.

아래 로그를 보면 쿼리에 대한 응답은 param이 한개일 때 것을 동일하게 쓰지만 요청은 파라미터 개수가 달라졌기 때문에 캐시 히트하지 못하고 새로 캐시한다.

2022-12-24 13:38:47.220 TRACE 73621 --- [io-8080-exec-10] o.h.engine.query.spi.QueryPlanCache      : Located HQL query plan in cache (select generatedAlias0 from User as generatedAlias0 where generatedAlias0.id in (:param0))
2022-12-24 13:38:47.221 TRACE 73621 --- [io-8080-exec-10] o.h.engine.query.spi.QueryPlanCache      : Unable to locate HQL query plan in cache; generating (select generatedAlias0 from User as generatedAlias0 where generatedAlias0.id in (:param0_0, :param0_1))

이렇게 in 쿼리의 개수가 매번 달라진다면 어떻게 될까?

각기 다른 in 개수를 가진 쿼리를 456회 날려보고 heapdump를 떠보았다.

org.hibernate.engine.query.spi.HQLQueryPlan    456 (0%)    18,240 B (0%)    n/a
org.hibernate.engine.query.spi.QueryPlanCache$HQLQueryPlanKey    456 (0%)    14,592 B (0%)    n/a

캐시만으로 꽤나 많은 공간을 차지하게 되고, 이 캐시전략이 그대로 실 운영상황에 적용된다면 heap 메모리를 굉장히 많이 점유하여 오래도록 old gen에 남아 메모리 릭으로 인한 시스템의 성능을 저하를 야기할 수 있다.

이런 문제를 해결하기 위해 hibernate에서는 in_clause_parameter_padding 이라는 옵션을 제공한다.

in_clause_parameter_padding

이 옵션은 말그대로 in clause에 있는 파라미터를 패딩한다. 패딩 전략은 2의 제곱수로 진행된다.

/api/samples/user?userIds=1,2,3 요청을 먼저 보내보자.

// 1, 2, 3 -> 4 개의 파라미터로 캐싱
2022-12-24 14:04:20.902 TRACE 9745 --- [nio-8080-exec-5] o.h.engine.query.spi.QueryPlanCache      : Located HQL query plan in cache (select generatedAlias0 from User as generatedAlias0 where generatedAlias0.id in (:param0))
2022-12-24 14:04:20.903 TRACE 9745 --- [nio-8080-exec-5] o.h.engine.query.spi.QueryPlanCache      : Unable to locate HQL query plan in cache; generating (select generatedAlias0 from User as generatedAlias0 where generatedAlias0.id in (:param0_0, :param0_1, :param0_2, :param0_3))
Hibernate: 
    select
        user0_.id as id1_2_,
        user0_.name as name2_2_,
        user0_.user_type as user_typ3_2_ 
    from
        users user0_ 
    where
        user0_.id in (
            ? , ? , ? , ?
        )
2022-12-24 14:04:20.912 TRACE 9745 --- [nio-8080-exec-5] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-12-24 14:04:20.913 TRACE 9745 --- [nio-8080-exec-5] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [2]
2022-12-24 14:04:20.913 TRACE 9745 --- [nio-8080-exec-5] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [3]

// 3을 한 번 더 넣어 네개로 채움
2022-12-24 14:04:20.913 TRACE 9745 --- [nio-8080-exec-5] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [3]

parameter 3개로 요청 보냈는데 마지막 파라미터를 하나 더 넣어 4개로 패딩하여 캐시한다.

다음으로 /api/samples/user?userIds=1,2,3,4 요청을 보내보면 캐시 히트 하는 것을 볼 수 있다.

2022-12-24 14:04:29.533 TRACE 9745 --- [nio-8080-exec-6] o.h.engine.query.spi.QueryPlanCache      : Located HQL query plan in cache (select generatedAlias0 from User as generatedAlias0 where generatedAlias0.id in (:param0))
2022-12-24 14:04:29.534 TRACE 9745 --- [nio-8080-exec-6] o.h.engine.query.spi.QueryPlanCache      : Located HQL query plan in cache (select generatedAlias0 from User as generatedAlias0 where generatedAlias0.id in (:param0_0, :param0_1, :param0_2, :param0_3))
Hibernate: 
    select
        user0_.id as id1_2_,
        user0_.name as name2_2_,
        user0_.user_type as user_typ3_2_ 
    from
        users user0_ 
    where
        user0_.id in (
            ? , ? , ? , ?
        )
2022-12-24 14:04:29.538 TRACE 9745 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-12-24 14:04:29.538 TRACE 9745 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [2]
2022-12-24 14:04:29.538 TRACE 9745 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [3]
2022-12-24 14:04:29.538 TRACE 9745 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [4]

간단히 설명하자면 2의 제곱수만큼 마지막 파라미터를 패딩하여 쿼리 캐시에 도움을 주고 캐시로 인한 메모리 누수를 막는 방법이다. 하지만 주의할 점이 있다면 in 파라미터의 개수가 많아진다면 인덱스를 타지 않고 full table scan이 이뤄질 수 있기 때문에 상황에 맞게 id를 chunk하여 쿼리를 사용해야한다.