Django select relate는 왜 one to many 관계의 연관 객체들을 불러오지 못할까?

Hoonkii·2022년 6월 1일
0
post-custom-banner

문득 든 질문...

Django ORM을 사용할 때 쿼리 수를 최적화 할 수 있는 방법으로 select_related() , prefetch_related() 가 있다. select_related() 는 객체가 역참조 하는 단일 객체이거나 또는 정참조하는 관계일 때 DB의 JOIN을 통해 데이터를 가져온다. 반면 prefetch_related() 는 객체가 정참조 혹은 역참조 일때 둘다 사용이 가능하며 WHERE IN ~의 추가 쿼리를 생성하여 데이터를 가져오고 파이썬 단에서 조인을 수행해준다.

타 프레임 워크 중 JPA를 써보신 분들이거나, DB 조인 개념을 이해하고 있는 분들은 select_related()에 대한 의문을 가질 수 있을 것 같다.

데이터베이스에서 외래키가 있다면 양방향으로 조인이 다 가능하다. 그러면 원래는 select_related() {JOIN} 를 통해 one-to-many관계에 있는 객체도 가져올 수 있어야 한다. 근데 Django ORM에서는 수행하지 못하게 설계되어 있다. 그 이유가 무엇일까?

JPA에서 인사이트를 얻다.

나는 내가 공부했던 Hibernate JPA에서 그 설계 의도를 유추해보았다. Hibernate JPA에서는 Fetch Join을 통해 one-to-many관계에 있는 객체들을 가져올 수 있다. 그러나 다음과 같은 경우에서 에러가 생긴다.

@Entity
@Getter
@NoArgsConstructor
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private List<PrivateComment> privateComments = new ArrayList<>();

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private List<PublicComment> publicComments = new ArrayList<>();
}
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(attributePaths = {"privateComments", "publicComments"})
    List<Mother> findAllWithAllCommentsBy();
}

쿼리를 최적화 하기 위해 findAllWithChildrenBy() 함수를 위와 같이 작성하고 실행하면, MultipleBagFetchException이 발생한다.

/**
 * Exception used to indicate that a query is attempting to simultaneously fetch multiple
 * {@link org.hibernate.type.BagType bags}
 */

MultipleBagFetchException 은 여러 BagType을 동시에 Fetch해올 때 발생하는 예외이다. Hibernate에서는 기본적으로 하나의 Bag Type 이상을 JOIN으로 불러오는 것을 허용하지 않는데, 바로 카테시안 곱 때문이다. 카테시안 곱 은 From 절에 2개 이상의 테이블이 있을 때 두 테이블 사이의 유효 조인 조건을 적지 않으면, 테이블에 존재하는 행 갯수를 곱한 만큼의 결과 값이 반환되는 것이다. 따라서 Hibernate는 DB단에서 산출된 데이터를 올바르게 객체로 매핑하는 것이 불가능하다.

Django ORM

Django ORM으로 돌아와보자. Django ORM에서 데이터를 쿼리할 때는 Model Manager를 활용하여 QuerySet을 통해 객체와 연관된 객체들을 가져온다. QuerySet API는 여러 QuerySet의 조건 절들을 체이닝 해서 새로운 QuerySet을 만들도록 설계되어 있다.

예시..

Post.objects.select_related("author").filter(author=author).filter(comment__contains='최고에요')

QuerySet API는 체이닝을 통해 복잡한 연관관계를 가져오고 필터링할 수 있는데, one-to-many관계에 있는 객체들을 가져올 때 select_related()로 어설프게 조인을 부분_ 허용 하여 추후 에러가 발생했을 때 개발자에게 혼란을 주기 보다는, select_related()는 정방향, 혹은 한 개의 연관 객체가 보장된 역방향 객체만 가져오게 하고, 연관 모델이 많을 수 있는 one-to-many의 경우에는 prefetch_related()를 사용하도록 설계한 것 같다.

ORM을 쓴다고 DB를 몰라서는 안된다.

Hibernate JPA를 공부할 때 List를 Set으로 바꾸면 된다~ 이런식으로 해결하는 솔루션을 너무 많이 보았는데, 사실 근본적인 솔루션은 아니라고 생각한다. 카테시안 곱이 왜 일어나는지 카테시안 곱이 발생하면 ORM입장에서 왜 객체로 매핑할 수 없는지를 이해하고 그 상황에서 어떻게 성능을 최적화할 수 있는지를(WHERE IN 절) 이해해야 근본적인 해결이 가능하다.

Django ORM에서는 one-to-many관계에서 JOIN 을 허용하지 않음으로 위와 같은 문제를 해결하였다.

profile
개발 공부 내용 정리
post-custom-banner

0개의 댓글