https://www.youtube.com/watch?v=zMAX7g6rO_Y 

 

목차

  1. 워밍업
    1. QueryDSL을 implementation/extends 없이 JPAQueryFactory로 사용하기
    2. 동적쿼리를 Boolean Expression 으로 사용해보기 ( BooleanBuilder 대신 )
  2. 성능개선 - Select
    1. Querydsl의 exist 금지
  3. 성능 개선 - Update/Insert

 

1. 워밍엄

1-1.QueryDSL을 implementation/extends 없이 JPAQueryFactory로 사용하기

1. 다음과 같이 QuerydslConfig를 선언하여 jpaQueryFactory를 스프링 컨텍스트 내에 존재하도록 Bean으로 설정한다.

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(entityManager);
    }
}

2. 다음과 같이 사용가능하다.

@RequiredArgsConstructor
@Repository
public class SeminarQueryRepository {
    private final JPAQueryFactory queryFactory;

    public List<Seminar> findByName(String seminar_name){
        QSeminar qSeminar = QSeminar.seminar;

        return queryFactory
                .selectFrom(qSeminar)
                .where(qSeminar.seminar_name.eq(seminar_name)
                .and(qSeminar.del_dt.isNull()))
                .fetch();
    }
}

 

3.실행테스트

@DisplayName("testGetWithSeminar_name")
@Test
public void testGetWithSeminar_name123(){
    // given, when
    List<Seminar> seminar = seminarQueryRepository.findByName("SeminarDummyIndex5");

    // then
    assertNotNull(seminar.size());
    System.out.println(seminar.get(0));
}

결과값: query가 3번이나 실행됬다. 왜일까? BeforeEach가실행되고있었다.

Hibernate: 
    select
        s1_0.seminar_no,
        s1_0.del_dt,
        s1_0.inst_dt,
        s1_0.seminar_explanation,
        s1_0.seminar_name,
        s1_0.updt_dt 
    from
        seminar s1_0 
    where
        s1_0.seminar_name=? 
        and s1_0.del_dt is null
Seminar(seminar_no=27, seminar_name=SeminarDummyIndex5, seminar_explanation=SeminarDummyExplanationIndex5, del_dt=null)

 

 

1-2 동적쿼리를 Boolean Expression 으로 사용해보기 ( BooleanBuilder 대신 )

BooleanBuilder를 사용할시 동적쿼리가 길어질경우 기능성 문제는 없지만 어떤 쿼리인지 알기 어렵다.

 

1-2-1. 먼저 BooleanBuilder를 사용할경우의 예시이다.

//Boolean Builder
public List<Seminar> findSeminarByBooleanBuilder(String seminar_name, String seminar_explanation){
    BooleanBuilder builder = new BooleanBuilder();

    //어떤 쿼리인지 예상하기 어렵다.
    if(!StringUtils.isEmpty(seminar_name)){
        builder.and(QSeminar.seminar.seminar_name.eq(seminar_name));
    }
    if(!StringUtils.isEmpty(seminar_explanation)){
        builder.and(QSeminar.seminar.seminar_explanation.eq(seminar_explanation));
    }
    return queryFactory
            .selectFrom(QSeminar.seminar)
            .where(builder)
            .fetch();
}

위의 테스트코드를 작성해보자

@DisplayName("FindSeminarByBooleanBuilder Test")
@Test
public void testFindSeminarByBooleanBuilder(){
    // given // when
    List<Seminar> seminarList = seminarQueryRepository.findSeminarByBooleanBuilder("SeminarDummyIndex23", "");

    // then
    assertNotNull( seminarList.size());
    seminarList.stream().forEach(seminar -> System.out.println(seminar.toString()));
}

 

Hibernate: 
    select
        s1_0.seminar_no,
        s1_0.del_dt,
        s1_0.inst_dt,
        s1_0.seminar_explanation,
        s1_0.seminar_name,
        s1_0.updt_dt 
    from
        seminar s1_0 
    where
        s1_0.seminar_name=?
Seminar(seminar_no=45, seminar_name=SeminarDummyIndex23, seminar_explanation=SeminarDummyExplanationIndex23, del_dt=null)

 

 

이번에는 Boolean Expression으로 작성해보자.

이떄 주석에 달려있듯이 where절에 booleanExpression이 달려있기에 eqName과 eqSeminarExplanation이 둘다 null 일경우 조건절이 비어있게 되어 모든 내용을 다 불러오게 되어 많은 시간이 걸릴 수 있다. 그러므로 애초에 paging 을 처리하여 데이터를 어느정도 나눠서 가져오도록 하든가 하는 조건을 넣어준다.

//동적쿼리는 Boolean Expression : null 반환시 자등으로 조건절에서 제거된다. 모든 조건이 null이 발생하는 경우는 모든 경우를 다 불러오므로 많은 시간이 소요되므로 따로 조건을 넣어야한다./
public List<Seminar> findByDynamicQuery(String seminar_name, String seminar_explanation){
    return queryFactory
            .selectFrom(QSeminar.seminar)
            .where(eqName(seminar_name),
                    eqSeminarExplanation(seminar_explanation))
            .fetch();
}

private BooleanExpression eqName(String seminar_name){
    if(StringUtils.isEmpty(seminar_name)){
        return null;
    }
    return QSeminar.seminar.seminar_name.eq(seminar_name);
}

private BooleanExpression eqSeminarExplanation(String seminar_explanation){
    if(StringUtils.isEmpty(seminar_explanation)){
        return null;
    }
    return QSeminar.seminar.seminar_explanation.eq(seminar_explanation);
}

똑같이 테스트를 진행해보자.

@DisplayName("FindSeminarByBooleanExpression Test")
@Test
public void testFindSeminarByBooleanExpression(){
    // given // when
    List<Seminar> seminarList = seminarQueryRepository.findSeminarByBooleanExpression("SeminarDummyIndex23", "");

    // then
    assertNotNull( seminarList.size());
    seminarList.stream().forEach(seminar -> System.out.println(seminar.toString()));
}
Hibernate: 
    select
        s1_0.seminar_no,
        s1_0.del_dt,
        s1_0.inst_dt,
        s1_0.seminar_explanation,
        s1_0.seminar_name,
        s1_0.updt_dt 
    from
        seminar s1_0 
    where
        s1_0.seminar_name=?
Seminar(seminar_no=45, seminar_name=SeminarDummyIndex23, seminar_explanation=SeminarDummyExplanationIndex23, del_dt=null)

 

 

2. 성능개선

2-1.Querydsl의 exist 금지

먼저 SQL에서 exist메소드와 count(1) > 0 쿼리의 속도를 비교한다.

2500만건 기준

SQL.exist 를 사용한경우
select exists(
	select 1
    from ad_item_sum
    where created_date > '2020-01-01')
execution : 3s

SQL.count(1) > 0 를 사용한경우
select count(1)
from ad_item_sum
where created_date > '2020-01-01'
execution : 5s

원인 : 특정조건을 만족하는 row가 있냐 없냐를 판단할떄 exist는 첫번쨰 것이 발견되는 순간 종료한다.

count()는 끝까지 이동한다. (세야하니)

스캔대상이 앞에 있을수록 더 심한 성능차이가 발생한다.

 

 

querydsl의 sql의 exists를 사용하지 않고, eixsts 는 실제로 count() > 0 으로 실행된다.

아래 코드에서 exists가 fetchCount로 코딩되어있는 모습을 확인할 수 있다.

QuerydslJpaPredicateExecutor.class

@Override
public boolean exists(Predicate predicate){
	return createQuery(predicate).fetchCount() > 0;
}

querydsl의 JPA의 근간이 되는 JPQL이 from 없이는 쿼리를 생성할 수 없다.

즉 select exists를 하고 하위에 select 쿼리를 만드는것은 JPQL에서 지원하지 않는다.

return queryFactory.select(queryFactory)
		.selectOne()
        .from(book)
        .where(book.id.eq(bookId))
        .fetchAll(.exists())
        .fetchOne();

위의 코드에서 JPQL은 from 없이는 쿼리를 생성할 수 없다.

 

exists가 빠른 이유는 조건에 해당하는 row 1개만 찾으면 바로 쿼리를 종료하기 때문이다.

이를 직접구현하자.

가장 쉬운방법은, limit 1 로 조회를 제한한다. 조회결과가 없으면 null이라서 체크

