Java

[Java] ORM์˜ N+1 ๋ฌธ์ œ

Castle Bird 2026. 1. 13. 18:22

๐Ÿ“ N+1 ๊ตฌํ˜„ ์ž๋ฃŒ GitHub

 

GitHub - castle-bird/castle-bird-lab: ๋งˆ์ฃผํ•œ ์ด์Šˆ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ณ  ๊ธฐ๋กํ•˜๋Š” ๊ณณ

๋งˆ์ฃผํ•œ ์ด์Šˆ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ณ  ๊ธฐ๋กํ•˜๋Š” ๊ณณ. Contribute to castle-bird/castle-bird-lab development by creating an account on GitHub.

github.com

 


1. N+1์ด๋ž€?

  • JPA์™€ ๊ฐ™์€ ORM(๊ฐ์ฒด ๊ด€๊ณ„ ๋งคํ•‘) ๊ธฐ์ˆ ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์„ฑ๋Šฅ ๋ฌธ์ œ
  • ํ•˜๋‚˜์˜ ์ฟผ๋ฆฌ(1)๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๋•Œ ์—ฐ๊ด€๋œ ๊ฐ์ฒด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์œ„ํ•ด N๊ฐœ์˜ ์ถ”๊ฐ€ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐ˜๋ณต์ ์œผ๋กœ ์‹คํ–‰
    • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ถ€ํ•˜์™€ ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์•ผ๊ธฐ โญ
  • @ManyToOne(fetch = FetchType.LAZY)์˜ ๊ฒŒ์œผ๋ฅธ ๋กœ๋”ฉ์‹œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ.

2. N+1์˜ ์›์ธ ๋ฐ ์ฝ”๋“œ

์—ฐ๊ด€๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ ์ง€์—ฐ ๋กœ๋”ฉ(Lazy Loading) ์ „๋žต์œผ๋กœ ์ธํ•ด, ์ฒซ ์ฟผ๋ฆฌ ์ดํ›„ ๊ฐ ๋ฐ์ดํ„ฐ๋งˆ๋‹ค ์ถ”๊ฐ€ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒ.

๐Ÿ“๋ฐœ์ƒ ์ฝ”๋“œ (์ƒ๋‹จ GitHub์—์„œ ์ž์„ธํžˆ ํ™•์ธ ๊ฐ€๋Šฅ โญ)

@Entity
@Table(name = "tbl_food")
public class Food {
    // ... ํ•„๋“œ

    // ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ ์ฐธ์กฐ ์ค‘. (Foreign Key)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;
}
@Entity
@Table(name = "tbl_category")
public class Category {
    // ... ํ•„๋“œ

    // ์ž์‹ ์—”ํ‹ฐํ‹ฐ์— ์ฐธ์กฐ ๋˜๋Š” ์ค‘.
    @OneToMany(mappedBy = "category")
    private List<Food> foods;
}
@RequiredArgsConstructor
@Service
public class MenuService {

    private final FoodRepository foodRepository;

    // ========== ์„œ๋น„์Šค ๋กœ์ง ==========
    public List<FoodDTO> findAll() {
        // 1. ์ด๋•Œ ๊นŒ์ง„ ์นดํ…Œ๊ณ ๋ฆฌ ๋‚ด์šฉ์ด ์—†๋Š” ์ˆœ์ˆ˜ Food์˜ ๋‚ด์šฉ๋งŒ ๋‹ด๊ฒจ์žˆ์Œ
        List<Food> foods = foodRepository.findAll();

        // 2. ๊ทธ๋Ÿฌ๋‚˜ DTO๋ณ€ํ™˜์‹œ convertToDTO๋‚ด๋ถ€ ์ฝ”๋“œ๋กœ์ธํ•ด N + 1 ๋ฐœ์ƒ ์ค‘!
        return foods.stream()
                .map(this::convertToDTO)
                .toList();
    }

