현재 aws의 프리티어 등급의 EC2(1gb ram, 30gb ssd)와 RDS(MySql, 1gb ram, 2gb storage)
그리고 ElastiCache(Redis, 0.5 gb ram)을 사용하여 springboot 프로젝트를 배포하고 있습니다.
대상으로 하는 조회 엔티티는 다음과 같습니다.
1. 현상
현재 Repository 클래스에서 join을 통해 review의 평균점수, 개수를 기준으로 정렬하여 데이터를 조회하고 있는데
아래는 데이터 20만건을 조회하였을때 평군 3초의 로딩시간이 소요되는것을 확인했습니다.
2. 워밍업 - 상속/구현을 사용하지 않고 querydsl 사용하기
이전에 querydsl을 사용할 때에는 다음과같이 별도의 ~RepositoryCustom이라는 인터페이스를 생성하고
해당 인터페이스를 구현한 Impl 클래스를 생성하여 querydsl을 사용했었습니다. 다음 그림과 같이 말이죠
Querydsl을 사용하기 위해서는 매번 인터페이스와 구현 클래스가 필요한 상황이었습니다.
이런 구조에 대해 귀찮음도 있었고 꼭 이렇게 해야하나? 라는 의문이 있었습니다.
2.1 QuerydslRepositorySupport 사용하기
@Repository
public class MemberRepositorySupport extends QuerydslRepositorySupport {
private final JPAQueryFactory queryFactory;
public MemberRepositorySupport(JPAQueryFactory queryFactory) {
super(Member.class);
this.queryFactory = queryFactory;
}
}
QuerydslRepositorySupport는 Spring Data JPA에서 제공하는
QueryDSL을 더 쉽게 사용할 수 있도록 도와주는 유틸리티 클래스입니다.
해당 인터페이스는 JPAQueryFactory를 내부적으로 제공해줍니다.
하지만 매번 QuerydslRepositorySupport를 상속받고 super 생성자에 Entity 클래스를 지정해야한다는 불편함이 있습니다.
2.2 JpaQueryFactory만 있으면 querydsl을 사용할 수 있다.
꼭 무언가를 상속/구현 받지 않더라도, Entity를 지정하지 않아도 Querydsl을 사용하는 방법이 없을까? 라는 고민을 하게 되었습니다.
그러다 이전에 테스트코드를 작성하며 EntityManager만 빈으로 등록되어있다면 JpaQueryFactory를 생성해
querydsl을 사용할 수 있었다는것이 생각났고 다음과 같은 형태로 상속과 구현을 사용하지 않고querydsl을 사용했습니다.
@Repository
@RequiredArgsConstructor
public class MemberQueryRepository {
private final JPAQueryFactory queryFactory;
...
}
다만, JpaRepository의 코드를 확장해서 쓰는게 아니다보니
repository를 사용하는 측면에서는 기본 repository와 custom repository의 메서드들을
하나의 인터페이스로 참조할 수 있다는 장점이 사라집니다.
3. 최적화 하기
그래서 조회시간을 줄이기 위해 여러가지 방법으로 최적화를 시도해보려고 합니다.
1. 필요한 컬럼만 조회하기
현재 select 절은 다음과 같습니다.
메서드의 재활용성은 떨어지겠지만 좀 더 최적화된 쿼리를 생성하여
어플리케이션과 db간의 데이터 전송량을 줄여 조회시간을 단축시킬 수 있을것이라고 생각했습니다.
또한 DTO로 조회된 데이터는 영속성 컨테스트의 관리 대상이 아니므로 dirty checking을 통한 데이터 변경이 불가하지만
조회의 목적이 데이터 변경이아니기 때문에 DTO로 조회하는것이 적절하다고 판단했습니다.
변경후 평균 소요시간은 3.1초 -> 2.6초 정도로 꽤 유의미한 차이를 낼 수 있었습니다.
2. 숙소 엔티티에 리뷰에 대한 통계값 사전에 계산하여 추가하기
해당 메서드에서 아마 가장 시간과 자원이 많이 소모되는 이유는 rental_home - reservation - reivew로 이어지는 테이블 조인을 통해
review의 평균값과 개수를 구하는 부분이라고 생각되어 이부분을 개선해보려고 합니다.
@Entity
@Table(name = "rental_home")
@Getter
public class RentalHome extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "rental_home_id", nullable = false)
private Long id;
...
@Column(nullable = false)
private Double reviewAvg = 0.0;
@Column(nullable = false)
private Long reviewSum = 0L;
@Column(nullable = false)
private Long reviewCount = 0L;
public void updateReviewStatistics(int reviewScore) {
reviewSum += reviewScore;
reviewCount++;
double avg = (double) reviewSum / reviewCount;
reviewAvg = Math.round(avg * 100) / 100.0;
}
}
@Entity
@Getter
@Table(name = "rental_home_review")
public class Review extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "rental_home_review_id")
private Long id;
...
public static Review createReview(Reservation reservation, int score, String content) {
Review review = new Review();
review.reservation = reservation;
review.score = score;
review.content = content;
review.status = WritingStatus.NORMAL;
reservation.getRentalHome().updateReviewStatistics(score);
return review;
}
}
숙소 엔티티에 리뷰점수의 합과 개수를 미리 저장해놓고
새로운 리뷰를 추가할때 기존의 합과 개수를 업데이트 하도록 하였습니다.
정말 큰 효과가 있었습니다.
조인하는 테이블 2개를 줄였고 집계함수를 사용하지 않았더니 api 호출 시간이 2.6초 에서 0.4초로 줄일 수 있었습니다.
또한 리뷰 수정, 삭제시에는 해당 숙소의 리뷰 통계값을 업데이트하도록 하였습니다.
@Override
public void updateAReviewStatistics(RentalHome targetRentalHome) {
JPAQuery<Double> avg = queryFactory.select(review.score.avg().coalesce(0.0))
.from(review)
.join(review.reservation, reservation)
.where(reservation.rentalHome.eq(rentalHome));
JPAQuery<Long> sum = queryFactory.select(review.score.sum().longValue().coalesce(0L))
.from(review)
.join(review.reservation, reservation)
.where(reservation.rentalHome.eq(rentalHome));
JPAQuery<Long> count = queryFactory.select(review.score.count())
.from(review)
.join(review.reservation, reservation)
.where(reservation.rentalHome.eq(rentalHome));
queryFactory.update(rentalHome)
.set(rentalHome.reviewAvg, avg)
.set(rentalHome.reviewSum, sum)
.set(rentalHome.reviewCount, count)
.where(rentalHome.eq(targetRentalHome))
.execute();
}
'Java > JPA' 카테고리의 다른 글
@DataJpaTest를 활용한 Repository 테스트 오류 (0) | 2024.11.16 |
---|---|
JPA Hibernate의 프록시와 일대일 관계에서의 N+1 문제 (0) | 2024.10.06 |
JPA 복잡한 쿼리 작성하기 (0) | 2024.09.20 |