https://www.youtube.com/watch?v=zMAX7g6rO_Y
목차
- 워밍업
- QueryDSL을 implementation/extends 없이 JPAQueryFactory로 사용하기
- 동적쿼리를 Boolean Expression 으로 사용해보기 ( BooleanBuilder 대신 )
- 성능개선 - Select
- Querydsl의 exist 금지
- 성능 개선 - 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를 통해 딱 필요한 항목들만 조회하고 업데이트한다.
'무언가에 대한 리뷰 > 테크영상리뷰' 카테고리의 다른 글
[무언가에 대한 리뷰][테크영상리뷰][쉬운코드]DB 인덱스(DB index) !! (0) | 2023.10.09 |
---|---|
[무언가에 대한 리뷰][테크영상리뷰][10분 테코톡] 라라, 제로의 데이터베이스 인덱스 (0) | 2023.10.07 |
[무언가에 대한 리뷰][10분 테코톡] 🤔 조엘의 GC (0) | 2023.10.04 |
[무언가에 대한 리뷰][10분 테코톡] 조조그린의 Thread Pool (0) | 2023.10.04 |
[무언가에 대한 리뷰][영상리뷰][10분 테코톡] 알렉스, 열음의 멀티스레드와 동기화 In Java (0) | 2023.10.04 |