인프런 커뮤니티 질문&답변

작성자 없음

작성자 정보가 삭제된 글입니다.

실전! Querydsl

조인 - 페치 조인

컬렉션 조회 최적화와 동적쿼리에 대한 질문입니다

작성

·

164

0

[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (/아니오)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (/아니오)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (/아니오)

[질문 내용]
여기에 질문 내용을 남겨주세요.

안녕하세요 강사님! 최근 JPA2 에서 수강한 컬렉션 최적화와 QueryDSL을 통해서 주변 병원 조회 기능을 구현하던중에 궁금한것이 생겨서 질문남깁니다.

현재

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SQLDelete(sql = "UPDATE store SET store_status = 'DEACTIVATE' WHERE store_id=?")
@SQLRestriction("store_status = 'ACTIVATE'")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn // 하위 테이블의 구분 컬럼 생성
@Table(name = "store", indexes = {
        @Index(name = "idx_store_name", columnList = "storeName")
})
public abstract class Store extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "store_id")
    private Long storeId;

    @NotNull
    @Size(min = 2)
    @Column(nullable = false)
    private String storeName;

    private String storePhone;

    private String thumbnailUrl;

    private String notice;

    private String websiteLink;

    @Column(columnDefinition = "TEXT")
    private String storeInfo;

    private String storeInfoPhoto;

    @Enumerated(EnumType.STRING)
    private BaseStatus storeStatus;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "store", fetch = FetchType.LAZY)
    private List<BusinessHour> businessHours = new ArrayList<>();

    @OneToMany(mappedBy = "store", fetch = FetchType.LAZY)
    private List<StorePhoto> storePhotos = new ArrayList<>();

//    @OneToMany(mappedBy = "store", fetch = FetchType.LAZY)
//    private List<Reserve> reserves = new ArrayList<>();

    @OneToMany(mappedBy = "store", fetch = FetchType.LAZY)
    private List<Review> reviews = new ArrayList<>();

    @OneToOne(mappedBy = "store", fetch = FetchType.LAZY, cascade = CascadeType.ALL, optional = false)
    private RegistrationInfo registrationInfo;
}

현재 이런식으로 Store 엔티티가 준비되어 있는 상황입니다.

아래는 이를 상속한 Hospital 엔티티입니다.

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@DiscriminatorValue("H")
@OnDelete(action = OnDeleteAction.CASCADE)
@Table(name = "hospital")
public class Hospital extends Store {
    private String additionalServiceTag;

    @OneToMany(mappedBy = "hospital", fetch = FetchType.LAZY)
    private List<TagMapper> tags = new ArrayList<>();
}

변경전 코드

List<StoreQueryInfo> hospitalInfoList = jpaQueryFactory
                .select(
                        Projections.constructor(
                                StoreQueryInfo.class,
                                hospital.storeId.as("storeId"),
                                hospital.storeName.as("storeName"),
                                hospital.thumbnailUrl.as("thumbnailUrl"),
                                businessHour.startTime.as("startTime"),
                                businessHour.endTime.as("endTime"),
                                businessHour.breakStartTime.as("breakStartTime"),
                                businessHour.breakEndTime.as("breakEndTime"),
                                review.reviewId.count().as("reviewCount"),
                                review.rating.avg().as("ratingAvg"),
                                Expressions.stringTemplate(
                                        "ST_Distance_Sphere(ST_PointFromText({0}, 4326), {1})",
                                        point, hospital.address.point
                                ).castToNum(Double.class).as("distance")
                        )
                ).from(hospital)
                .leftJoin(hospital.businessHours, businessHour)
                .on(businessHour.dayOfWeek.eq(dayOfWeek))
                .leftJoin(hospital.reviews, review)
                .leftJoin(hospital.tags, tagMapper)
                .leftJoin(tagMapper.hospitalTag, hospitalTag)
                .where(
                        inDistance(point, queryCond.radius()),
                        businessHourEq(queryCond.businessHourCond()),
                        specialitiesEq(queryCond.specialitiesCond()),
                        emergencyEq(queryCond.emergencyCond()),
                        isOpen(queryCond.openCond(), Time.valueOf(now.minusHours(4)))
                )
                .groupBy(
                        hospital.storeId,
                        hospital.storeName,
                        businessHour.startTime,
                        businessHour.endTime,
                        businessHour.breakStartTime,
                        businessHour.breakEndTime
                )
                .orderBy(Expressions.stringTemplate(
                        "ST_Distance_Sphere(ST_PointFromText({0}, 4326), {1})",
                        point, hospital.address.point
                ).asc())
                .fetch();

