인프런 커뮤니티 질문&답변

말랑한참깨님의 프로필 이미지
말랑한참깨

작성한 질문수

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

주문 조회 V3.1: 엔티티를 DTO로 변환 - 페이징과 한계 돌파

19:04 초 where in 쿼리 대신 where array_conatins 쿼리가 나왔습니다. (스프링 부트 3.1)

해결된 질문

작성

·

1.6K

·

수정됨

0

[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (예)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)

[질문 내용]
현재 스프링 부트 3.1 로 강의를 따라가고 있습니다.

default_batch_fetch_size:10 을 설정해서 나가는 쿼리가 줄어드는 것을 확인했습니다만, 쿼리가 두 개가 아니고 총 세 개가 나왔습니다.

023-06-13T23:24:02.666+09:00 DEBUG 68750 --- [nio-8080-exec-5] org.hibernate.SQL                        : 
    select
        o1_0.order_id,
        d1_0.delivery_id,
        d1_0.city,
        d1_0.street,
        d1_0.zipcode,
        d1_0.status,
        m1_0.memeber_id,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.name,
        o1_0.order_date,
        o1_0.status 
    from
        orders o1_0 
    join
        member m1_0 
            on m1_0.memeber_id=o1_0.member_id 
    join
        delivery d1_0 
            on d1_0.delivery_id=o1_0.delivery_id offset ? rows fetch first ? rows only
2023-06-13T23:24:02.676+09:00  INFO 68750 --- [nio-8080-exec-5] p6spy                                    : #1686666242676 | took 2ms | statement | connection 8| url jdbc:h2:tcp://localhost/~/springbootjpa
select o1_0.order_id,d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status,m1_0.memeber_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name,o1_0.order_date,o1_0.status from orders o1_0 join member m1_0 on m1_0.memeber_id=o1_0.member_id join delivery d1_0 on d1_0.delivery_id=o1_0.delivery_id offset ? rows fetch first ? rows only
select o1_0.order_id,d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status,m1_0.memeber_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name,o1_0.order_date,o1_0.status from orders o1_0 join member m1_0 on m1_0.memeber_id=o1_0.member_id join delivery d1_0 on d1_0.delivery_id=o1_0.delivery_id offset 0 rows fetch first 100 rows only;
2023-06-13T23:24:02.681+09:00 DEBUG 68750 --- [nio-8080-exec-5] org.hibernate.SQL                        : 
    select
        o1_0.order_id,
        o1_0.order_item_id,
        o1_0.count,
        o1_0.item_id,
        o1_0.order_price 
    from
        order_item o1_0 
    where
        array_contains(?,o1_0.order_id)
2023-06-13T23:24:02.689+09:00  INFO 68750 --- [nio-8080-exec-5] p6spy                                    : #1686666242689 | took 0ms | statement | connection 8| url jdbc:h2:tcp://localhost/~/springbootjpa
select o1_0.order_id,o1_0.order_item_id,o1_0.count,o1_0.item_id,o1_0.order_price from order_item o1_0 where array_contains(?,o1_0.order_id)
select o1_0.order_id,o1_0.order_item_id,o1_0.count,o1_0.item_id,o1_0.order_price from order_item o1_0 where array_contains('ar2: ARRAY [CAST(1 AS BIGINT), CAST(2 AS BIGINT), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL]',o1_0.order_id);
2023-06-13T23:24:02.690+09:00 DEBUG 68750 --- [nio-8080-exec-5] org.hibernate.SQL                        : 
    select
        i1_0.item_id,
        i1_0.dtype,
        i1_0.category_item_id,
        i1_0.name,
        i1_0.price,
        i1_0.stock_quantity,
        i1_0.author,
        i1_0.isbn,
        i1_0.artist,
        i1_0.etc 
    from
        item i1_0 
    where
        array_contains(?,i1_0.item_id)
2023-06-13T23:24:02.691+09:00  INFO 68750 --- [nio-8080-exec-5] p6spy                                    : #1686666242691 | took 0ms | statement | connection 8| url jdbc:h2:tcp://localhost/~/springbootjpa
select i1_0.item_id,i1_0.dtype,i1_0.category_item_id,i1_0.name,i1_0.price,i1_0.stock_quantity,i1_0.author,i1_0.isbn,i1_0.artist,i1_0.etc from item i1_0 where array_contains(?,i1_0.item_id)
select i1_0.item_id,i1_0.dtype,i1_0.category_item_id,i1_0.name,i1_0.price,i1_0.stock_quantity,i1_0.author,i1_0.isbn,i1_0.artist,i1_0.etc from item i1_0 where array_contains('ar3: ARRAY [CAST(1 AS BIGINT), CAST(2 AS BIGINT), CAST(3 AS BIGINT), CAST(4 AS BIGINT), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL]',i1_0.item_id);

수강 중에 제가 따라 작성한 코드를 스프링부트 2.4.1에 그대로 옮겨보니 강의 내용대로 where in 쿼리가 나오는 것을 확인했습니다.

스프링부트 버전 차이에서 나오는 쿼리가 달라진 것 같은데, 제 생각이 맞을까요?

 


답변 확인 : 

빠르게 답변해주셔서 감사합니다!!

덕분에 array_contains 로 최적화된 이유를 쉽게 이해할 수 있었습니다. (Hibernate에서 최적화를 했었었군요!)


아 그리고 강의 내용에서 쿼리 세 개 나온다는 것을 확인했습니다(잘못 본 것에 대해 질문글에서 수정을 해놨어야 했는데 빼놓지 않았었네요 ㅠㅠ)

다시 한번 상세한 답변 정말 감사드립니다.

답변 1

6

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 공부하는 거 아님^^

덕분에 변경 사항을 빨리 찾았네요. 어서 메뉴얼에도 업데이트 해야겠습니다.

스프링 부트 3.1 부터는 하이버네이트 6.2를 사용하는데요.

하이버네이트 6.2 부터는 where in 대신에 array_contains를 사용합니다.

where in 사용 문법

where item.item_id in(?,?,?,?)

array_contains 사용 문법

where array_contains(?,item.item_id)

침거러 where in에서 array_contains를 사용하도록 변경해도 결과는 완전히 동일합니다. 그런데 이렇게 변경하는 이유는 성능 최적화 때문입니다.

select ... where item.item_id in(?)

SQL을 실행할 때 데이터베이스는 SQL 구문을 이해하기 위해 SQL을 파싱하고 분석하는 등 여러가지 복잡한 일을 처리해야 합니다. 그래서 성능을 최적화하기 위해 이미 실행된 SQL 구문은 파싱된 결과를 내부에 캐싱하고 있습니다.

이렇게 해두면 다음에 같은 모양의 SQL이 실행되어도 이미 파싱된 결과를 그대로 사용해서 성능을 최적화 할 수 있습니다.

참고로 여기서 말하는 캐싱은 SQL 구문 자체를 캐싱한다는 뜻이지 SQL의 실행 결과를 캐싱한다는 뜻이 아닙니다.

SQL 구문 차제를 캐싱하기 때문에 여기서 ?에 바인딩 되는 데이터는 변경되어도 캐싱된 SQL 결과를 그대로 사용할 수 있습니다.

그런데 where in 쿼리는 동적으로 데이터가 변하는 것을 넘어서 SQL 구문 자체가 변해버리는 문제가 발생합니다.

다음 예시는 in에 들어가는 데이터 숫자에 따라서 총 3개의 SQL구문이 생성됩니다.

where item.item_id in(?)

where item.item_id in(?,?)

where item.item_id in(?,?,?,?)

SQL 입장에서는 ?로 바인딩 되는 숫자 자체가 다르기 때문에 완전히 다른 SQL입니다. 따라서 총 3개의 SQL 구문이 만들어지고, 캐싱도 3개를 따로 해야 합니다. 이렇게 되면 성능 관점에서 좋지 않습니다.

array_contains를 사용하면 이런 문제를 깔끔하게 해결할 수 있습니다.

이 문법은 결과적으로 where in과 동일합니다. array_contains은 왼쪽에 배열을 넣는데, 배열에 들어있는 숫자가 오른쪽(item_id)에 있다면 참이 됩니다.

예시) 다음 둘은 같다.

select ... where array_contains([1,2,3],item.item_id)

select ... where item.item_id in(1,2,3)

이 문법은 ?에 바인딩 되는 것이 딱1개 입니다. 배열1개가 들어가는 것이지요.

select ... where array_contains(?,item.item_id)

따라서 배열에 들어가는 데이터가 늘어도 SQL 구문 자체가 변하지 않습니다. ?에는 배열 하나만 들어가면 되니까요.

이런 방법을 사용하면 앞서 이야기한 동적으로 늘어나는 SQL 구문을 걱정하지 않아도 됩니다.

결과적으로 데이터가 동적으로 늘어나도 같은 SQL 구문을 그대로 사용해서 성능을 최적화 할 수 있습니다.

참고로 array_contains에서 default_batch_fetch_size에 맞추어 배열에 null 값을 추가하는데, 이 부분은 아마도 특정 데이터베이스에 따라서 배열의 데이터 숫자가 같아야 최적화가 되기 때문에 그런 것으로 추정됩니다.

 

그리고 실행되는 쿼리 숫자는 강의 내용에서도 지금과 동일하게 총 3번이 호출됩니다.

  • 직접 실행한 최초의 쿼리, orderItem, item

말랑한참깨님의 프로필 이미지
말랑한참깨

작성한 질문수

질문하기