Skip to content

쿼리 성능 개선 ‐ 템플릿 수정

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

데이터 스펙

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

테스트 조건

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

수정하는 템플릿 조건

사용하는 태그 : 10개 소스 코드 : 5개 -> 5개 (수정하는 코드 1 / 생성하는 코드 2 / 삭제하는 코드 2)


개선 전

속도 측정

Total request count: 1000
Total elapsed time: 75215ms
Average elapsed time: 75ms

쿼리 분석

1. 템플릿의 카테고리 조회

  • Repository: CategoryJpaRepository
  • 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. 수정할 템플릿 조회

  • Repository: TemplateJpaRepository
  • Method: fetchById
select  
    t1_0.id,  
    t1_0.category_id,  
    t1_0.created_at,  
    t1_0.description,  
    (select  
         count(*)  
     from  
         likes  
     where  
         likes.template_id = t1_0.id),  
    t1_0.member_id,  
    t1_0.modified_at,  
    t1_0.title  
from  
    template t1_0  
where  
    t1_0.id=?
  • 호출 횟수: 1번

3. 기존에 연결되어 있던 태그 제거

  • Repository: TemplateTagRepository
  • Method: deleteAllByTemplateId
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.template_id=?
  • 호출 횟수: 1번
select  
    t1_0.id,  
    t1_0.created_at,  
    t1_0.modified_at,  
    t1_0.name  
from  
    tag t1_0  
where  
    t1_0.id=?
  • 호출 횟수: (템플릿에 저장되어 있던 태그 개수)

4. 사용할 태그 중 저장되어 있는 태그의 이름 조회

  • Repository: TagJpaRepository
  • Method: findNameByNamesIn
select  
    t1_0.name  
from  
    tag t1_0  
where  
    t1_0.name in (?, ?, ?, ?, ?)
  • 호출 횟수: 1번

5. 존재하는 태그 조회

  • 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=?  
  • 호출 횟수: (전체 태그 중 존재하는 태그 개수)

6. 템플릿 내용 업데이트

  • 도메인 업데이트 쿼리 늦게 날아감
update  
    template  
set  
    category_id=?,  
    description=?,  
    member_id=?,  
    modified_at=?,  
    title=?  
where  
    id=?
  • 호출 횟수: 1번

7. ( 3. 태그 제거 ) 삭제 쿼리 늦게 날아감

delete  
from  
    template_tag  
where  
    tag_id=?  
  and template_id=?
  • 호출 횟수: (템플릿에 저장되어 있던 태그 개수)

8. 존재하는 태그들을 템플릿에 저장

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 ((?, ?))
  • 호출 횟수: (템플릿에 저장되어 있던 태그 개수)

9. 썸네일 조회

select  
    t1_0.id,  
    t1_0.created_at,  
    t1_0.modified_at,  
    t1_0.source_code_id,  
    t1_0.template_id  
from  
    thumbnail t1_0  
where  
    t1_0.template_id=?
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.id=?
  • 호출 횟수: 1번

10. 수정할 소스 코드 조회

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.id=?
  • 호출 횟수: 1번

11. 템플릿에 태그 저장

insert  
into  
    template_tag  
(created_at, modified_at, tag_id, template_id)  
values  
    (?, ?, ?, ?)
  • 호출 횟수: (사용하는 태그 수)

12. 소스코드 저장

insert   
into  
    source_code  
(content, created_at, filename, modified_at, ordinal, template_id)  
values  
    (?, ?, ?, ?, ?, ?)
  • 호출 횟수: (새로운 소스코드)

13. 소스코드 삭제

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.id=?
  • 호출 횟수: (삭제할 소스코드)

13. 소스코드 업데이트

update  
    source_code  
set  
    content=?,  
    filename=?,  
    modified_at=?,  
    ordinal=?,  
    template_id=?  
where  
    id=?
  • 호출 횟수: (업데이트할 소스코드)

14. 소스코드 삭제 (나중에 날아가는 쿼리)

delete  
from  
    source_code  
where  
    id=?
  • 호출 횟수: (삭제할 소스코드)

15. 소스코드 개수 조회

