๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

JPA

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 ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค.