티스토리 뷰
spring 에서 JPA으로 개발하다보면 누구나 N+1 을 만나게 되어 있다.
사실 Spring에만 있는 것은 아니고 대부분의 ORM에 다 있는 문제이다.
관련해서 키워드로 나오는게
1. JOIN FETCH
2. Batch Size
3. Eager Loading , Lazy Loading
등이 있다.
물론 해결책을 아는 것도 좋지만 본질을 이해하지못하면 택도 없는 요구사항을 받았다가,
나중에 눈물을 흘리며 기획 수정을 요청해야 하는 경우가 생길 수 있다.
N+1의 시작은 데이터를 표현하는 방법에 있어서의 RDBMS와 OOP의 차이에서 기인한다.
RDBMS 세계관에서는 각 사물을 테이블로 정의하고,
각 사물에 대한 고유성은 PK로 확보하며,
개별 사물은 레코드로 표현한다.
그리고 사물간의 관계는 FK를 사용해 정의한다.
OOP 세계관은 사물을 클래스로 정의한다.
각 사물에 대한 고유성은 기본적으론 객체 주소를 활용한다.(equals와 hashcode 등을 활용하기도 한다)
개별 사물은 객체(Object)로 표현한다.
그리고 사물간의 관계는 "상속"과 "포함"으로 정의한다.
N+1과 무관한 상속에 대한 논의는 패싱하고,
포함에 대해 조금 얘기를 해봐야 한다.
"포함"과 FK로 데이터를 표현한다는 컨셉은 유사해보이지만,
실제 나오는 결과물은 차이가 크다.
바로 예제로 들어가겠다.
Order와 Customer 를 정의한다고 가정해보자.
RDBMS 세계관에서는 아래와 같이 정의할 것이다.
CREATE TABLE Customer (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20),
address TEXT
);
CREATE TABLE `Order` (
id INT PRIMARY KEY AUTO_INCREMENT,
order_date DATE NOT NULL,
status VARCHAR(50) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
customer_id INT,
FOREIGN KEY (customer_id) REFERENCES Customer(id)
);
OOP 세계관에서는 아래처럼 정의할 것이다.
class Customer(
val id: Int,
val name: String,
val email: String,
var phone: String?,
var address: String?
) {
val orders = mutableListOf<Order>()
fun addOrder(order: Order) {
orders.add(order)
order.customer = this
}
}
class Customer(
val id: Int,
val name: String,
val email: String,
var phone: String?,
var address: String?
) {
val orders = mutableListOf<Order>()
fun addOrder(order: Order) {
orders.add(order)
order.customer = this
}
}
이렇게 보면 뭔 차이인지 모를 것이다.
데이터를 추가하고 select 쿼리를 넣어보자.
INSERT INTO Customer (id, name, email, phone, address) VALUES
(1, 'Alice', 'alice@example.com', '123-456-7890', '123 Maple Street'),
(2, 'Bob', 'bob@example.com', '098-765-4321', '456 Oak Avenue'),
(3, 'Charlie', 'charlie@example.com', '555-123-4567', '789 Pine Road');
INSERT INTO `Order` (id, order_date, status, total_amount, customer_id) VALUES
(1, '2024-11-01', 'Completed', 150.50, 1),
(2, '2024-11-02', 'Pending', 75.25, 1),
(4, '2024-11-04', 'Pending', 300.00, 2),
(6, '2024-11-06', 'Pending', 100.25, 3);
SELECT
c.id AS customer_id,
c.name AS customer_name,
c.email AS customer_email,
o.id AS order_id,
o.order_date,
o.status,
o.total_amount
FROM
Customer c
LEFT JOIN
`Order` o ON c.id = o.customer_id
ORDER BY
c.id, o.order_date;
SELECT 쿼리의 결과를 조회해보자.
레코드가 4개가 나오고 있는 것을 볼 수 있다.
반면 객체지향 프로그래밍에서는 아래처럼 나온다.
import java.math.BigDecimal
import java.time.LocalDate
// Order 클래스
class Order(
val id: Int,
val orderDate: LocalDate,
var status: String,
var totalAmount: BigDecimal,
var customer: Customer? = null // Customer와의 관계를 표현
) {
override fun toString(): String {
return "Order(id=$id, orderDate=$orderDate, status='$status', totalAmount=$totalAmount)"
}
}
// Customer 클래스
class Customer(
val id: Int,
val name: String,
val email: String,
var phone: String?,
var address: String?
) {
// 여러 Order를 가지는 OneToMany 관계
val orders = mutableListOf<Order>()
// 주문 추가 메서드
fun addOrder(order: Order) {
orders.add(order)
order.customer = this // 양방향 관계 설정
}
override fun toString(): String {
return "Customer(id=$id, name='$name', email='$email', orders=$orders)"
}
}
// 객체 생성 및 데이터 삽입
fun main() {
// Customer 생성
val customer1 = Customer(1, "Alice", "alice@example.com", "123-456-7890", "123 Maple Street")
val customer2 = Customer(2, "Bob", "bob@example.com", "098-765-4321", "456 Oak Avenue")
val customer3 = Customer(3, "Charlie", "charlie@example.com", "555-123-4567", "789 Pine Road")
// Order 생성 및 할당
customer1.addOrder(Order(1, LocalDate.of(2024, 11, 1), "Completed", BigDecimal("150.50")))
customer1.addOrder(Order(2, LocalDate.of(2024, 11, 2), "Pending", BigDecimal("75.25")))
customer2.addOrder(Order(4, LocalDate.of(2024, 11, 4), "Pending", BigDecimal("300.00")))
customer3.addOrder(Order(6, LocalDate.of(2024, 11, 6), "Pending", BigDecimal("100.25")))
// 결과 출력
val customers = listOf(customer1, customer2, customer3)
customers.forEach { println(it) }
}
차이가 뭔지 알겠는가?
리스트의 크기가 3개이다.
즉, ORM에서는 Object와 RDB를 매핑해줘야 하는데, 표현하고자 하는 데이터 결과의 개수부터가 문제가 된다.
이렇게 되면 페이징처리를 해야 하는 상황에서 데이터 개수가 안맞는 문제가 발생한다.
사내 백오피스를 구현해야 하는데, 고객목록을 보는 페이지가 있다고 가정해보자.
그리고 고객의 주문들을 목록에서 같이 보고 싶다고 가정해보자.
그런데, 한 페이지엔 3개만 있어야 한다고 한다.
쿼리는 아래처럼 생성해야 한다.
SELECT
c.id AS customer_id,
c.name AS customer_name,
c.email AS customer_email,
o.id AS order_id,
o.order_date,
o.status,
o.total_amount
FROM
Customer c
LEFT JOIN
`Order` o ON c.id = o.customer_id
ORDER BY
c.id, o.order_date
LIMIT 3 OFFSET 0;
결과는 아래처럼 나온다.
객체지향적으로 표현하게 되는 경우, 아래와 같이 표현된다.
분명히 고객 3명의 데이터를 요구했지만 리턴되는 고객 목록은 2명이다.
// Customer 생성
val customer1 = Customer(1, "Alice", "alice@example.com", "123-456-7890", "123 Maple Street")
val customer2 = Customer(2, "Bob", "bob@example.com", "098-765-4321", "456 Oak Avenue")
// Order 생성 및 할당
customer1.addOrder(Order(1, LocalDate.of(2024, 11, 1), "Completed", BigDecimal("150.50")))
customer1.addOrder(Order(2, LocalDate.of(2024, 11, 2), "Pending", BigDecimal("75.25")))
customer2.addOrder(Order(4, LocalDate.of(2024, 11, 4), "Pending", BigDecimal("300.00")))
// 결과 출력
val customers = listOf(customer1, customer2)
customers.forEach { println(it) }
3명분의 데이터를 요구했지만 2명분만 받는 기현상이 나타나는 것이다.
보통은 얼렁뚱땅 넘어가지만, count 쿼리나 pagination을 사용할 때 문제가 된다.
결국 JOIN으로 한방에 데이터를 넘기기보다는 N+1으로, 나눠서 쿼리를 나눔으로써 데이터 중복을 피하는 방법을 택했다고 할 수 있다.
요약하자면 아래와 같다.
- RDBMS와 OOP는 데이터를 표현하는 방법에 차이가 있다.
- ORM에서 매핑할 때 이슈가 되는 부분이 있다.
- 이걸 N+1으로 넘겼다.
다음 포스팅에선 Customer와 Order의 예제를 가지고,
실제로 어떤 구현이 불가능한지, 어떻게 파훼해야하는지를 살펴보도록 하겠다.
'개발 > 스프링' 카테고리의 다른 글
JPA에 대한 단상(2) - 백오피스에서의 OneToMany (1) | 2024.11.10 |
---|---|
spring boot 3.2.2 전환기(5) - pom.xml 추가수정 (0) | 2024.04.13 |
spring boot 3.2.2 전환기(4) - 타임리프 변경사항 (0) | 2024.03.25 |
spring boot 3.2.2 전환기(3)- Spring Security 변경 (0) | 2024.03.25 |
spring boot 3.2.2 전환기(2) - Spring Web MVC 변경 (0) | 2024.03.23 |
- Total
- Today
- Yesterday
- 티스토리챌린지
- Request
- 부가가치세
- 오블완
- 홈택스
- Thymeleaf
- k베뉴
- 한국교통안전공단
- 광군제
- n+1
- JPA
- java
- 알리익스프레스
- 알리
- Spring
- Azure
- 전세사기
- ORM
- 포상금
- 이륜차
- 현금영수증
- ouath2
- springboot
- 토스페이
- 안전신문고
- 탈세
- springboot3
- Java17
- tomcat
- 공익제보단
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |