API
API가 점점 느려질 때 performance를 생각해보자
API 응답이 100ms에서 500ms로 느려지는 건 보통 데이터베이스 쿼리 성능 때문이다. 인덱스와 쿼리 최적화가 필요하다.
API가 예전에는 빨랐는데 지금은 느리다고 한다. 데이터가 많아지면서 자연스러운 일이지만, 방치하면 사용자 경험이 악화된다.
느린 쿼리 찾기
# PostgreSQL에서 느린 쿼리 로그 활성화
SET log_min_duration_statement = 1000; -- 1초 이상 쿼리 로그
# 로그 확인
grep 'duration:' /var/log/postgresql/postgresql.log | sort -k2 -rn | head -20
MySQL이면:
# MySQL 느린 쿼리 로그
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
# 확인
tail -50 /var/log/mysql/slow-query.log
보통 가장 느린 쿼리 3-5개만 최적화해도 전체 성능이 크게 개선된다.
쿼리 실행 계획 분석하기
-- 쿼리가 어떻게 실행되는지 본다
EXPLAIN ANALYZE
SELECT * FROM posts
WHERE author_id = 123
AND created_at > '2024-01-01';
-- 결과에서:
-- Seq Scan: 테이블 전체 스캔 (느림)
-- Index Scan: 인덱스 사용 (빠름)
Seq Scan이 나오면 WHERE 절에 인덱스가 필요하다는 뜻이다.
필요한 인덱스 생성하기
-- author_id로 자주 검색하는가?
CREATE INDEX idx_posts_author_id ON posts(author_id);
-- 날짜 범위로 검색하는가?
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
-- 여러 컬럼으로 함께 검색하는가?
CREATE INDEX idx_posts_author_created
ON posts(author_id, created_at DESC);
-- 인덱스 생성 후 쿼리 실행 계획 다시 확인
EXPLAIN ANALYZE SELECT * FROM posts WHERE author_id = 123;
인덱스 생성은 쓰기 성능을 약간 저하시키지만, 읽기를 크게 개선한다.
N+1 문제 확인하기
// N+1: 글 목록을 가져온 후, 각 글의 저자를 다시 쿼리
const posts = await Post.find();
for (const post of posts) {
post.author = await User.findById(post.author_id); // 100글이면 101번 쿼리!
}
// 해결: JOIN으로 한 번에
const posts = await Post.find().populate('author');
// 또는 SQL JOIN
SELECT p.*, u.name as author_name
FROM posts p
JOIN users u ON p.author_id = u.id;
N+1은 쿼리 수가 선형적으로 증가해서 매우 느리다.
데이터베이스 연결 풀 확인하기
# 연결 풀이 부족해서 요청이 대기 중인가?
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';
# 최대 연결 수
SHOW max_connections;
# 필요시 늘리기
ALTER SYSTEM SET max_connections = 200;
연결 풀이 부족하면 새 요청들이 연결을 기다리면서 응답이 느려진다.
캐시 추가하기
// Redis 캐시 추가
const redis = require('redis').createClient();
async function getPost(id) {
// 캐시에 있는가?
const cached = await redis.get(`post:${id}`);
if (cached) return JSON.parse(cached);
// 없으면 DB에서 조회
const post = await Post.findById(id);
// 캐시에 저장 (10분)
await redis.setex(`post:${id}`, 600, JSON.stringify(post));
return post;
}
자주 조회하는 데이터는 캐시하면 DB 부하를 크게 줄일 수 있다.
페이징 확인하기
-- 목록을 가져올 때 LIMIT을 제한하는가?
SELECT * FROM posts LIMIT 20; -- 좋음
-- 아니면 모든 글을 가져오는가?
SELECT * FROM posts; -- 나쁨 (1000만 개면?)
목록은 항상 페이징해야 한다. 아무리 인덱스가 좋아도 100만 개 행을 메모리에 올리면 느려진다.
응답 시간 모니터링하기
# 엔드포인트별 응답 시간
curl -w '\nTotal time: %{time_total}s\n' https://api.example.com/posts
# 또는 API 모니터링 도구
Datadog, New Relic, APM
응답 시간의 추이를 보면 언제부터 느려졌는지 알 수 있다.
데이터베이스 청소하기
-- 필요 없는 데이터 삭제
DELETE FROM logs WHERE created_at < '2024-01-01';
DELETE FROM sessions WHERE expired_at < NOW();
-- 테이블 최적화
VACUUM ANALYZE posts; -- PostgreSQL
OPTIMIZE TABLE posts; -- MySQL
불필요한 데이터가 많으면 테이블 크기가 커지고 스캔이 느려진다.
성능 개선 체크리스트
- 느린 쿼리를 찾았는가?
- 실행 계획에서 인덱스를 사용하는가?
- N+1 문제가 없는가?
- 페이징이 되는가?
- 캐시를 사용하는가?
- 데이터베이스 연결 풀이 충분한가?
- 필요 없는 데이터는 정리했는가?
이 항목들을 하나씩 개선하면 API 성능을 크게 향상시킬 수 있다.