Skip to content

쿼리 성능 개선 ‐ 템플릿 생성

Jang Hyeok-su edited this page Sep 26, 2024 · 6 revisions

데이터 스펙

멤버: 10 건
카테고리: 100 건 (멤버 당 10 건)
태그: 2000 건 (멤버 당 200 건)
템플릿: 10만 건 (멤버 당 1만 건)
소스 코드: 10만 ~ 50만 건 (템플릿 당 1~5 개 랜덤 생성)

테스트 조건

10개의 스레드로 100번씩 실행
총 1000번의 요청 실행

생성하는 템플릿 조건

사용하는 태그 : 20개 (10개는 존재하지 않는 태그 사용) 소스 코드 : 2개


개선 전

속도 측정

Total request count: 1000
Total elapsed time: 81569ms
Average elapsed time: 81ms

쿼리 분석

1. Template 을 저장할 Category 조회

  • Repository: CategoryRepository
  • Method: fetchById
select  
    c1_0.id,
    c1_0.created_at,
    c1_0.is_default,
    c1_0.member_id,
    m1_0.id,
    m1_0.created_at,
    m1_0.modified_at,
    m1_0.name,
    m1_0.password,
    m1_0.salt,
    c1_0.modified_at,
    c1_0.name
from  
    category c1_0  
        join  
            member m1_0  
            on m1_0.id=c1_0.member_id  
where  
    c1_0.id=?  
  • 호출 횟수: 1회

2. Template 내용 저장

  • Repository: TemplateRepository
  • Method: save
insert  
into  
    template  
(category_id, created_at, description, member_id, modified_at, title)  
values  
    (?, ?, ?, ?, ?, ?) 
  • 호출 횟수: 1회

3. Template 에서 사용할 태그 중 존재하는 태그의 이름 조회

  • Repository: TagRepository
  • Method: findNameByNamesIn (직접 쿼리 작성)
select  
    t1_0.name  
from  
    tag t1_0  
where  
    t1_0.name in (?, ?, ?, ?, ?)  
  • 호출 횟수: 1회

4. 존재하는 태그 조회

  • Repository: TagRepository
  • Method: fetchByName
select  
    t1_0.id,  
    t1_0.created_at,  
    t1_0.modified_at,  
    t1_0.name  
from  
    tag t1_0  
where  
    t1_0.name=?  
  • 호출 횟수: (전체 태그 중 존재하는 태그 개수)

5. 템플릿에 기존의 태그를 저장

  • Repository: templateTagRepository
  • Method: saveAll
select  
    tt1_0.tag_id,  
    tt1_0.template_id,  
    tt1_0.created_at,  
    tt1_0.modified_at,  
    t1_0.id,  
    t1_0.created_at,  
    t1_0.modified_at,  
    t1_0.name,  
    t2_0.id,  
    t2_0.category_id,  
    t2_0.created_at,  
    t2_0.description,  
    (select  
         count(*)  
     from  
         likes  
     where  
         likes.template_id = t2_0.id),  
    t2_0.member_id,  
    t2_0.modified_at,  
    t2_0.title  
from  
    template_tag tt1_0  
    join  
        tag t1_0  
        on t1_0.id=tt1_0.tag_id  
    join  
        template t2_0  
        on t2_0.id=tt1_0.template_id  
where  
    (tt1_0.tag_id, tt1_0.template_id) in ((?, ?))
insert  
into  
    template_tag  
(created_at, modified_at, tag_id, template_id)  
values  
    (?, ?, ?, ?)
  • 호출 횟수: (전체 태그 중 존재하는 태그 개수)

6. 새로운 태그를 저장

  • Repository: TagRepository
  • Method: saveAll
insert  
into  
    tag  
(created_at, modified_at, name)  
values  
    (?, ?, ?)
  • 호출 횟수: (전체 태그 중 존재하는 새로운 태그 개수)

7. 템플릿에 새로운 태그를 저장

  • Repository: TemplateTagRepository
  • Method: saveAll
select  
    tt1_0.tag_id,  
    tt1_0.template_id,  
    tt1_0.created_at,  
    tt1_0.modified_at,  
    t1_0.id,  
    t1_0.created_at,  
    t1_0.modified_at,  
    t1_0.name,  
    t2_0.id,  
    t2_0.category_id,  
    t2_0.created_at,  
    t2_0.description,  
    (select  
         count(*)  
     from  
         likes  
     where  
         likes.template_id = t2_0.id),  
    t2_0.member_id,  
    t2_0.modified_at,  
    t2_0.title  
