[Project] 카테시안 곱의 발생과 해결

현주·2023년 5월 3일
1
post-custom-banner

📌 QueryDsl과 fetchJoin()에 대한 자세한 개념들은 아래 포스팅을 참고해주세요.

프로젝트에서 N+1 문제가 발생해서 fetchJoin()을 사용하여 이를 해결하는 과정에서,

yata 게시물에 신청한 yataMember 중, yata 등록자에게 수락되어 차를 타기로 확정된 yataMember를 계산해서 아래와 같이 보여줄 필요가 있었다.

yata와 yataMember는 1:N 관계로 연관관계가 맺어져 있고,

@OneToMany 애너테이션의 경우 카테시안 곱 문제나
여러개의 fetchJoin을 사용할 경우 발생하는 MulipleBagFetchException을 고려해야한다.


✔️ 카테시안 곱 (Cartesian Product)

  • From절에 2개 이상의 Table이 있을 때, 두 Table 사이에 유효 join 조건을 적지 않았을 경우
    해당 테이블에 대한 모든 데이터를 전부 결합하여 Table에 존재하는 행 갯수를 곱한 만큼의 결과값이 반환되는 것

➜ N개의 행을 가진 테이블과 M개의 행을 가진 테이블이 Join된 경우, N*M개의 결과값이 산출

Ex. A 테이블에 요소가 3가지 있고, B 테이블에도 요소가 3가지 있다고 할 경우,
( 각각 A_id, B_id가 3개씩인 경우 )

SELECT *
FROM   A LEFT OUTER JOIN
       B  ON  1 = 1;

위와 같은 쿼리를 적었다고 가정한다면 쿼리 실행 시에 3*3개인 9개의 결과가 나온다고 한다.

👉 즉, 카테시안 곱은 join 쿼리 중에 WHERE 절에 기술하는 join 조건이 잘못 기술되었거나 아예 없을 경우 발생하는 현상


한번의 쿼리로 yataMember를 조회할 때, 그와 연관관계가 있는 yata까지 한번에 조회할 수 있다.

fetchType이 Lazy로 설정되어있어도 fetchJoin이 우선순위를 가지기 때문에 한번의 조회로 연관된 엔티티를 같이 조회한다.

ManyToOne 관계에서는 fetchJoin을 쓰면 N+1 문제를 해결할 수 있다.

❗❗ But, OneToMany 관계에서는 데이터가 카테시안 곱만큼 늘어나서 조회된다는 것이다.


우리 프로젝트의 예를 들어,

아래와 같이 Yata와 YataMember 테이블이 있다고 할 때,

SELECT *
FROM Yata A LEFT OUTER JOIN
     YataMember B ON 1 = 1

위와 같은 쿼리로 YataMember와 Join된 Yata 테이블을 조회할 경우,

YATA 테이블과 YATAMEMBER 테이블의 값이 중복되어

결국 3 * 6 = 18개의 결과값이 조회되는 카테시안 곱 현상이 발생한다.


💡 카테시안 곱 (Cartesian Product) 해결 방법

이 문제를 해결하기 위해, 자료의 중복을 제거하는 두가지 방법을 일반적으로 사용한다.

1. 컬렉션 Set 사용 (중복 허용 X)

2. distinct() 사용


우리 프로젝트에서는 YataMapper 클래스에서 yataMemeber를 얻어올 때 Distinct를 사용해 중복을 제거해주어 문제를 해결하였다.

  • yataMapper 클래스
    default YataDto.Response yataToYataResponse(Yata yata) {
        if (yata == null) {
            return null;
        }YataDto.Response.ResponseBuilder response = YataDto.Response.builder();if (yata.getYataId() != null) {
            response.yataId(yata.getYataId());
        }
        if (yata.getYataMembers() == null) response.reservedMemberNum(0);
        else response.reservedMemberNum(yata.getYataMembers().stream().distinct().mapToInt(YataMember::getBoardingPersonCount).sum()); //중복제거
        .
        .
        .
        return response.build();
    }default List<YataDto.AcceptedResponse> yataToMyYatas(List<Yata> yatas , String email){
        if (yatas == null) {
            return null;
        }
        return yatas.stream().map(yata -> {
            YataDto.AcceptedResponse response = new YataDto.AcceptedResponse();
            response.setYataResponse(yataToYataResponse(yata));
            response.getYataResponse().setYataMembers(null);
            YataMember yataMember = yata.getYataMembers()
                    .stream().distinct() // 중복 제거
                    .filter(yataMember1 ->
                            yataMember1.getMember().getEmail().equals(email)).findFirst().orElse(null);
            response.setYataMemberId(yataMember != null ? yataMember.getYataMemberId() : null);
            .
            .
            .
            return response;
        }).collect(Collectors.toList());
    }

💬 도움이 된 사이트

post-custom-banner

0개의 댓글