읽기 전용 쿼리의 성능 최적화에 대한 고민

2023. 7. 31. 00:08프로젝트/[Dotoring] 멘토링 어플리케이션

728x90

아래 사진은 저희 도토링 프로젝트의 홈 화면입니다.

 

 

 

 

이는 멘토가 로그인한다면, 멘토의 직무와 학과를 고려하여 동일한 직무 또는 학과를 가진 멘티님들을 최신순으로 보여주는 화면입니다.

 

 

이 화면의 엔티티들은 다른 회원들의 정보들이므로 수정할 필요가 없습니다. 하지만, 홈화면이기 때문에 다시 조회할 일은 꽤나 많을 것으로 판단됩니다.

 

따라서, 저는 아래와 같은 선택지들을 고민해보았습니다.

 

 

  1. JPQL을 스칼라 타입으로 조회하기 또는 읽기 전용 쿼리 힌트 사용하기
  2. 읽기 전용 트랜잭션 사용 또는 트랜잭션 밖에서 읽기
  3. 캐시 적용하기 → 2차 캐시 적용!!

 

먼저, 위 선택지들의 개념과 특징들을 알아보겠습니다.

 

 

스칼라 타입으로 조회 or 읽기 전용 쿼리 힌트

 

  • 스칼라 타입으로 조회
    • 스칼라 타입은 영속성 컨텍스트가 결과를 관리하지 않는다.
  • 읽기 전용 쿼리 힌트
    • 엔티티를 읽기 전용으로 조회하여 영속성 컨텍스트에서 스냅샷을 보관하지 않는다. 따라서, 메모리 사용량을 최적화할 수 있다.

JPQL은 DB에서 엔티티를 조회해온 후에 영속성 컨텍스트에 해당 엔티티와 식별자가 같은 엔티티가 있다면, 영속성 컨텍스트에 있는 엔티티를 반환한다.
만약, 없다면 영속성 컨텍스트에 DB에서 조회해온 엔티티를 저장한 후 반환한다.
하지만, 스칼라 타입으로 조회하거나 읽기 전용 쿼리 힌트를 사용하면 영속성 컨텍스트에 엔티티를 저장하지 않는다.

 

 

이 방법을 사용한다면, 영속성 컨텍스트에서 엔티티를 관리하지 않기 때문에 메모리 사용량을 최적화할 수 있다.

 

현재 도토링 프로젝트에서는 아래와 같은 방식으로 회원 엔티티를 사용하고 있습니다.

아래 코드는 도토링 프로젝트의 회원 엔티티들을 간략화한 것입니다.

 

 

@Entity
@DiscriminatorColumn(name = "DTYPE")
...
public abstract class Member extends CommonEntity implements Serializable {
	
    private String loginId;

 
    private String password;

   
    private String email;

	  ...
}
@Entity
@DiscriminatorValue("I")
...
public class Menti extends Member {

    private String school;

    private Long grade;

	   ...

}

@DiscriminatorValue("O")
@Entity
...
public class Mento extends Member {

    private String company;

    private Long careerLevel;

    ...

}

 

 

현재, 멤버 엔티티를 각각 멘토 엔티티와 멘티 엔티티가 상속받아 사용하고 있습니다.

 

(조회 성능을 생각하여 싱글 테이블 전략을 사용했습니다.)

도토링 프로젝트에서는 해당 테이블에 조회하는 컬럼이 아닌 다른 컬럼들을 고려하여 DTO를 사용하여 필요한 것만 조회하였습니다.

 

이를 적용한 코드는 아래와 같습니다.

 

 

public Slice<MentoCardResponseDTO> findAllBySlice(Long lastMentoId, MentoFilterCondition mentoFilterCondition, Pageable pageable) {

        BooleanExpression jobCondition = eqJob(mentoFilterCondition.getJobs());
        BooleanExpression majorCondition = eqMajor(mentoFilterCondition.getMajors());

        if (jobCondition == null) {
            jobCondition = mento.job.isNull();
        }

        if (majorCondition == null) {
            majorCondition = mento.major.isNull();
        }

        List<MentoCardResponseDTO> results = query
                .select(Projections.bean(MentoCardResponseDTO.class, mento.id, mento.profileImage, mento.nickname, mento.job, mento.major, mento.introduction))
                .from(mento)
                .where(lessThanMentoId(lastMentoId), jobCondition.or(majorCondition))
                .orderBy(mento.job.asc(), mento.major.asc(), mento.id.desc())
                .limit(pageable.getPageSize() + 1)
                .setHint("org.hibernate.readOnly", true)
                .fetch();
       
        return checkLastPage(pageable, results);
    }

 

위 코드는 QueryDSL를 사용하여 동적 쿼리를 구현한 코드입니다.

QueryDSL에 대한 포스팅은 추후에 하도록 하겠습니다!!

 

@Getter
@Setter
@NoArgsConstructor
public class MentoCardResponseDTO {

    private Long id;
    private String profileImage;
    private String nickname;
    private String mentoringSystem;
    private Job job;
    private Major major;
    private String introduction;

    @Builder
    public MentoCardResponseDTO(Long id, String profileImage, String nickname, String mentoringSystem, Job job, Major major, String introduction) {
        this.id = id;
        this.profileImage = profileImage;
        this.nickname = nickname;
        this.mentoringSystem = mentoringSystem;
        this.job = job;
        this.major = major;
        this.introduction = introduction;
    }
}

 

위 코드는 스칼라 타입으로 조회하기 위해 사용한 DTO입니다.

 

읽기 전용 트랜잭션 사용

 

 

  • 읽기 전용 트랜잭션
@Transactional(readOnly = true)

 

 

이를 적용하면, 플러시를 수행하지 않아 스냅샷 비교와 같은 무거운 로직들은 수행하지 않는다. 물론, 트랜잭션을 시작했으므로 트랜잭션의 시작, 로직 수행, 트랜잭션의 커밋과 같은 과정은 수행된다.

 

트랜잭션이 커밋될 때 내부적으로 flush가 호출되는 데, readOnly = true로 하면 flush가 호출되지 않는다.

 


위 방법을 사용해 플러시를 작동하지 않도록 하여 성능을 향상시킬 수 있다.

@Transactional(readOnly = true)
public Slice<MentoCardResponseDTO> findAllMentoBySlice(Long lastMentoId, Integer size, MentoFilterCondition mentoFilterCondition){
    PageRequest pageRequest = PageRequest.of(0, size);
    Slice<MentoCardResponseDTO> mentoSlice = mentoQueryRepository.findAllBySlice(lastMentoId, mentoFilterCondition, pageRequest);
    return mentoSlice;
}

 

2차 캐시 적용

 

 

이는 다음 포스팅에서 다루도록 하겠습니다.

 

 

이상입니다!!

728x90