반응형

N+1 문제와 1+N 문제는 사실상 같은 문제를 다르게 표현한 것입니다.

데이터베이스 성능 최적화에서 가장 흔히 발생하는 문제 중 하나입니다.

 

N+1 문제란?

하나의 메인 쿼리 + N개의 추가 쿼리가 실행되는 문제입니다.

구체적인 예시

시나리오: 사용자 10명과 각자의 게시글을 조회

 
 
sql
-- 1. 첫 번째 쿼리: 사용자 목록 조회
SELECT * FROM users LIMIT 10;

-- 2. 각 사용자마다 개별 쿼리 실행 (N번)
SELECT * FROM posts WHERE user_id = 1;
SELECT * FROM posts WHERE user_id = 2;
SELECT * FROM posts WHERE user_id = 3;
...
SELECT * FROM posts WHERE user_id = 10;

결과: 총 11개의 쿼리 (1 + 10)

코드에서 발생하는 예시

 
 
python
# 문제가 있는 코드
users = User.objects.all()[:10]  # 1번째 쿼리

for user in users:
    posts = user.posts.all()     # 각 사용자마다 추가 쿼리
    print(f"{user.name}: {len(posts)} posts")
 
 
javascript
// JavaScript/Node.js 예시
const users = await User.findAll({ limit: 10 });  // 1번째 쿼리

for (const user of users) {
    const posts = await Post.findAll({             // N번의 추가 쿼리
        where: { userId: user.id }
    });
    console.log(`${user.name}: ${posts.length} posts`);
}

성능에 미치는 영향

 
 
사용자 10명: 11개 쿼리
사용자 100명: 101개 쿼리  
사용자 1000명: 1001개 쿼리

데이터가 증가할수록 쿼리 수가 선형적으로 증가하여 성능이 급격히 저하됩니다.

해결 방법들

1. JOIN 사용 (가장 기본적)

 
 
sql
-- 하나의 쿼리로 해결
SELECT u.*, p.* 
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.id <= 10;

2. Eager Loading

 
 
python
# Django ORM
users = User.objects.prefetch_related('posts')[:10]

# SQLAlchemy
users = session.query(User).options(joinedload(User.posts)).limit(10)
 
 
javascript
// Sequelize
const users = await User.findAll({
    limit: 10,
    include: [{ model: Post, as: 'posts' }]
});

3. 배치 조회

 
 
python
# 두 번의 쿼리로 해결
users = User.objects.all()[:10]
user_ids = [user.id for user in users]
posts = Post.objects.filter(user_id__in=user_ids)

# 메모리에서 매핑
posts_by_user = {}
for post in posts:
    if post.user_id not in posts_by_user:
        posts_by_user[post.user_id] = []
    posts_by_user[post.user_id].append(post)

4. DataLoader 패턴 (GraphQL에서 주로 사용)

 
 
javascript
const DataLoader = require('dataloader');

const postLoader = new DataLoader(async (userIds) => {
    const posts = await Post.findAll({
        where: { userId: userIds }
    });
    
    // userIds 순서대로 정렬하여 반환
    return userIds.map(id => 
        posts.filter(post => post.userId === id)
    );
});

1+N과 N+1의 차이점

표현 방식의 차이일 뿐 본질적으로는 동일합니다:

  • N+1: N개의 데이터를 위해 1개의 메인 쿼리 + N개의 추가 쿼리
  • 1+N: 1개의 메인 쿼리 + N개의 추가 쿼리

둘 다 같은 문제를 설명하며, N+1이 더 일반적으로 사용되는 표현입니다.

실무에서 주의할 점

숨겨진 N+1 문제

 
 
python
# 언뜻 문제없어 보이지만...
users = User.objects.select_related('profile').all()

for user in users:
    # 여기서 추가 쿼리 발생!
    recent_posts = user.posts.filter(created_at__gte=last_week)

ORM의 지연 로딩

대부분의 ORM은 기본적으로 지연 로딩(Lazy Loading)을 사용하므로, 의도치 않게 N+1 문제가 발생할 수 있습니다.

N+1 문제는 애플리케이션 성능에 치명적인 영향을 미칠 수 있으므로, 개발 단계에서부터 쿼리 최적화를 고려하고 데이터베이스 쿼리 로그를 모니터링하는 것이 중요합니다.

반응형

+ Recent posts