본문 바로가기

개발

[ORM] N:1을 지향하고, 1:N을 지양해야하는 이유

반응형

안녕하세요 도깨비 개발자입니다.

쿼리를 직접 짜는 비율을 줄여주는 ORM 기술(Spring의 JPA, Node의 TypeOrm등등)은 요즘 많이 쓰입니다. 

엔티티 관점으로 영속성 있는 데이터를 다룰수 있어 저 또한 ORM을 쓰는데요.

많이 사용되는 1:N, N:1 릴레이션을 효과적으로 가져갈 방안을 전달드리려 합니다.


 

@Entity()
class Customer {
  @OneToMany(() => Order, (order) => order.customer)
  orders: Order[];
}


@Entity()
class Order {
  @ManyToOne(() => Customer, (customer) => customer.orders)
  customer: Customer;
}

한 유저는 여러 주문 정보를 가질수 있다로 가정해보겠습니다.

 

쿼리 성능 관점에선  N:1은 참조 대상이 하나여서 일반 조인으로 조회가 가능합니다.

그러나 1:N은 부모(1)에 대한 자식(N)을 가져올때 Join이나 서브 쿼리를 사용해 성능이 저하됩니다.

-- N:1 관계에서는 단순한 JOIN으로 해결 가능
SELECT * FROM orders o JOIN customers c ON o.customer_id = c.id;

-- 1:N 관계에서 고객과 그 고객이 가진 모든 주문을 한 번에 가져오려면
-- 경우에 따라 서브쿼리, GROUP_CONCAT, JSON_AGG 등 별도 처리가 필요

 

1:N에선 부모의 자식 정보 조회시, N+1문제도 발생합니다.

보통 1:N은 Lazy Loading이 기본 값인데요.

leftJoin을 쓰지 않는다면 아래 상황에선 customer 1번, 각 customer에 대응되는 order N번을 호출해야합니다.

const customers = await dataSource.getRepository(Customer).find();
for (const customer of customers) {
  console.log(customer.orders); // 주문 리스트 접근 (Lazy Loading)
}


-- 1. 먼저 고객 목록을 조회 (1개의 쿼리)
SELECT * FROM customer;

-- 2. 각 고객의 주문을 조회하는 쿼리가 반복 실행됨 (N개의 쿼리)
SELECT * FROM orders WHERE customer_id = 1;
SELECT * FROM orders WHERE customer_id = 2;
SELECT * FROM orders WHERE customer_id = 3;
...

 

 

연관 관계 설정의 명확성에도 차이가 납니다.

외래 키(FK)는 보통 자식(N)쪽에 위치합니다. N:1 에서는 N이 1을 참조하므로 FK가 직관적으로 관리됩니다

반대로 1:N은 부모 엔티티에 리스트를 추가해줘야하는데,  별도 설정은 존재하지 않습니다.

@Entity()
class Customer {
  @OneToMany(() => Order, (order) => order.customer)
  orders: Order[];
}


@Entity()
class Order {
  @ManyToOne(() => Customer, (customer) => customer.orders)
  @JoinColumn({ name: 'customer_id' })
  customer: Customer;
}

 

 

그래서 N:1은 기본적으로 사용하고, 1:N은 필요한 경우에 사용하는게 전반적입니다.

다만 저는 엔티티간 릴레이션을 API단에서 모두 표현하는게 직관적이라 생각됩니다.

고객의 주문 정보가 더 많이 쓰이기도 하고, 효율적 접근 가능성을 열어두려면 N:1, 1:N 모두 열어두는게 좋겠죠.

 

성능 문제가 고민되긴 했는데, 다행히 TypeOrm에선 relation 옵션을 제공합니다.

find시 릴레이션 엔티티 조회가 Eager하지 않고, relation 옵션을 사용할때에만 Inner 조회 하도록 해줍니다.

혹은 QueryBuilder의 LeftJoin으로 접근해도 되구요.

 

이상입니다