from  
    template_tag tt1_0  
        join  
            tag t1_0  
            on t1_0.id=tt1_0.tag_id  
        join  
            template t2_0  
            on t2_0.id=tt1_0.template_id  
where  
    (tt1_0.tag_id, tt1_0.template_id) in ((?, ?))
insert
into
    template_tag
(created_at, modified_at, tag_id, template_id)
values
    (?, ?, ?, ?)
  • 호출 횟수: (전체 태그 중 존재하는 새로운 태그 개수)

8. 소스코드를 저장

  • Repository: SourceCodeRepository
  • Method: saveAll
insert  
into  
    source_code  
(content, created_at, filename, modified_at, ordinal, template_id)  
values  
    (?, ?, ?, ?, ?, ?)
  • 호출 횟수: 소스코드 개수

9. 썸네일 소스코드 조회

  • Repository: SourceCodeJpaRepository
  • Method: fetchByTemplateAndOrdinal
select  
    sc1_0.id,  
    sc1_0.content,  
    sc1_0.created_at,  
    sc1_0.filename,  
    sc1_0.modified_at,  
    sc1_0.ordinal,  
    sc1_0.template_id  
from  
    source_code sc1_0  
where  
    sc1_0.template_id=?  
  and sc1_0.ordinal=?
  • 호출 횟수: 1번

10. 썸네일 저장

  • Repository: ThumbnailRepository
  • Method: save
insert  
into  
    thumbnail  
(created_at, modified_at, source_code_id, template_id)  
values  
    (?, ?, ?, ?)
  • 호출 횟수: 1번

개선을 위해 필요한 작업


인덱스 개선 사항

Tag 테이블

  • 인덱스 제안:
    • name
      • 이유: 템플릿에서 사용할 태그들을 이름으로 조회한다.
      • 추가 여부: O, CREATE INDEX idx_tag_name ON tag(name);
explain
    SELECT t1_0.id,
           t1_0.created_at,
           t1_0.modified_at,
           t1_0.name
    FROM tag t1_0
    WHERE t1_0.name IN ('newTag0', 'newTag1', 'newTag2');
id select_type type possible_keys key Extra
1 SIMPLE range idx_tag_name idx_tag_name Using index condition

쿼리 최적화

TemplateTag 생성 로직 개선

참고 : Tag 를 생성할 때, 이미 존재하는 Tag 는 생성하지 않는다.

as-is

존재하는 태그들을 List<String> existingTags 로 이름만 조회한 후, 각각의 이름에 대해 TagRepository.fetchByName() 으로 Tag 조회한다.
존재하는 태그의 개수 만큼 TagRepository.fetchByName() 가 호출된다.

    @Transactional
    public void createTags(Template template, List<String> tagNames) {
        List<String> existingTags = tagRepository.findNameByNamesIn(tagNames);

        templateTagRepository.saveAll(
                existingTags.stream()
                        .map(tagRepository::fetchByName) 
                        .map(tag -> new TemplateTag(template, tag))
                        .toList()
        );

        List<Tag> newTags = tagRepository.saveAll(
                tagNames.stream()
                        .filter(tagName -> !existingTags.contains(tagName))
                        .map(Tag::new)
                        .toList()
        ); 

        templateTagRepository.saveAll(
                newTags.stream()
                        .map(tag -> new TemplateTag(template, tag))
                        .toList()
        );
    }

to-be

tagRepository.findByNameIn(List<String> names); 으로 태그 자체를 조회하여 Tag 객체를 재사용한다.
tagRepository.fetchByName() 을 추가로 호출할 필요가 없다.

    @Transactional
    public void createTags(Template template, List<String> tagNames) {
        List<Tag> existingTags = tagRepository.findByNameIn(tagNames);
        List<String> existNames = existingTags.stream()
                .map(Tag::getName)
                .toList();

        List<Tag> newTags = tagRepository.saveAll(
                tagNames.stream()
                        .filter(name -> !existNames.contains(name))
                        .map(Tag::new)
                        .toList()
        );
        existingTags.addAll(newTags);
        for (Tag existingTag : existingTags) {
            templateTagRepository.save(new TemplateTag(template, existingTag));
        }
    }

TemplateTag 저장 로직 개선

TemplateTag 를 저장하기 전, TemplateTag 를 조회하고 이 때 사용하지 않는 Template, Tag 를 조회하기 위해 불필요하게 긴 쿼리문을 사용한다.