select  
    count(sc1_0.id)  
from  
    source_code sc1_0  
where  
    sc1_0.template_id=?
  • 호출 횟수: 1번

개선을 위해 필요한 작업


인덱스 개선 사항

Tag 테이블

  • 인덱스 제안:
    • name
      • 이유: 템플릿에서 사용할 태그들을 이름으로 조회한다.
      • 추가 여부: O, CREATE INDEX idx_tag_name ON tag(name);

쿼리 최적화

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 를 조회하고 이 때 사용하지 않는 Tag 를 조회하기 위해 추가 쿼리를 사용한다.

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

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

개선 후

속도 측정

Total request count: 1000
Total elapsed time: 60483ms
Average elapsed time: 60ms

쿼리 분석

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. 수정할 템플릿 조회

  • Repository: TemplateJpaRepository
  • Method: fetchById
select  
    t1_0.id,  
    t1_0.category_id,  
    t1_0.created_at,  
    t1_0.description,  
    (select  
         count(*)  
     from  
         likes  
     where  
         likes.template_id = t1_0.id),  
    t1_0.member_id,  
    t1_0.modified_at,  
    t1_0.title  
from  
    template t1_0  
where  
    t1_0.id=?
  • 호출 횟수: 1번

3. 기존에 연결되어 있던 태그 제거

  • Repository: TemplateTagRepository
  • Method: deleteAllByTemplateId
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.template_id=?
  • 호출 횟수: 1번

4. Template 에서 사용할 태그 조회

  • Repository: TagRepository
  • Method: findByNameIn
select  
    t1_0.name  
from  
    tag t1_0  
where  
    t1_0.name in (?, ?, ?, ?, ?)  

5. 템플릿 내용 업데이트

  • 도메인 업데이트 쿼리 늦게 날아감
update  
    template  
set  
    category_id=?,  
    description=?,  
    member_id=?,  
    modified_at=?,  
    title=?  
where  
    id=?
  • 호출 횟수: 1번

6. ( 3. 태그 제거 ) 삭제 쿼리 늦게 날아감

delete  
from  
    template_tag  
where  
    tag_id=?  
  and template_id=?
  • 호출 횟수: (템플릿에 저장되어 있던 태그 개수)

7. 존재하는 태그들을 템플릿에 저장

    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 ((?, ?))
  • 호출 횟수: (템플릿에 저장되어 있던 태그 개수)

9. 썸네일 조회

select  
    t1_0.id,  
    t1_0.created_at,  
    t1_0.modified_at,  
    t1_0.source_code_id,  
    t1_0.template_id  
from  
    thumbnail t1_0  
where  
    t1_0.template_id=?
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.id=?
  • 호출 횟수: 1번

10. 수정할 소스 코드 조회

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.id=?
  • 호출 횟수: 1번

11. 템플릿에 태그 저장

insert  
into  
    template_tag  
(created_at, modified_at, tag_id, template_id)  
values  
    (?, ?, ?, ?)
  • 호출 횟수: (사용하는 태그 수)

12. 소스코드 저장

insert   
into  
    source_code  
(content, created_at, filename, modified_at, ordinal, template_id)  
values  
    (?, ?, ?, ?, ?, ?)
  • 호출 횟수: (새로운 소스코드)

13. 소스코드 삭제

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.id=?
  • 호출 횟수: (삭제할 소스코드)

13. 소스코드 업데이트

update  
    source_code  
set  
    content=?,  
    filename=?,  
    modified_at=?,  
    ordinal=?,  
    template_id=?  
where  
    id=?
  • 호출 횟수: (업데이트할 소스코드)

14. 소스코드 삭제 (나중에 날아가는 쿼리)

delete  
from  
    source_code  
where  
    id=?
  • 호출 횟수: (삭제할 소스코드)

15. 소스코드 개수 조회

select  
    count(sc1_0.id)  
from  
    source_code sc1_0  
where  
    sc1_0.template_id=?

⚡️ 코드zap

프로젝트

규칙 및 정책

공통

백엔드

프론트엔드

매뉴얼

백엔드

기술 문서

백엔드

프론트엔드

회의록


Clone this wiki locally