@Transactional
public Boolean exist(String seminar_name){
    Integer fetchOne = queryFactory
            .selectOne()
            .from(QSeminar.seminar)
            .where(QSeminar.seminar.seminar_name.eq(seminar_name))
            .fetchFirst();
    //limit 1로 조회 제한
    //fetchFirst == limit(1).fetchOne() 과 같다.
    //조회결과가 없으면 null이라서 체크
    return fetchOne != null; //0보다 크다 아니다가아닌 조회결과가 없으면 null이 반환된다.
}

 

2500만건 기준

SQL.exist 를 사용한경우
select exists(
	select 1
    from ad_item_sum
    where created_date > '2020-01-01')
execution : 3s

Hibernate: 
    select
        1 
    from
        seminar s1_0 
    where
        s1_0.seminar_name=? limit ?



Querydsl.limit 1 을 사용한경우
select 1
from ad_item_sum
where created_date > '2020-01-01'
limit 1
execution : 3 s 

성능이 비슷하다.

querydsl의 eixsts를 사용할떄는 우회해서 limit 1로 만들어서 사용하는것이 좋다.

 

     2-2CrossJoin 회피

inner join에 on 사용한 방법

public List<Member_Seminar> crossJoin(){
    QSeminar qSeminar = QSeminar.seminar;
    QMember_Seminar qMember_seminar = QMember_Seminar.member_Seminar;
    return queryFactory
            .selectFrom(qMember_seminar)
            .innerJoin(qSeminar)
            .on(qMember_seminar.seminar.seminar_no.eq(qSeminar.seminar_no))
            .fetch();
}

Hibernate: 
    select
        m1_0.member_seminar_no,
        m1_0.del_dt,
        m1_0.inst_dt,
        m1_0.member_no,
        m1_0.seminar_no,
        m1_0.updt_dt 
    from
        member_seminar m1_0 
    join
        seminar s1_0 
            on m1_0.seminar_no=s1_0.seminar_no
@DisplayName("crossJoinTest")
@Test
@Transactional // To Maintain the Proxy Object Until the End of the Test
public void testCrossJoin(){
    List<Member_Seminar> memberSeminarList = seminarQueryRepository.crossJoin();

    memberSeminarList.stream().forEach(memberSeminar -> System.out.println(memberSeminar.getMember()+" "+memberSeminar.getSeminar()));

}

inner join에 where 사용한 방법

public List<Member_Seminar> crossJoin(){
    QSeminar qSeminar = QSeminar.seminar;
    QMember_Seminar qMember_seminar = QMember_Seminar.member_Seminar;
    return queryFactory
            .selectFrom(qMember_seminar)
            .innerJoin(qSeminar)
            .where(qMember_seminar.seminar.seminar_no.eq(qSeminar.seminar_no))
            .fetch();
}
 
 Hibernate:
select
    m1_0.member_seminar_no,
    m1_0.del_dt,
    m1_0.inst_dt,
    m1_0.member_no,
    m1_0.seminar_no,
    m1_0.updt_dt 
from
    member_seminar m1_0 
join
    seminar s1_0 
        on 1 
where
    m1_0.seminar_no=s1_0.seminar_no
@DisplayName("crossJoinTest")
@Test
@Transactional // To Maintain the Proxy Object Until the End of the Test
public void testCrossJoin(){
    List<Member_Seminar> memberSeminarList = seminarQueryRepository.crossJoin();

    memberSeminarList.stream().forEach(memberSeminar -> System.out.println(memberSeminar.getMember()+" "+memberSeminar.getSeminar()));

}

 

    2-3 Entity 보다는 DTO를 우선.

Entity를 활용할경우. 아래 4가지의 단점이있다. 각 4가지의 단점을 DTO를 사욤하으로써 어떻게 해결하는지 확인하자.

1. Hibernate 1차, 2차 캐시

2. 불필요한 컬럼 조회

3. OneToOne N+1 쿼리 등

4. 단순조회기능에서는 성능이슈요소가 많다.

 

Entity 조회는 아래의 경우에 좋다.

- 실시간으로 Entity 변경이 필요한경우

 

JPA와 Entity는 동일선상이 아닌, DTO를 쓰면 어떤장점이있는지 보자.

DTO 조회는 다음의 경우에 좋다.

- 고강도 성능갯너 or 대량의 데이터 조회가 필요한 경우

 

Entity를 활용해서 불필요한 column을 모두 조회하는경우