변경 후 코드 (변경전의 동적쿼리 미적용)

public List<StoreQueryTotalInfo> findHospitalOptimization(
            Pageable pageable,
            int dayOfWeek, String point, LocalTime now, HospitalQueryCond queryCond) {

        NumberPath<Double> distanceAlias = Expressions.numberPath(Double.class, "distance");


        // 일단 반경 내의 병원 정보를 모두 가져옴.
        List<Tuple> hospitals = jpaQueryFactory
                .select(
                        hospital,
                        Expressions.stringTemplate(
                                "ST_Distance_Sphere(ST_PointFromText({0}, 4326), {1})",
                                point, hospital.address.point
                        ).castToNum(Double.class).as(distanceAlias)
                ).from(hospital)
                .leftJoin(hospital.registrationInfo, registrationInfo).fetchJoin()
                .where(inDistance(point, queryCond.radius()))
                .fetch();

//        // 병원 돌면서 DTO 채우기
        List<TestDTO> list = new ArrayList<>();
        for (Tuple tuple : hospitals) {
            Hospital hospital1 = tuple.get(hospital);
            Double distance = tuple.get(distanceAlias);
            log.info("Hospital: " + hospital1 + ", Distance: " + distance);

            list.add(TestDTO.builder()
                    .storeId(hospital1.getStoreId())
                    .storeName(hospital1.getStoreName())
                    .thumbnailUrl(hospital1.getThumbnailUrl())
                    .time(Times.of(hospital1.getBusinessHours(), dayOfWeek))
                    .reviewCount((long) hospital1.getReviews().size())
                    .ratingAvg(Review.getRatingAvg(hospital1.getReviews()))
                    .distance(formatDistance(distance))
                    .tags(TagInfo.from(hospital1.getTags()))
                    .build());
        }

        return null;
    }

강의를 듣기 전에는 @OneToMany 관계까지 모두 leftJoin()을 이용해서 데이터를 가져왔는데 쿼리가 무진장 많이 나가는 상황이 발생하더라고요.

그래서 컬렉션 쿼리 최적화 수업을 들은 후 위의 코드로 변경하였습니다. (@ToOne 관계만 fetchJoin 하기, @OneToMany 관계는 가져와진 객체에 직접 접근해서 가져오는 방식으로 진행했습니다.)
근데 이때 동적쿼리를 어떤식으로 적용해야하는지 감이 잡히지 않더라고요..!

  1. 일단 원하는 반경 내의 병원을 모두 조회해서 가져오기

  2. repository단에서 반복문을 돌면서 queryCond의 null값을 체크하며 수동으로 동적쿼리를 적용해야 하기 (코드단에서 동적쿼리 적용)

위의 방식을 생각하고 있는데 이게 과연 동적 쿼리(?)가 맞는지 의구심이 들더라고요.

적용해야 하는 동적쿼리는 아래 코드와 같습니다.

private BooleanExpression businessHourEq(String businessHourCond) {
    return businessHourCond != null ? hospitalTag.tagType.eq(HospitalTagType.BUSINESSHOUR).and(hospitalTag.tagContent.eq(businessHourCond)) : null;
}

private BooleanExpression specialitiesEq(String specialitiesCond) {
    return specialitiesCond != null ? hospitalTag.tagType.eq(HospitalTagType.SPECIALITIES).and(hospitalTag.tagContent.eq(specialitiesCond)) : null;
}