    // ========== ์—”ํ‹ฐํ‹ฐ -> DTO ==========
    public FoodDTO convertToDTO(Food food) {

        // 3. CategoryId, CategoryName์— ์ ‘๊ทผ ์•ˆํ•œ์ฑ„ DTO ๋ณ€ํ™˜ ์‹œ N+1 ๋ฐœ์ƒ ์•ˆํ•จ
        FoodDTO foodDTO = FoodDTO.builder()
                .id(food.getId())
                .foodName(food.getFoodName())
                .price(food.getPrice())
                .build();

        // 4. ์ด๋•Œ ๋ฌธ์ œ ๋ฐœ์ƒ.
        //  findAll()์˜ 1๋ฒˆ์—์„œ ํ™•์ธํ–ˆ๋“ฏ์ด ์ตœ์ดˆ์—๋Š” Category๋ฅผ ์กฐํšŒํ•˜์ง€ ์•Š์•˜์Œ.
        //  ๊ทธ๋Ÿฌ๋‚˜ ํ˜„์žฌ getCategory()์— ์ ‘๊ธ‰ํ•˜๋Š” ์ˆœ๊ฐ„ ์—†๋˜ Category๋ฅผ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒ
        if (food.getCategory() != null) {
            foodDTO.setCategoryId(food.getCategory().getId());
            foodDTO.setCategoryName(food.getCategory().getCategoryName());
        }

        return foodDTO;
    }
}

 

์œ„ ์ฝ”๋“œ ์ฒ˜๋Ÿผ N+1์ด ๋ฐœ์ƒ ํ–ˆ์„๋•Œ ์•„๋ž˜ ์ด๋ฏธ์ง€์™€ ๊ฐ™์ด ์ตœ์ดˆ ์กฐํšŒ ์ดํ›„์—๋„ ์—ฌ๋Ÿฌ๋ฒˆ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Œ.

N+1 ๋ฐœ์ƒ


3. N+1์˜ ํ•ด๊ฒฐ๋ฒ•

3-1. Fetch Join

  • "๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฐฉ์— ๊ฐ€์ ธ์˜ค๊ธฐ"
  • JPQL์„ ์‚ฌ์šฉํ•˜์—ฌ ์กฐํšŒ ์‹œ์ ์— ์—ฐ๊ด€๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ•์ œ๋กœ ์กฐ์ธ(Join) ํ•˜์—ฌ ๋‹จ ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ๊ฐ€์ ธ์˜จ๋‹ค.
  • INNER JOIN ๊ธฐ์ค€์œผ๋กœ ๊ฐ€์ ธ์˜ด โญ
// JPQL 
@Query("SELECT f FROM Food f JOIN FETCH f.category")
List<Food> findAllJPQL();

JPQL๋กœ N + 1 ํ•ด๊ฒฐ

 

3-2. EntityGraph

  • "๊ฐ€์ ธ์˜ฌ ๋ชฉ๋ก์„ ๋ฏธ๋ฆฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ ์ „๋‹ฌํ•˜๊ธฐ"
  • JPQL์„ ์ง์ ‘ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ๋ฒˆ๊ฑฐ๋กœ์šธ ๋•Œ ์‚ฌ์šฉํ•˜๋Š”, JPA ํ‘œ์ค€ ์ŠคํŽ™์ธ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ.
  • LEFT JOIN ๊ธฐ์ค€์œผ๋กœ ๊ฐ€์ ธ์˜ด โญ
// @EntityGraph
@Override
@EntityGraph(attributePaths = {"category"})
List<Food> findAll();

@EntityGraph๋กœ N+1ํ•ด๊ฒฐ

 

3-3. Batch Size

  • "๋ฐ์ดํ„ฐ๋ฅผ ๋ฌถ์Œ ๋ฐฐ์†ก์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ"
  • ์œ„์˜ ๋‘ ๋ฐฉ๋ฒ•์ด ์ฟผ๋ฆฌ๋ฅผ '1๋ฒˆ'์œผ๋กœ ์ค„์ด๋Š” ๋ฐฉ๋ฒ•์ด๋ผ๋ฉด, Batch Size๋Š” N๋ฒˆ์˜ ์ฟผ๋ฆฌ๋ฅผ ๋งค์šฐ ๋งŽ์ด ์ค„์ด๋Š” ๋ฐฉ์‹.
  • ์—ฐ๊ด€๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ IN ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„ค์ •ํ•œ ๊ฐœ์ˆ˜๋งŒํผ ๋ชจ์•„์„œ ์กฐํšŒ.
  • ๋งŒ์•ฝ Batch Size๋ฅผ 100์œผ๋กœ ์„ค์ •ํ•œ๋‹ค๋ฉด ์—ฐ๊ด€๋œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ฟผ๋ฆฌ์‹œ(N๋ฒˆ ์กฐํšŒ), 100๊ฐœ์”ฉ ๋ฌถ์Œ์œผ๋กœ ์กฐํšŒ.
