
1:N 관계를 가지는 A와 B 엔티티의 데이터를 불러오는 API를 작성했는데, COUNT(*) 과정에서 실제와 다른 수치가 반환되는 문제를 발견했다.
확인 결과, COUNT(*)가 A LEFT JOIN B의 값 대신 A INNER JOIN B의 값을 반환하는 상태로 확인되었다.
엔티티 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 반환(아예 다른 값)
// 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을 통해 SQL의 ON에 사용할 컬럼을 지정할 수 있다.
// Service.java
...
if (searchParams.isEmpty()) {
return criteriaBuilder.conjunction();
}
목록을 요청하면 조건에 따라 데이터를 검색하고, 페이지네이션을 통해 검색 결과를 반환한다.
criteriaBuilder.conjunction()을 사용하여 검색 조건이 없을 때 이를 반환하거나, 검색 조건을 추가한 쿼리를 생성한다.

criteriaBuilder.conjunction()은 WHERE 1=1을 적용하고, 추가적인 조건을 쿼리에 적용한다.
문제는 검색 조건이 없는 경우에도 WHERE 1=1이 적용되어 COUNT(*)의 결과가 LEFT JOIN의 결과가 아닌 INNER JOIN을 했을 때와 동일한 값이 되었다는 점이다.
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
...
private List<B> bList = new ArrayList<>();
기본적으로 @OneToMany는 LEFT JOIN의 형태이지만, B가 존재하지 않는 경우를 고려하여 @JoinColumn에 nullable=true를 적용했다.
따라서 B가 존재하지 않으면, B를 검색하는 조건을 사용하지 않는 경우에도 LEFT JOIN을 할 수 있도록 @OneToMany에 fetch = FetchType.LAZY를 사용한다.
FetchType.LAZY
: 연관된 B를 즉시 가져오지 않고 필요할 때 조회한다.
Join<A, B> bJoin = root.join("bList", JoinType.LEFT);
CriteriaBuilder로 쿼리를 작성할 때, 항상 LEFT JOIN을 적용할 수 있도록 JoinType을 LEFT로 지정한다.
처음에는 conjunction()의 WHERE 1=1을 발생시키지 않으려고 조건이 없는 경우에는 criteriaBuilder가 없는 findAll()을 사용하고, 조건이 있는 경우에는 criteriaBuilder를 적용한findAll()을 사용했다.
이후에 LEFT JOIN이 제대로 이루어지지 않아서 COUNT(*) 결과에 오류가 있는 것을 파악하고 문제를 해결할 수 있었다.
데이터 파이프라인을 구축하고 이를 시각적으로 확인할 수 있도록 돕는 백엔드 API를 구현하면서 Spring Boot를 이용하고 있다.
기능에 대해 구현하고 이를 확장할 때마다 새롭게 모르는 문제와 오류가 발생할 때마다 이를 기록하고 다시 한번 공부할 수 있도록 해야겠다.