N+1 ๋ฌธ์ ์ ํด๊ฒฐ๋ฐฉ๋ฒ
N+1 ๋ฌธ์ ์ ํด๊ฒฐ๋ฐฉ๋ฒ
N+1 ๋ฌธ์ ๋ฅผ ์์๋ณด๊ณ ์ด๋ฅผ ํด๊ฒฐํ ์ ์๋ ๋ฐฉ๋ฒ์ ์์๋ณด์.
ํด๋น ๋ธ๋ก๊ทธ ๊ธ์ ์์
: ์ ํฌ๋ธ ์์
N+1 ๋ฌธ์
Join์ด ์๋ ์๋ฃ ๋ฆฌ์คํธ
{
"isSuccess": true,
"code": 1000,
"message": "์์ฒญ์ ์ฑ๊ณตํ์์ต๋๋ค.",
"result": [
{
"restaurantId": 2,
"thumbnail": "https://user-images.githubusercontent.com/54254402/128039334-e2f7c7be-3caf-41f4-9df0-2605e1b18761.jpg",
"discountValue": null,
"name": "๋งค์ด๊ตญ๋ฌผ๋ก๋ณถ์ด-๊ด์
์ ",
"grade": 4.9,
"reviewCount": 200,
"deliveryTime": 60,
"isExpress": false,
"cesco": true
},
{
"restaurantId": 1,
"thumbnail": "https://user-images.githubusercontent.com/54254402/127957257-639ca25f-aad1-4697-a5e6-e969f0a8caea.jpg",
"discountValue": null,
"name": "๋ฉ๊ฐ์ปคํผ-๋์๋จ์ฑ์ ",
"grade": 4.8,
"reviewCount": 236,
"deliveryTime": 26,
"isExpress": true,
"cesco": false
},
{
"restaurantId": 3,
"thumbnail": "https://user-images.githubusercontent.com/54254402/128040764-069b27d0-f5b2-475b-ae3c-51ef7dbfcbad.jpg",
"discountValue": 1000,
"name": "๋ฎ๋ฐฅ์ด๋",
"grade": 4.9,
"reviewCount": 150,
"deliveryTime": 50,
"isExpress": true,
"cesco": false
},
{
"restaurantId": 4,
"thumbnail": "https://user-images.githubusercontent.com/54254402/128047006-d41002af-1101-40fb-80ca-899f015706da.jpg",
"discountValue": 5000,
"name": "๋ฉ์์นด๋-์ฌ๋น์ ",
"grade": 4.6,
"reviewCount": 400,
"deliveryTime": 80,
"isExpress": false,
"cesco": false
}
]
}
๋ค์๊ณผ ๊ฐ์ ๋ ์คํ ๋์ ๋ฆฌ์คํธ๋ฅผ ์กฐํํ๋ API๋ฅผ ๊ฐ๋ฐํ๋ค๊ณ ์๊ฐํ์.
๋ฐ๋ก ๋ค๋ฅธ ํ ์ด๋ธ์ Join ํ์ง ์์ ๋ฆฌ์คํธ์ด๊ณ ํด๋น API๋ฅผ JPA๋ก ๋ง๋ค์ด ์ฟผ๋ฆฌ๋ฅผ ํ์ธํ์.
๋ค๋ฅธ ํ ์ด๋ธ์ ์กฐ์ธํ์ง ์์๊ธฐ ๋๋ฌธ์ ์ด์ฒ๋ผ ํ๋ฐฉ ์ฟผ๋ฆฌ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์๋ค.
Join์ด ๋ค์ด๊ฐ 1+N ์๋ฃ ๋ฆฌ์คํธ
๋ ์คํ ๋๊ณผ ๋ค๋์ผ ๊ด๊ณ์ธ ๋ ์คํ ๋์ ์ด์์๊ฐ์ ํด๋น ๋ฆฌ์คํธ์์ ํจ๊ป ๋ถ๋ฌ์ฃผ๋๋ก API๋ฅผ ๋ณ๊ฒฝํด ๋ณด์.
public LookupRestaurantRes(Restaurant restaurant) {
this.restaurantId = restaurant.getRestaurantId();
this.thumbnail = restaurant.getThumbnail();
this.discountValue = restaurant.getDiscountValue();
this.name = restaurant.getName();
this.grade = restaurant.getGrade();
this.reviewCount = restaurant.getReviewCount();
this.deliveryTime = restaurant.getDeliveryTime();
this.isExpress = restaurant.getIsExpress();
this.cesco = restaurant.getCesco();
}
๋ ์คํ ๋์ Response DTO์์ ์ด์์๊ฐ ๋ฆฌ์คํธ๋ฅผ ์ถ๊ฐํ๊ณ ๋ค์๊ณผ ๊ฐ์ ์์ฑ์์๋ ๊ทธ ๋ด์ฉ์ ์ถ๊ฐํด ์ค๋ค.
List<HoursRes> hoursList = new ArrayList<>(); // ํ๋ ์ถ๊ฐ
public LookupRestaurantRes(Restaurant restaurant) {
this.restaurantId = restaurant.getRestaurantId();
this.thumbnail = restaurant.getThumbnail();
this.discountValue = restaurant.getDiscountValue();
this.name = restaurant.getName();
this.grade = restaurant.getGrade();
this.reviewCount = restaurant.getReviewCount();
this.deliveryTime = restaurant.getDeliveryTime();
this.isExpress = restaurant.getIsExpress();
this.cesco = restaurant.getCesco();
// ์์ฑ์์๋ HourList ์ถ๊ฐ (Java Stream)
this.hoursList = restaurant.getHoursList().stream().map(HoursRes::new).collect(Collectors.toList());
}
Java์ Stream ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑ์์์ ๋ค์๊ณผ ๊ฐ์ด ์ถ๊ฐํด ์ฃผ์๋ค.
์ฌ๊ธฐ์ HoursRes๋ ์ด์์๊ฐ Entity์ Response DTO ๊ฐ์ฒด์ด๋ค.
{
"isSuccess": true,
"code": 1000,
"message": "์์ฒญ์ ์ฑ๊ณตํ์์ต๋๋ค.",
"result": [
{
"restaurantId": 2,
"thumbnail": "https://user-images.githubusercontent.com/54254402/128039334-e2f7c7be-3caf-41f4-9df0-2605e1b18761.jpg",
"discountValue": null,
"name": "๋งค์ด๊ตญ๋ฌผ๋ก๋ณถ์ด-๊ด์
์ ",
"grade": 4.9,
"reviewCount": 200,
"deliveryTime": 60,
"isExpress": false,
"cesco": true,
"hoursList": [
{
"day": "ํ์ผ",
"startMeridiem": "์ค์ ",
"startHour": 9,
"startMinute": 30,
"endMeridiem": "์คํ",
"endHour": 9,
"endMinute": 30,
"isTommorw": false,
"isSales": true
}
]
},
{
"restaurantId": 1,
"thumbnail": "https://user-images.githubusercontent.com/54254402/127957257-639ca25f-aad1-4697-a5e6-e969f0a8caea.jpg",
"discountValue": null,
"name": "๋ฉ๊ฐ์ปคํผ-๋์๋จ์ฑ์ ",
"grade": 4.8,
"reviewCount": 236,
"deliveryTime": 26,
"isExpress": true,
"cesco": false,
"hoursList": [
{
"day": "๋งค์ผ",
"startMeridiem": "์ค์ ",
"startHour": 10,
"startMinute": 0,
"endMeridiem": "์คํ",
"endHour": 10,
"endMinute": 0,
"isTommorw": false,
"isSales": true
}
]
},
{
"restaurantId": 3,
"thumbnail": "https://user-images.githubusercontent.com/54254402/128040764-069b27d0-f5b2-475b-ae3c-51ef7dbfcbad.jpg",
"discountValue": 1000,
"name": "๋ฎ๋ฐฅ์ด๋",
"grade": 4.9,
"reviewCount": 150,
"deliveryTime": 50,
"isExpress": true,
"cesco": false,
"hoursList": [
{
"day": "์",
"startMeridiem": "์ค์ ",
"startHour": 10,
"startMinute": 0,
"endMeridiem": "์คํ",
"endHour": 10,
"endMinute": 0,
"isTommorw": false,
"isSales": true
},
{
"day": "ํ",
"startMeridiem": "์ค์ ",
"startHour": 10,
"startMinute": 0,
"endMeridiem": "์คํ",
"endHour": 10,
"endMinute": 0,
"isTommorw": false,
"isSales": true
},
{
"day": "์",
"startMeridiem": "์ค์ ",
"startHour": 10,
"startMinute": 0,
"endMeridiem": "์คํ",
"endHour": 10,
"endMinute": 0,
"isTommorw": false,
"isSales": true
}
]
},
{
"restaurantId": 4,
"thumbnail": "https://user-images.githubusercontent.com/54254402/128047006-d41002af-1101-40fb-80ca-899f015706da.jpg",
"discountValue": 5000,
"name": "๋ฉ์์นด๋-์ฌ๋น์ ",
"grade": 4.6,
"reviewCount": 400,
"deliveryTime": 80,
"isExpress": false,
"cesco": false,
"hoursList": [
{
"day": "์ฃผ๋ง",
"startMeridiem": "์ค์ ",
"startHour": 8,
"startMinute": 50,
"endMeridiem": "์คํ",
"endHour": 9,
"endMinute": 0,
"isTommorw": false,
"isSales": true
}
]
}
]
}
๋ฆฌ์คํธ์์ ๋ค๋์ผ ๊ด๊ณ์ธ ์ด์์๊ฐ ์ญ์ ์๋ต๋ฐ์๋ค. ์ฝ์์ ์ฟผ๋ฆฌ๋ฅผ ํ์ธํด ๋ณด์.
์์ ํ๋ฐฉ ์ฟผ๋ฆฌ์ ๋ค๋ฅด๊ฒ ์ฌ๋ฌ ๊ฐ์ ์ฟผ๋ฆฌ๋ฌธ์ด ์๊ฒผ๋ค.
์ด๋ฌํ ์ด์ ๋ ๋งจ ์์ค์์ ์ ์ฒด ๋ ์คํ ๋์ ์ฐพ๋ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฐ ๋ค ๋ค๋์ผ ๊ด๊ณ์ธ ์ด์์๊ฐ๋ง๋ค ํ๋์ ์ฟผ๋ฆฌ๊ฐ ๋ ๋ค์ด๊ฐ๊ธฐ ๋๋ฌธ์ด๋ค.
์ด๋ฌํ ๋ฌธ์ ๋ฅผ N+1 ๋ฌธ์
๋ผ๊ณ ํ๋ค.
ํด๊ฒฐ๋ฒ
์ฐ์ ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ๋ชจ๋ ์นผ๋ผ์ fetch ์ ๋ต์ LAZY
๋ก ์ค์ ํด ์ฃผ์ด์ผ ํ๋ค.
~ToOne ์ด๋
ธํ
์ด์
(ex. @ManyToOne)์์ EAGER
์ ๋ต์ด Default์์ผ๋ก ์์ ํด ์ฃผ์.
~ToMany ์ด๋
ธํ
์ด์
์์ (ex. @OneToMany)์์ LAZY
์ ๋ต์ด Defalult์์ผ๋ก ๋ฐ๋ก ์์ ํด ์ค ํ์๊ฐ ์๋ค.
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "restaurantId")
private Restaurant restaurant;
์ด์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํด ์ฃผ์.
JOIN FETCH
์ฒซ ๋ฒ์งธ ๋ฐฉ์์ FETCH JOIN์ ํ์ฉํ ํด๊ฒฐ ๋ฐฉ๋ฒ์ด๋ค.
List<LookupRestaurantRes> findAllByStatusAndGeneralAddressOrderByUpdatedAtDesc(Status status, String generalAddress);
๋ค์๊ณผ ๊ฐ์ JPA์ ์ฟผ๋ฆฌ ๋ฉ์๋์ JPQL ์ฟผ๋ฆฌ๋ฌธ์ ์ถ๊ฐํ์.
@Query(
"SELECT r FROM Restaurant r " +
"JOIN FETCH r.hoursList " +
"WHERE (r.status = :status AND r.generalAddress = :generalAddress)"
)
List<LookupRestaurantRes> findAllByStatusAndGeneralAddressOrderByUpdatedAtDesc(Status status, String generalAddress);
WHERE ์ ์ ํด๋น ์ดํ๋ฆฌ์ผ์ด์
์ ๊ด๋ จ๋ ๋ด์ฉ์ด๊ณ , JOIN FETCH ๋ถ๋ถ์ ์ฃผ๋ชฉํ์.
FROM ์ ์ ์ ์ธํ ๋ ์คํ ๋์ผ๋ก hourList๋ฅผ JOIN FETCH๋ฅผ ์ฐ๊ฒฐํด ์ค๋ค.
๊ฒฐ๊ณผ
ํด๋น JPQL ์ฟผ๋ฆฌ๋ฅผ ์ ์ธ ํ ๋ค์ API ํธ์ถ์ ํ๋ฉด ํ๋ฐฉ ์ฟผ๋ฆฌ๋ก ๋์ผํ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
JOIN FETCH์ ํ๊ณ
JOIN FETCH์ ๋ค์๊ณผ ๊ฐ์ ํ๊ณ๊ฐ ์๋ค.
- ์ฟผ๋ฆฌ ๋ฉ์๋์ ๋ฆฌํด ํ์ ์ Page ์ผ ๋ ์ผ๋๋ค์ธ ์ปฌ๋์ ์ JOIN FETCH ์ ์ ์ ์ธํ ์ ์๋ค.
- ์์ ์์์์ ๋ ์คํ ๋์ ๊ธฐ์ค์ผ๋ก ๋ค๋์ผ, ์ผ๋์ผ์ ์๋ฃํ์ JOIN FETCH ์ ์ ์ ํ ์์ด ์ ์ธํ ์ ์์ง๋ง, ์ผ๋๋ค์ ์๋ฃํ์ ๋ ๊ฐ ์ด์ ์ ์ธํ ์ ์๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด @BatchSize๋ฅผ ์ฌ์ฉํ๋ค.
Batch Size
์์์ ์์ ํ๋ ์ฟผ๋ฆฌ๋ฉ์๋๋ฅผ ๋ค์ ๋๋๋ฆฐ ๋ค, ๋ ์คํ ๋์ Entity์ ์ด์์๊ฐ List์ ๋ค์๊ณผ ๊ฐ์ ์ด๋ ธํ ์ด์ ์ ์ ์ธํ๋ค.
@BatchSize(size = 100)
@OneToMany(mappedBy = "restaurant")
List<Hours> hoursList = new ArrayList<>();
@BatchSize ์ด๋
ธํ
์ด์
์ ์ฌ์ด์ฆ๋ฅผ ๋ช
์ํ์ฌ ์ ์ธํ๋ฉด, ์ฟผ๋ฆฌ๋ฌธ์ ์คํํ ๋ size ํฌ๊ธฐ๋งํผ์ in ์ฐ์ฐ์ ํตํด ์ฒ๋ฆฌํ๋ค.
์ ์ ํ size๋ฅผ ๊ฐ ์นผ๋ผ๋ง๋ค ๋ช
์ํ ์ ์๊ณ application.property์ ๊ฐ์ ์ค์ ํ์ผ์ ์ ์ฒด์ ์ผ๋ก BatchSize๋ฅผ ์ ์ธํ ์ ์๋ค.
๊ฒฐ๊ณผ
ํด๋น ์ด๋
ธํ
์ด์
์ผ๋ก ์ ์ธ ํ ๋ค์ API ํธ์ถ์ ํ๋ฉด ํ๋ฐฉ ์ฟผ๋ฆฌ๋ ์๋์ง๋ง ํจ์ฌ ์ ์ด์ง ์ฟผ๋ฆฌ์ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ฟผ๋ฆฌ ๋ฌธ์ ๋ง์ง๋ง ์ค์ ํ์ธํ๋ฉด in
์ด ๋ค์ด๊ฐ ๊ฒ์ ํ์ธํ ์ ์์ ๊ฒ์ด๋ค.
Batch Size๋ Page ํ์ ๊ณผ ์ฌ์ฉํ ์ ์๋ ์ฅ์ ๊ณผ, 2๊ฐ ์ด์์ ์ผ๋๋ค ์ฟผ๋ฆฌ๋ฅผ ๋ง๋ค์ด์ผ ํ๋ ๋ฌธ์ ๋ฅผ ์ฃผ์ ์ปฌ๋ ์ ์ JOIN FETCH๋ฅผ ์ฌ์ฉํ๊ณ ๋๋จธ์ง ์ปฌ๋ ์ ์ @BatchSize ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ์ฌ ํด๊ฒฐํ ์ ์๋ค.