public List<Member_Seminar> getMemberSeminarWithEntityExample(long member_seminar_no, int pageNo){
    QMember_Seminar member_seminar = QMember_Seminar.member_Seminar;
    List<Member_Seminar> member_seminarList = queryFactory
            .selectFrom(member_seminar)
            .where(member_seminar.member_seminar_no.eq(member_seminar_no))
            .offset(pageNo)
            .limit(10)
            .fetch();
    return member_seminarList;
}

test를 활용해서 확인해보자.

@DisplayName("QueryDsl getMemberSeminarWithEntityExample Test")
@Test
public void testgetMemberSeminarWithEntityExample(){
    System.out.println(memberSeminarQuerydslRepository.getMemberSeminarWithEntityExample(2,0));
}

실행된 쿼리문, 불필요하게 모든 컬럼을 가져오고 있다.

Hibernate: 
    select
        m1_0.member_seminar_no,
        m1_0.del_dt,
        m1_0.inst_dt,
        m1_0.member_no,
        m1_0.seminar_no,
        m1_0.updt_dt 
    from
        member_seminar m1_0 
    where
        m1_0.member_seminar_no=? limit ?,?
[Member_Seminar(member_seminar_no=2, del_dt=null)]

 

DTO를 활용해서 불필요한 column을 조회하는경우로 개선

public List<Member_SeminarDTO> getMemberSeminarWithDTOExample(long member_seminar_no, int pageNo){
    QMember_Seminar member_seminar = QMember_Seminar.member_Seminar;
    List<Member_SeminarDTO> member_seminarDTOList = queryFactory
            .select(Projections.fields(Member_SeminarDTO.class,
                    member_seminar.member_seminar_no,
                    member_seminar.member.member_no
                    ))
            .from(member_seminar)
            .where(member_seminar.member_seminar_no.eq(member_seminar_no))
            .offset(pageNo)
            .limit(10)
            .fetch();
    return member_seminarDTOList;
}

테스트를 진행한다.

@DisplayName("QueryDsl getMemberSeminarWithDTOExample Test")
@Test
public void testgetMemberSeminarWithDTOExample(){
    System.out.println(memberSeminarQuerydslRepository.getMemberSeminarWithDTOExample(2,0));
}

쿼리가 훨씬 짧아졌다. 내가 원하는 내용만 가져온다.

Hibernate: 
    select
        m1_0.member_seminar_no,
        m1_0.member_no 
    from
        member_seminar m1_0 
    where
        m1_0.member_seminar_no=? limit ?,?
[Member_SeminarDTO(member_seminar_no=2, member_no=20, seminar_no=null, inst_dt=null, updt_dt=null, del_dt=null)]

 

    2-4  DTO사용하면서  'as' 표현식으로 조회컬럼 더 최소화하기

이 내용은 DTO를 사용했던것에 추가의 내용이다.

- 'as 표현식' 으로 DTO를 활용해서 불필요한 column을 조회하는경우

- as 표현식으로 대체할 수 있다.

public List<Member_SeminarDTO> getMemberSeminarWithDTOExample(long member_seminar_no, int pageNo){
    QMember_Seminar member_seminar = QMember_Seminar.member_Seminar;
    List<Member_SeminarDTO> member_seminarDTOList = queryFactory
            .select(Projections.fields(Member_SeminarDTO.class,
                    Expressions.asNumber(member_seminar_no).as("member_seminar_no"),
                    member_seminar.member.member_no
                    ))
            .from(member_seminar)
            .where(member_seminar.member_seminar_no.eq(member_seminar_no))
            .offset(pageNo)
            .limit(10)
            .fetch();
    return member_seminarDTOList;
}

테스트코드이다.

@DisplayName("QueryDsl getMemberSeminarWithDTOExample With AS Test")
@Test
public void testgetMemberSeminarWithDTOExample(){
    System.out.println(memberSeminarQuerydslRepository.getMemberSeminarWithDTOExample(2,0));
}

'as' 사용하여 member_seminar_no는 기존값에서 가져온다. 이를 통해 불필요한 컬럼조회를 없앤다.

 

Hibernate: 
select
    m1_0.member_no 
from
    member_seminar m1_0 
where
    m1_0.member_seminar_no=? limit ?,?
