오늘은 데이터베이스 성능 최적화의 핵심인 '인덱스'에 대해 알아보고, NestJS 프로젝트에서 어떻게 활용할 수 있는지 함께 살펴보려고 합니다.
인덱스란 무엇인가요?
인덱스는 책의 색인과 정확히 같은 개념입니다. 두꺼운 책에서 특정 주제를 찾으려고 할 때, 처음부터 끝까지 모든 페이지를 넘기면서 찾나요? 아니죠! 책 뒤쪽의 색인 페이지를 보고 바로 원하는 페이지로 이동합니다. 데이터베이스 인덱스도 똑같은 역할을 합니다.
인덱스가 없으면 어떻게 될까요?
-- 인덱스 없이 사용자 찾기
SELECT * FROM users WHERE email = 'user@example.com';
만약 인덱스가 없다면, 데이터베이스는 테이블에 있는 모든 행을 하나씩 확인해야 합니다. 사용자가 10,000명이라면? 최악의 경우 10,000번의 비교 작업이 필요하죠.
인덱스의 마법
-- 인덱스 있는 상태에서 사용자 찾기
SELECT * FROM users WHERE email = 'user@example.com';
email 필드에 인덱스가 설정되어 있다면, 데이터베이스는 특별한 데이터 구조(대부분 B-트리)를 사용해 훨씬 효율적으로 검색합니다. 10,000명의 사용자 데이터에서는 약 14번 정도의 비교만으로 원하는 정보를 찾을 수 있어요. 이는 인덱스가 없을 때보다 수백 배 빠른 속도입니다!
NestJS에서 인덱스 설정하기
NestJS와 TypeORM을 사용하면 인덱스 설정이 정말 간단해집니다. 몇 가지 데코레이터만으로 강력한 인덱스 기능을 활용할 수 있어요.
기본 인덱스 설정
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
@Index() // 이메일에 인덱스 추가
email: string;
@Column()
name: string;
@Column()
age: number;
}
이렇게 하면 email 필드에 인덱스가 생성되고, 해당 필드로 검색할 때 성능이 크게 향상됩니다.
복합 인덱스 설정
여러 필드를 함께 검색하는 경우가 많다면 복합 인덱스를 설정하는 것이 좋습니다.
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
@Entity('products')
@Index(['category', 'brand']) // 카테고리와 브랜드로 구성된 복합 인덱스
export class Product {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
category: string;
@Column()
brand: string;
@Column()
price: number;
}
이제 category와 brand를 함께 사용한 검색이 빨라집니다. 예를 들어 '전자제품' 카테고리의 '삼성' 브랜드 제품을 검색할 때 효과적이죠.
유니크 인덱스 설정
중복을 허용하지 않는 필드에는 유니크 인덱스를 설정할 수 있습니다.
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column()
@Index({ unique: true }) // 주문 번호에 유니크 인덱스 추가
orderNumber: string;
@Column()
customerName: string;
@Column('decimal')
totalAmount: number;
}
또는 간단히 아래와 같이 작성할 수도 있습니다.
@Column({ unique: true })
orderNumber: string;
실제 사용 예시와 성능 차이
블로그 시스템을 예로 들어보겠습니다.
// 블로그 게시물 엔티티
@Entity('blog_posts')
export class BlogPost {
@PrimaryGeneratedColumn()
id: number;
@Column()
@Index()
slug: string; // URL에 사용되는 식별자
@Column()
title: string;
@Column('text')
content: string;
@Column()
@Index()
authorId: number; // 작성자 ID
@CreateDateColumn()
createdAt: Date;
}
// 댓글 엔티티
@Entity('comments')
export class Comment {
@PrimaryGeneratedColumn()
id: number;
@Column()
@Index() // 게시물 ID에 인덱스 추가
postId: number;
@Column()
content: string;
@Column()
authorName: string;
@CreateDateColumn()
createdAt: Date;
}
실제 성능 차이는?
인덱스 없이 특정 게시물의 모든 댓글을 검색한다고 가정해 봅시다:
-- 특정 게시물의 모든 댓글 검색 (인덱스 없음)
SELECT * FROM comments WHERE postId = 123;
만약 댓글이 10,000개 있다면, 이 쿼리는 매우 느릴 수 있습니다. 제
반면 인덱스가 있는 경우:
-- 인덱스가 있는 상태에서 동일 쿼리
SELECT * FROM comments WHERE postId = 123;
같은 데이터에서 select 하는 시간이 단축 가능합니다.
조인 쿼리에서의 효과
블로그 게시물과 댓글을 함께 가져오는 쿼리를 생각해 봅시다:
-- 게시물과 댓글 함께 가져오기
SELECT p.*, c.*
FROM blog_posts p
JOIN comments c ON p.id = c.postId
WHERE p.slug = 'database-indexing-guide';
이런 조인 쿼리에서는 관련 필드 모두에 인덱스가 있을 때 성능이 극대화됩니다.
인덱스 사용 시 주의할 점
인덱스가 만능은 아닙니다. 몇 가지 고려해야 할 점이 있습니다:
- 쓰기 성능 저하: 인덱스는 검색 속도를 높이지만, INSERT, UPDATE, DELETE 작업 시에는 인덱스도 함께 업데이트해야 하므로 약간의 성능 저하가 있을 수 있습니다.
- 저장 공간 증가: 인덱스도 디스크 공간을 차지합니다. 너무 많은 인덱스를 생성하면 저장 공간이 낭비될 수 있어요.
- 적절한 필드 선택: 자주 검색하는 필드에만 인덱스를 적용하는 것이 좋습니다. 모든 필드에 인덱스를 걸면 오히려 성능이 저하될 수 있습니다.
마무리
인덱스는 데이터베이스 성능 최적화의 기본 중의 기본입니다. 특히 데이터가 많아질수록 그 효과는 더욱 커집니다.