@OneToMany의 COUNT 오차 문제 해결하기

..·2025년 2월 15일

finder

목록 보기
17/23

문제 상황

1:N 관계를 가지는 A와 B 엔티티의 데이터를 불러오는 API를 작성했는데, COUNT(*) 과정에서 실제와 다른 수치가 반환되는 문제를 발견했다.

확인 결과, COUNT(*)A LEFT JOIN B의 값 대신 A INNER JOIN B의 값을 반환하는 상태로 확인되었다.

  • API 구조

엔티티 A에 대한 목록을 불러올 때,
A와 동일한 key를 가지는 B가 있다면 A의 목록 하위에 B 데이터를 리스트 형식으로 추가

{
  	"result": [
      {
        	"a_number": 1,
        	"a_name": "first_a",
        	"key": "27289",
        	"b_list": []
      },
      ...
    ],
    "totalCount": 1234
}
  • 문제 상황

검색 조건 없이 A에 대한 목록을 불러올 때,
목록의 totalCount가 실제 수치와 다른 값으로 반환

예시) 실제 totalCount가 1234일 때,
해당 오류에서는 5 반환(아예 다른 값)


상황 배경

@OneToMany를 이용한 JOIN

// A.java

@Table(name = "table_a")
public class A {

	...
    
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
    @JoinColumn(name = "a_key", nullable = false)
    private List<B> bList = new ArrayList<>();
}

A 엔티티를 SELECT할 때, 동일한 key를 가지는 B 엔티티를 ArrayList 형식으로 불러오기 위해서 @OneToMany 어노테이션을 사용한다.

SELECT * FROM A JOIN B ON A.key = B.a_key

@JoinColumn 어노테이션에서는 name을 통해 SQLON에 사용할 컬럼을 지정할 수 있다.


쿼리 초기화: criteriaBuilder.conjunction()

// Service.java

...

if (searchParams.isEmpty()) {
	return criteriaBuilder.conjunction();
} 

목록을 요청하면 조건에 따라 데이터를 검색하고, 페이지네이션을 통해 검색 결과를 반환한다.
criteriaBuilder.conjunction()을 사용하여 검색 조건이 없을 때 이를 반환하거나, 검색 조건을 추가한 쿼리를 생성한다.


문제의 원인

conjunction을 통한 WHERE 1=1 문제

criteriaBuilder.conjunction()WHERE 1=1을 적용하고, 추가적인 조건을 쿼리에 적용한다.
문제는 검색 조건이 없는 경우에도 WHERE 1=1이 적용되어 COUNT(*)의 결과가 LEFT JOIN의 결과가 아닌 INNER JOIN을 했을 때와 동일한 값이 되었다는 점이다.


해결 방법

항상 LEFT JOIN하기

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
...
private List<B> bList = new ArrayList<>();

기본적으로 @OneToManyLEFT JOIN의 형태이지만, B가 존재하지 않는 경우를 고려하여 @JoinColumnnullable=true를 적용했다.
따라서 B가 존재하지 않으면, B를 검색하는 조건을 사용하지 않는 경우에도 LEFT JOIN을 할 수 있도록 @OneToManyfetch = FetchType.LAZY를 사용한다.

FetchType.LAZY
: 연관된 B를 즉시 가져오지 않고 필요할 때 조회한다.


CriteriaBuilder로 LEFT JOIN하기


Join<A, B> bJoin = root.join("bList", JoinType.LEFT);

CriteriaBuilder로 쿼리를 작성할 때, 항상 LEFT JOIN을 적용할 수 있도록 JoinTypeLEFT로 지정한다.


기존의 해결방법

처음에는 conjunction()WHERE 1=1을 발생시키지 않으려고 조건이 없는 경우에는 criteriaBuilder가 없는 findAll()을 사용하고, 조건이 있는 경우에는 criteriaBuilder를 적용한findAll()을 사용했다.
이후에 LEFT JOIN이 제대로 이루어지지 않아서 COUNT(*) 결과에 오류가 있는 것을 파악하고 문제를 해결할 수 있었다.



마무리

데이터 파이프라인을 구축하고 이를 시각적으로 확인할 수 있도록 돕는 백엔드 API를 구현하면서 Spring Boot를 이용하고 있다.
기능에 대해 구현하고 이를 확장할 때마다 새롭게 모르는 문제와 오류가 발생할 때마다 이를 기록하고 다시 한번 공부할 수 있도록 해야겠다.

0개의 댓글