[Member_SeminarDTO(member_seminar_no=2, member_no=20, seminar_no=null, inst_dt=null, updt_dt=null, del_dt=null)]

     2-5 Select 컬럼에 Entity 자제

Member_Seminar와 연결될 Member를 조회하기 위해 Member_no를 사용하기 위해 MEmber Entity를 모두 호출.

Member_Seminar의 member Entity를 그대로 호출하고있다. 불필요한 엔티티의 모든 정보가 호출된다.

public List<Member_SeminarDTO> getMemberSeminarWithDTOANDSelectEntityExample(long member_seminar_no){
    QMember_Seminar member_seminar = QMember_Seminar.member_Seminar;
    List<Member_SeminarDTO> member_seminarDTOList = queryFactory
            .select(Projections.fields(Member_SeminarDTO.class,
                    member_seminar.member_seminar_no,
                    member_seminar.member))
            .from(member_seminar)
            .where(member_seminar.member_seminar_no.eq(member_seminar_no))
            .fetch();
    return member_seminarDTOList;
}

테스트를 진행한다.

@DisplayName("QueryDsl getMemberSeminarWithDTOANDSelectEntityExample With AS Test")
@Test
public void testgetMemberSeminarWithDTOANDSelectEntityExample(){
    System.out.println(memberSeminarQuerydslRepository.getMemberSeminarWithDTOANDSelectEntityExample(2));
}

쿼리결과. 역시 모든 member Entity를 모두 조회하여 불필요하다. Member의 모든 컬럼들이 조회된다.

Hibernate: 
    select
        m1_0.member_seminar_no,
        m2_0.member_no,
        m2_0.del_dt,
        m2_0.inst_dt,
        m2_0.member_from_social,
        m2_0.member_id,
        m2_0.member_nickname,
        m2_0.member_password,
        m2_0.updt_dt 
    from
        member_seminar m1_0 
    join
        member m2_0 
            on m2_0.member_no=m1_0.member_no 
    where
        m1_0.member_seminar_no=?
[Member_SeminarDTO(member_seminar_no=2, member_no=null, seminar_no=null, inst_dt=null, updt_dt=null, del_dt=null)]

 

내가 진행하는 프로젝트에는 OneToOn

추가로, Member와 @OneToOne 관계인 Shop이 매건마다 조회된다. 

즉, 내가 Member를 조회할떄마다 Shop이 모두 조회된다. 

(OneToOne은 Lazy Loading이 안된다. 즉, N+1이 무조건 발생)

만약 Shop에도 @OneToOne이 있다면,

100배, 1000배의 쿼리가 실행된다. Shop 호출하면서 Shop과 연관된 @OneTOOne도 N+1 이 발생하기떄문이다.

 

여기서 애초에 우리가 하려는것은 Member_Seminar와 Member의 연관관계를 맺기 위한 Member_no의 정보를 원한다.

Entity 간 연관관계를 맺으려면 반대  Entity가 있어야하는 것이 아니라, Member의 Member_no.

즉, 연관된 Entity의 save를 위해서는 반대편 Entity의 ID만 있으면된다. (Join Column에 들어갈 ID만 필요)

 

아래와 같이 수정한다.

public List<Member_SeminarDTO> getMemberSeminarWithDTOANDSelectEntityExample(long member_seminar_no){
    QMember_Seminar member_seminar = QMember_Seminar.member_Seminar;
    List<Member_SeminarDTO> member_seminarDTOList = queryFactory
            .select(Projections.fields(Member_SeminarDTO.class,
                    member_seminar.member_seminar_no,
                    member_seminar.member.member_no.as("member_no")))
            .from(member_seminar)
            .where(member_seminar.member_seminar_no.eq(member_seminar_no))
            .fetch();
    return member_seminarDTOList;
}

테스트진행

@DisplayName("QueryDsl getMemberSeminarWithDTOANDSelectEntityExample With AS Test")
@Test
public void testgetMemberSeminarWithDTOANDSelectEntityExample(){
    System.out.println(memberSeminarQuerydslRepository.getMemberSeminarWithDTOANDSelectEntityExample(2));
}

쿼리결과. Member_no정보만 가져와서 사용한다.

Hibernate: 
    select
        m1_0.member_seminar_no,
        m1_0.member_no 
    from
        member_seminar m1_0 
    where
        m1_0.member_seminar_no=?
[Member_SeminarDTO(member_seminar_no=2, member_no=20, seminar_no=null,

이떄 추가로, Member Entity 와 연관시키기 위해서, 다음과 같이 member_no로 constructor 생성자를 만든다.

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Member extends BaseEntity {

	... 

    public Member(long member_no) {
        this.member_no = member_no;
    }

}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Member_SeminarDTO {

    @Schema(description = "member_seminar_no")
    private Long member_seminar_no;

    @Schema(description = "member")
    private Long member_no;

    @Schema(description = "seminar")
    private Long seminar_no;

    @Schema(description = "inst_dt")
    private LocalDateTime inst_dt;

    @Schema(description = "updt_dt")
    private LocalDateTime updt_dt;

    @Schema(description = "del_dt")
    private LocalDateTime del_dt;

    public Member_Seminar dtoToEntity(){
        return Member_Seminar.builder()
                .member_seminar_no(member_seminar_no)
                .member(new Member(member_no))
                .build();
    }
}

테스트진행. DTO 형태를 Entity 로 변환시킨다. stream을 활용한다.

@DisplayName("QueryDsl getMemberSeminarWithDTOANDSelectEntityExample With AS Test")
@Test
public void testgetMemberSeminarWithDTOANDSelectEntityExample(){
    List<Member_Seminar> member_seminarList = memberSeminarQuerydslRepository.getMemberSeminarWithDTOANDSelectEntityExample(2).stream().map(memberSeminarDTO -> memberSeminarDTO.dtoToEntity()).collect(Collectors.toList());
    System.out.println(member_seminarList);
}

sql쿼리와 결과값 Member_Seminar Entity에 올바르게 member_no값이 들어왔다.

Hibernate: 
    select
        m1_0.member_seminar_no,
        m1_0.member_no 
    from
        member_seminar m1_0 
    where
        m1_0.member_seminar_no=?
[Member_SeminarDTO(member_seminar_no=2, member_no=20, seminar_no=null, inst_dt=null, updt_dt=null, del_dt=null)]
Member_Seminar(member_seminar_no=2, del_dt=null)
20

 

불필요한 컬럼들을 사용하지 않으므로 데이터가 크고 컬럼개수가 많을수록 큰 차이가 날것이다.

영상에서는 2500만건 기준으로 13초 나오는것이 2초 로 줄었다.

 

Entity 조회경우에는 시간이 많이 걸리기에 실제로 필요한 컬럼들로 가져온다.

   2-6.또한 추가로, Select에 선언된 Entity의 컬럼자체가 distinct 대상이 된다. 

distinct가 있을경우 Member의 Column까지 포함되서 작동하기에 distinct table을 만들기 위한 공간을 사용하기에 성능이 많이 떨어진다.

distinct란 중복제외하여 데이터를 보여주는 작업을 의미한다.

 

     2-7 Group By 최적화

 

Querydsl group by 를 2-6 코드에서 가져와 추가해준다.

public List<Member_SeminarDTO> getMemberSeminarWithDTOANDSelectEntityExample(long member_seminar_no){
    QMember_Seminar member_seminar = QMember_Seminar.member_Seminar;
    List<Member_SeminarDTO> member_seminarDTOList = queryFactory
            .select(Projections.fields(Member_SeminarDTO.class,
                    member_seminar.member_seminar_no,
                    member_seminar.member.member_no.as("member_no")))
            .from(member_seminar)
            .where(member_seminar.member_seminar_no.eq(member_seminar_no))
            .groupBy(member_seminar.member_seminar_no)
            .fetch();
    System.out.println(member_seminarDTOList);
    return member_seminarDTOList;

}
@DisplayName("QueryDsl getMemberSeminarWithDTOANDSelectEntityExample Test")
@Test
public void testgetMemberSeminarWithDTOANDSelectEntityExample(){
//        System.out.println(memberSeminarQuerydslRepository.getMemberSeminarWithDTOANDSelectEntityExample(2));
    List<Member_Seminar> member_seminarList = memberSeminarQuerydslRepository.getMemberSeminarWithDTOANDSelectEntityExample(2).stream().map(memberSeminarDTO -> memberSeminarDTO.dtoToEntity()).collect(Collectors.toList());
    System.out.println(member_seminarList.get(0));
    System.out.println(member_seminarList.get(0).getMember().getMember_no());
}
Hibernate: 
    select
        m1_0.member_seminar_no,
        m1_0.member_no 
    from
        member_seminar m1_0 
    where
        m1_0.member_seminar_no=? 
    group by
        m1_0.member_seminar_no
[Member_SeminarDTO(member_seminar_no=2, member_no=20, seminar_no=null, inst_dt=null, updt_dt=null, del_dt=null)]
Member_Seminar(member_seminar_no=2, del_dt=null)

Mysql에서 Group By를 실행하면 FileSort가 필수로 발생한다. (Index가 아닌경우)

모든 Group by 가 index 를 타지 않을 수도 있기에. 수정이 필요하다.

( 내가 테스트해보았을때는 Index를 탔다. )

Mysql에서는 order by null을 사용하면 FileSort가 제거된다.

하지만 Querydsl에서는 order by null 문법을 지원하지 않는다.

직접 만들어서 사용한다.

 

1. OrderByNull.class 를 생성한다. 이는 Order by null asc 로 구현하기 위함이다.

public class OrderByNull extends OrderSpecifier {
    public static final OrderByNull DEFAULT = new OrderByNull();

    private OrderByNull(){
        super(Order.ASC, NullExpression.DEFAULT, Default);
    }
}

 

2. orderBy(OrderByNull.DEFAULT)를 추가하여 order by null asc 를 추가한다.

조건 클래스를 생성해서 적용한다.

public List<Member_SeminarDTO> getMemberSeminarWithDTOANDSelectEntityExample(long member_seminar_no){
    QMember_Seminar member_seminar = QMember_Seminar.member_Seminar;
    List<Member_SeminarDTO> member_seminarDTOList = queryFactory
            .select(Projections.fields(Member_SeminarDTO.class,
                    member_seminar.member_seminar_no,
                    member_seminar.member.member_no.as("member_no")))
            .from(member_seminar)
            .where(member_seminar.member_seminar_no.eq(member_seminar_no))
            .groupBy(member_seminar.member_seminar_no)
            .orderBy(OrderByNull.DEFAULT)
            .fetch();
    System.out.println(member_seminarDTOList);
    return member_seminarDTOList;

}

 

3. Test 단계

@DisplayName("QueryDsl getMemberSeminarWithDTOANDSelectEntityExample With AS Test")
@Test
public void testgetMemberSeminarWithDTOANDSelectEntityExample(){
    List<Member_Seminar> member_seminarList = memberSeminarQuerydslRepository.getMemberSeminarWithDTOANDSelectEntityExample(2).stream().map(memberSeminarDTO -> memberSeminarDTO.dtoToEntity()).collect(Collectors.toList());
    System.out.println(member_seminarList.get(0));
    System.out.println(member_seminarList.get(0).getMember().getMember_no());
}

 

4. query

Hibernate: 
    select
        m1_0.member_seminar_no,
        m1_0.member_no 
    from
        member_seminar m1_0 
    where
        m1_0.member_seminar_no=? 
    group by
        m1_0.member_seminar_no 
    order by
        null asc
[Member_SeminarDTO(member_seminar_no=2, member_no=20, seminar_no=null, inst_dt=null, updt_dt=null, del_dt=null)]
Member_Seminar(member_seminar_no=2, del_dt=null)
20

 

5. 단순 Group By와 group by order by null 의 성능차이는 2500만건 기준 47 초와 8초가 나왔다.

반드시 사용해야한다.

 

6. 정렬이 필요하더라도, 조회결과가 100건 이하라면, 애플리케이션에서 정렬한다.

- DB보다는 WAS의 자원이 더 저렴하다.

result.sort(comparingLong(PointCalculateAmount::getPointAmount));

-위의 코드는 PointCalculateAmount 객체를 요소로 갖는 리스트인 result를 PointCalculateAmount 객체의 pointAMount 필드를 기준으로 오름차순 정렬한다. 

- comparingLong은 Java 8부터 제공되는 정적 메서드로, 객체를 비교할 때 사용된다. 이 메서드는 비교할 필드나 속성을 기준으로 비교자(Comparator)를 생성한다.

- PointCalculateAmount::getPointAmount는 PointCalculateAmount 클래스의 getPointAmount 메서드에 대한 메서드 참조이다. 이 메서드 참조는 PointCalculateAmount 객체의 pointAmount 값을 반환하는 메서드를 가리킨다.

- 만약 내림차순으로 할경우에는 단순히 뒤집어서 내놓으면된다.

result.sort(comparingLong(PointCalculateAmount::getPointAmount).reversed());

- 단, 페이징일경우에는 order by null을 사용하지 못한다. (페이징이 아닌경우에만 사용하자)

 

 

 

     2-8 커버링 인덱스

커버링 인덱스는 쿼리를 출력시키는데 필요한 모든 컬럼을 갖고있는 인덱스이다 .

select / where /order by / group by

등에서 사용되는 모든 컬럼이 인덱스에 포함된 상태이다.

NoOffSet 방식과 더불어 페이징 조회성능을 향상 시키는 가장 보편적인 방법이다.

select *
	from Member m
join ( select member_id
			from member
        order by member_id
        limit 10000, 10) as temp
on temp.id = a.id;

 

 

하지만, JPQL은 from 절의 서부쿼리를 지원하지 않는다. 즉, JPQL을 지원하는 querydsl도 안된다.

Cluster Key(PK)를 커버링 인덱스로 빠르게 조회하고, 조회된 Key로 SELECT 컬럼들을 후속조회한다.

List<Long> ids = queryFactory
		.select(book.id)
        .from(book)
        .where(book.name.like(name + "%"))
        .orderBy(book.id.desc())
        .limit(pageSize)
        .offset(pageNo * pageSize)
        .fetch();

if(CollectionUtils.isEmpty(ids)){
	return new ArrayList<>();
}

return queryFactory
		.select(Projections.fields(BookPaginationDto.class,
        		book.id.as("bookId"),
                book.name,
                book.bookNo,
                book.bookType))
        .from(book)
        .where(book.id.in(ids))
        .orderBy(book.id.desc())
        .fetch();

- where order by limit 까지는 Covering Index가 들어간 쿼리로 빠르게 Id를 조회하고

최종적으로 나온 10개의 값을 통해 SELECT문을 통해 Query DSL로도 Covering Index를 사용할 수 있다.

 

1억 건 기준으로

- 기존 페이징이 26s이다.

- jdbc 커버링은 0.27s이다.

- querydsl 커버링은 0.58s이다.

 

 

3. 성능 개선 - Update/Insert

3-1. 일괄 Update 최적화

  • 객체 지향을 핑계로 성능을 버리진 않는지, 무분별한 DirtyChecking을 꼭 확인한다.

DirtyChecking로 Update하는경우

  • JPA를 쓰다보면 Dirty Checking을 많이 쓴다.
  • Transaction 내부에 있을떄 Entity를 조회해서 해당 Entity의 값을 바꾸면 자동으로 적용되는 것을 DirtyChecking이라 한다.
  • 만약 100건, 1000건 이 되더라도 그렇게 한다면 해당 Entity를 전부 조회해서 하는 경우가 있다.
DirtyChecking 예시
List<Student> students = queryFactory
		.selectFrom(student)
        .where(student.id.loe(studentId))
        .fetch();

for(Student student : students){
	student.updateName(name);
}

 

Querydsl로 한번에 update하는 경우

queryFactory.update(student)
		.where(student.id.loe(studentId))
        .set(student.name, name)
        .execute();

DirtyChecking vs Querydsl

  • 1만건 기준으로 약 2000배 차이가 난다, DirthCyecking은 9분, querydsl.update는 277ms이다.
  • 그렇다고 일괄업데이트가 좋은 방식은 아니다. 
  • 일괄업데이트의 단점은,
    • hibernate 의 1차 캐시와 2차 캐시가 일괄업데이트시에 갱신이 안된다.
    • 하이버네이트 캐시는 일괄 업데이트시 캐시 갱신이 안된다.
    • 이럴 경우엔 업데이트 대상들에 대한 Cache Eviction이 필요하다.
  • DirtyChecking이 필요한경우
    • 실시간 비지니스 처리, 실시간 단건처리
  • Querydsl.update
    • 대량의 데이터를 일괄로 Update 처리
    • hibernate 캐시 갱신이 필요없는 서비스에서 querydsl update를 사용하는것이 좋다.
  • 진짜 Entity가 필요한게 아니라면 Querydsl과 Dto를 통해 딱 필요한 항목들만 조회하고 업데이트한다.

+ Recent posts