select  
    tt1_0.tag_id,  
    tt1_0.template_id,  
    tt1_0.created_at,  
    tt1_0.modified_at,  
    t1_0.id,  
    t1_0.created_at,  
    t1_0.modified_at,  
    t1_0.name,  
    t2_0.id,  
    t2_0.category_id,  
    t2_0.created_at,  
    t2_0.description,  
    (select  
         count(*)  
     from  
         likes  
     where  
         likes.template_id = t2_0.id),  
    t2_0.member_id,  
    t2_0.modified_at,  
    t2_0.title  
from  
    template_tag tt1_0  
    join  
        tag t1_0  
        on t1_0.id=tt1_0.tag_id  
    join  
        template t2_0  
        on t2_0.id=tt1_0.template_id  
where  
    (tt1_0.tag_id, tt1_0.template_id) in ((?, ?))

이를 개선하기 위해 TemplateTag.template, TemplateTag.tag 필드를 lazy loading 한다.

각 필드에 @ManyToOne(fetch = FetchType.LAZY) 어노테이션 추가

개선 후

속도 측정

Total request count: 1000
Total elapsed time: 50798ms
Average elapsed time: 50ms

쿼리 분석

1. Template 을 저장할 Category 조회

  • Repository: CategoryRepository
  • Method: fetchById
select  
    c1_0.id,  
    c1_0.created_at,  
    c1_0.is_default,  
    c1_0.member_id,  
    m1_0.id,  
    m1_0.created_at,  
    m1_0.modified_at,  
    m1_0.name,  
    m1_0.password,  
    m1_0.salt,  
    c1_0.modified_at,  
    c1_0.name  
from  
    category c1_0  
        join  
            member m1_0  
            on m1_0.id=c1_0.member_id  
where  
    c1_0.id=?  
  • 호출 횟수: 1회

2. Template 내용 저장

  • Repository: TemplateRepository
  • Method: save
insert  
into  
    template  
(category_id, created_at, description, member_id, modified_at, title)  
values  
    (?, ?, ?, ?, ?, ?) 
  • 호출 횟수: 1회

3. Template 에서 사용할 태그 중 존재하는 태그 조회

  • Repository: TagRepository
  • Method: findByNameIn (직접 쿼리 작성)
select
    t1_0.id,
    t1_0.created_at,
    t1_0.modified_at,
    t1_0.name
from
    tag t1_0
where
    t1_0.name in (?, ?, ?, ?, ?)
  • 호출 횟수: 1회

4. 새로운 태그를 저장

  • Repository: TagRepository
  • Method: saveAll
insert  
into  
    tag  
(created_at, modified_at, name)  
values  
    (?, ?, ?)
  • 호출 횟수: (전체 태그 중 존재하는 새로운 태그 개수)

5. 템플릿에 새로운 태그를 저장

  • Repository: TemplateTagRepository
  • Method: saveAll
select  
    tt1_0.tag_id,  
    tt1_0.template_id,  
    tt1_0.created_at,  
    tt1_0.modified_at  
from  
    template_tag tt1_0  
where  
    (  
        tt1_0.tag_id, tt1_0.template_id  
    ) in ((?, ?))
insert
into
    template_tag
(created_at, modified_at, tag_id, template_id)
values
    (?, ?, ?, ?)
  • 호출 횟수: (태그 개수)

6. 소스코드를 저장

  • Repository: SourceCodeRepository
  • Method: saveAll
insert  
into  
    source_code  
(content, created_at, filename, modified_at, ordinal, template_id)  
values  
    (?, ?, ?, ?, ?, ?)
  • 호출 횟수: 소스코드 개수

7. 썸네일 소스코드 조회

  • Repository: SourceCodeJpaRepository
  • Method: fetchByTemplateAndOrdinal
select  
    sc1_0.id,  
    sc1_0.content,  
    sc1_0.created_at,  
    sc1_0.filename,  
    sc1_0.modified_at,  
    sc1_0.ordinal,  
    sc1_0.template_id  
from  
    source_code sc1_0  
where  
    sc1_0.template_id=?  
  and sc1_0.ordinal=?
  • 호출 횟수: 1번

8. 썸네일 저장

  • Repository: ThumbnailRepository
  • Method: save
insert  
into  
    thumbnail  
(created_at, modified_at, source_code_id, template_id)  
values  
    (?, ?, ?, ?)
  • 호출 횟수: 1번

⚡️ 코드zap

프로젝트

규칙 및 정책

공통

백엔드

프론트엔드

매뉴얼

백엔드

기술 문서

백엔드

프론트엔드

회의록


Clone this wiki locally