← 전체 글로 돌아가기

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

불필요한 데이터가 많으면 테이블 크기가 커지고 스캔이 느려진다.

성능 개선 체크리스트

  1. 느린 쿼리를 찾았는가?
  2. 실행 계획에서 인덱스를 사용하는가?
  3. N+1 문제가 없는가?
  4. 페이징이 되는가?
  5. 캐시를 사용하는가?
  6. 데이터베이스 연결 풀이 충분한가?
  7. 필요 없는 데이터는 정리했는가?

이 항목들을 하나씩 개선하면 API 성능을 크게 향상시킬 수 있다.