1. 중복 제거 by groupBy

@Override
    public Slice<RoomSearchResponseDto> findRoomCustom(RoomSearchRequestDto req, Pageable pageable) {
        List<RoomSearchResponseDto> content = jpaQueryFactory.select(Projections.constructor(RoomSearchResponseDto.class,
                        room.id,
                        member.memberId,
                        member.nickname,
                        room.title,
                        room.description,
                        room.link,
                        room.roomImage,
                        room.mcount,
                        room.capacity,
                        room.isLocked,
                        room.password,
                        room.constraints,
                        room.type)).
                from(room)
                .innerJoin(member).on(room.manager.memberId.eq(member.memberId))
                .leftJoin(roomKeyword).on(roomKeyword.room.eq(room))
                .leftJoin(keyword).on(roomKeyword.keyword.eq(keyword))
                .where(makeBooleanBuilder(req))
                .groupBy(room.id)  // group by를 사용하여 중복된 room.id를 제거
                .orderBy(makeOrder(req)).
                offset(pageable.getOffset()).
                limit(pageable.getPageSize() + 1). // 1개를 더 가져온다
                        fetch();

        boolean hasNext = content.size() > pageable.getPageSize(); // 뒤에 더 있는지 확인
        content = hasNext ? content.subList(0, pageable.getPageSize()) : content; // 뒤에 더 있으면 1개 더 가져온거 빼고 넘긴다
        return new SliceImpl<>(content, pageable, hasNext);
    }
private BooleanBuilder makeBooleanBuilder(RoomSearchRequestDto req) {
        BooleanBuilder builder = new BooleanBuilder();

        if (req.getSearchKeyword() != null) {
            builder.andAnyOf(
                    room.title.contains(req.getSearchKeyword()),
                    room.description.contains(req.getSearchKeyword())
            );
        }
        if (req.getIsLocked() != null)
            builder.and(room.isLocked.eq(req.getIsLocked()));
        if (req.getMinMcount() != null || req.getMaxMcount() != null) {
            builder.and(room.mcount.between(req.getMinMcount(), req.getMaxMcount()));
        }
        if (req.getMinCapacity() != null || req.getMaxCapacity() != null) {
            builder.and(room.capacity.between(req.getMinCapacity(), req.getMaxCapacity()));
        }
        if (req.getConstraints() != null && !req.getConstraints().isEmpty())
            builder.and(room.constraints.in(req.getConstraints()));
        if (req.getType() != null)
            builder.and(room.type.eq(req.getType()));
        if (req.getKeywordIds() != null && req.getKeywordIds().size() > 0){
            builder.and(roomKeyword.keyword.id.in(req.getKeywordIds()));
        }

        return builder;
    }

그런데 원래 Group By는 중복 제거가 아닌 집계 함수를 사용하기 위함이 아닌가?

맞다. 따라서 일반적으로는 **GROUP BY**를 사용할 때는 집계 함수와 함께 사용하며, 집계 함수를 사용하지 않는다면 SELECT 절에 명시된 모든 열은 GROUP BY 절에 포함되어야 한다.

https://kimsyoung.tistory.com/entry/GROUP-BY下-오류문-해결하기

그러나 Querydsl에서 groupby는 어떻게 정확히 동작하는지 모르겠으나 집계함수를 사용하지 않았는데도 제대로 작동이 되었다. 아마 group by 기준을 room 엔티티의 pk로 잡았고, dto의 값들이 모두 이 pk를 기준으로 유일하기 때문에 제대로 작동된 것이 아닐까..! 정확한 것은 더 찾아봐야겠다 ㅠㅠ

2. 중복 제거 by distinct()

Spring Data JPA에서 **distinct**를 사용하려면 특정 필드에 대해 **distinct**를 적용하는 것이 아니라, 특정 엔터티에 대해 **distinct**를 적용합니다. 따라서 **room.id**가 아닌 room 엔터티에 대한 **distinct**를 적용할 수 있습니다.

그러나 “엔티티”에 대해 **distinct**를 사용하면 JPA는 엔터티의 식별자 (ID)에 대한 **distinct**를 적용하게 됩니다. 이 경우에는 결과 집합에 대해 **distinct**를 사용하는 것이 큰 의미가 없습니다. 특히 **Projections.constructor**를 사용하는 경우 유의 ⇒ (모든 필드를 기준으로 중복제거를 함)

public Slice<RoomSearchResponseDto> findRoomCustom(RoomSearchRequestDto req, Pageable pageable) {
        List<RoomSearchResponseDto> content = jpaQueryFactory.select(Projections.constructor(RoomSearchResponseDto.class,
                        room.id,
                        member.memberId,
                        member.nickname,
                        room.title,
                        room.description,
                        room.link,
                        room.roomImage,
                        room.mcount,
                        room.capacity,
                        room.isLocked,
                        room.password,
                        room.constraints,
                        room.type)).distinct(). // groupBy 대신 distinct로 중복 제거
                from(room) // from 이후에 들어가면 
                .innerJoin(member).on(room.manager.memberId.eq(member.memberId))
                .leftJoin(roomKeyword).on(roomKeyword.room.eq(room))
                .leftJoin(keyword).on(roomKeyword.keyword.eq(keyword))
                .where(makeBooleanBuilder(req))
//                .groupBy(room.id)
                .orderBy(makeOrder(req)).
                offset(pageable.getOffset()).
                limit(pageable.getPageSize() + 1). // 1개를 더 가져온다
                        fetch();

        boolean hasNext = content.size() > pageable.getPageSize(); // 뒤에 더 있는지 확인
        content = hasNext ? content.subList(0, pageable.getPageSize()) : content; // 뒤에 더 있으면 1개 더 가져온거 빼고 넘긴다
        return new SliceImpl<>(content, pageable, hasNext);
    }