private BooleanExpression emergencyEq(String emergencyCond) {
    return emergencyCond.equals("EMERGENCY") ? hospitalTag.tagType.eq(HospitalTagType.EMERGENCY).and(hospitalTag.tagContent.eq(emergencyCond)) : null;
}

private BooleanExpression inDistance(String point, Integer radius) {
    return radius != null
            ?
            Expressions.booleanTemplate(
                    "ST_Contains(ST_Buffer(ST_PointFromText({0}, 4326), {1}), {2})",
                    point, radius, hospital.address.point
            ) : null;
}

private BooleanExpression isOpen(String isOpen, Time now) {
    return isOpen.equals("OPEN")
            ?
            businessHour.startTime.isNotNull().and(businessHour.endTime.isNotNull())
                    .and(businessHour.startTime.loe(now)).and(businessHour.endTime.goe(now))
            :
            null;
}


답변 2

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 박성춘님

repository에서 반복문을 돌게되면 쿼리가 너무 많이 나가게 되겠지요?

따라서 병원 id를 사용해서 나머지 테이블들에 대해 in 쿼리를 사용하는 방법이 필요합니다.

JPA 활용 2편에서 in 쿼리를 사용해서 최적화 하는 부분을 참고해주세요.

참고로 이 방법으로 모든 기능이 다 최적화가 되는 것은 아니고, 본인의 상황에 맞는 최적화에 도움이 될 때 사용하시면 됩니다.

감사합니다.

0

안녕하세요, 인프런 AI 인턴입니다.

박성춘님의 질문은 컬렉션 조회 최적화와 동적쿼리 적용에 관한 것으로 이해됩니다. 질문에서 언급된 컬렉션 최적화는 일대다 관계(@OneToMany)를 쿼리할 때 발생할 수 있는 N+1 문제를 최소화하는 것과 관련이 있으며, 동적 쿼리는 사용자의 요청 조건에 따라 SQL 쿼리의 where 조건이 변경되는 것을 의미합니다.

동적 쿼리와 컬렉션 조회 최적화 관련 유사 질문

  1. 컬렉션 조회 최적화

    • fetch join시 where관련해서 질문드립니다.
      • 이 링크에서 병원 예시와 유사하게 조인을 사용할 때 발생할 수 있는 문제와 fetch join에 대해 설명하고 있습니다. XToOne 관계에서 fetch join을 적용할 때 발생할 수 있는 데이터 부정합 문제와 이를 해결하는 방법에 대한 설명이 도움이 될 수 있습니다.
  2. 동적 쿼리 활용

    • 일대다 collection select 질문
      • 일대다 관계에서 특정 조건에 따라 데이터를 조회하는 방법에 대해 질문하고 있으며, 이와 관련해 동적 쿼리를 어떻게 활용할 수 있는지를 다룹니다.

추가 조언

  • 변경 후 코드에서 동적 쿼리 적용 방법: 변경 후 코드에서 동적 쿼리를 적용하는 방식과 관련하여, QueryDSL를 사용하여 조건에 따라 where 절을 동적으로 변경하는 것이 맞습니다. 각 조건(예: businessHourEq, specialitiesEq, emergencyEq, inDistance, isOpen)을 체크하여 null이 아닌 경우에만 해당 조건을 쿼리의 where 절에 추가하는 것이 동적 쿼리 적용의 핵심입니다.
  • 컬렉션 조회 최적화: @OneToMany 관계의 컬렉션을 조회할 때는 각 컬렉션을 별도의 쿼리로 분리하여 조회하는 방식(batch fetch size 조정 또는 @EntityGraph 사용)을 고려할 수 있습니다.

이와 같은 방식으로 변경 후 코드에서 동적 쿼리를 적용하고, 컬렉션 최적화를 진행하는 것이 좋습니다. 강의 내용과 유사한 예시를 찾지 못하거나 추가적인 구체적인 예제가 필요하다면, QueryDSL 공식 문서 또는 Spring Data JPA 관련 공식 가이드를 참조하는 것도 좋은 방법입니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

작성자 없음

작성자 정보가 삭제된 글입니다.

질문하기