// application.yml
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

Batch Size๋กœ N+1ํ•ด๊ฒฐ


4. N+1์€ ๋ฌด์กฐ๊ฑด ์—†์• ์•ผ ํ•˜๋Š” ๊ณผ์ œ์ผ๊นŒ?

N+1 ๋ฌธ์ œ๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ๋Š” @ManyToOne(fetch = FetchType.LAZY) ์„ค์ •์ด ๋ฌด์กฐ๊ฑด ๋‚˜์œ ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.
ํ•ต์‹ฌ์€ ๊ธฐ๋Šฅ์˜ ๊ตฌํ˜„๊ณผ ์„ฑ๋Šฅ์˜ ์ตœ์ ํ™” ์‚ฌ์ด์—์„œ ์ ์ ˆํ•œ ํ•ฉ์˜์ ์„ ์ฐพ๋Š” ๊ฒƒ์ด๋‹ค.

1) ์™œ ์ง€์—ฐ ๋กœ๋”ฉ(LAZY)์„ ๊ธฐ๋ณธ์œผ๋กœ ์“ฐ๋Š”๊ฐ€?

  • ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„ ๋ฐฉ์ง€: ์‚ฌ์šฉ์ž๊ฐ€ ํ™•์ธํ•˜์ง€๋„ ์•Š์„ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ํ•œ๊บผ๋ฒˆ์— ์กฐํšŒํ•˜๋Š” ๊ฒƒ์€ DB์™€ ๋ฉ”๋ชจ๋ฆฌ์— ํฐ ๋ถ€๋‹ด.
  • ์˜ˆ์ธก ๊ฐ€๋Šฅ์„ฑ: ์ฆ‰์‹œ ๋กœ๋”ฉ(EAGER)์€ ์–ด๋””์„œ ์–ด๋–ค ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐˆ์ง€ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ค์–ด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฐœ์ƒ.

2) ๊ธฐ์ˆ ๋ณด๋‹ค ์ค‘์š”ํ•œ ๊ฒƒ์€ '์‚ฌ์šฉ์ž ๋ถ„์„'

๊ฒฐ๊ตญ ์–ด๋–ค ๋กœ๋”ฉ ๋ฐฉ์‹์„ ์„ ํƒํ• ์ง€๋Š” ์‚ฌ์šฉ์ž์˜ ์„œ๋น„์Šค ์ด์šฉ ํŒจํ„ด์ด ์ค‘์š”

  • ์ƒ์„ธ ๋ณด๊ธฐ ์ง„์ž…๋ฅ ์ด ๋†’๋‹ค๋ฉด? ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ํšจ์œจ์ .
  • ๋ชฉ๋ก๋งŒ ํ›‘์–ด๋ณด๋Š” ์œ ์ €๊ฐ€ ๋งŽ๋‹ค๋ฉด? ํ•„์š”ํ•  ๋•Œ๋งŒ ๊ฐ€์ ธ์˜ค๋Š” ์ง€์—ฐ ๋กœ๋”ฉ์ด ์œ ๋ฆฌ.

3) ๊ฒฐ๋ก : ๋ฌด์กฐ๊ฑด์ ์ธ ์ œ๊ฑฐ๋ณด๋‹ค๋Š” '์„ ๋ณ„์  ์ตœ์ ํ™”'

์‹ค์ œ ์‚ฌ์šฉ์ž ๋ถ„์„์„ ํ†ตํ•ด ์„ฑ๋Šฅ ๋ณ‘๋ชฉ์ด ๋ฐœ์ƒํ•˜๋Š” ํŠน์ • ์ง€์ ์—์„œ๋งŒ Fetch Join ๋“ฑ์„ ํ™œ์šฉํ•ด N+1์„ ํ•ด๊ฒฐํ•˜๋Š” ์ „๋žต์„ ์ทจํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ๋ฐ”๋žŒ์งํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“
N+1 ํ•ด๊ฒฐ์€ ๊ธฐ์ˆ ์ ์ธ '์ •๋‹ต'์„ ๋งžํžˆ๋Š” ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ, 
๋น„์ฆˆ๋‹ˆ์Šค ์ƒํ™ฉ์— ๋งž์ถฐ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„(Trade-off)๋ฅผ ์กฐ์ ˆํ•˜๋Š” ๊ณผ์ •.