최범균 님의 "주니어 백엔드 개발자가 반드시 알아야 할 실무 지식" 책을 정리한 포스팅 입니다.
1. 성능에 핵심인 DB
- 테이블 풀 스캔일 경우, DB 부하 커짐
2. 조회 트래픽을 고려한 인덱스 설계
- 일반적인 시스템에서는 조회 기능의 실행 비율이 높음
- 인기 있는 게시글과 인기 없는 게시글이 구분될 수 있음
- ➡️ 인덱스 사용 (카테고리, 작성자 id 등)
Full-Text Search
더보기
- 데이터베이스나 검색 시스템에서 문자열 전체를 대상으로 검색하는 기능
- ➡️ 효율적으로 검색할 수 있음
- ✅ 단어 단위 인덱스
- ✅ 불용어 처리
- ✅ 어근화 / 형태소 분석
- ✅ 랭킹/점수 부영
MySQL
CREATE FULLTEXT INDEX idx_title_content ON articles(title, content);
SELECT *
FROM articles
WHERE MATCH(title, content) AGAINST('spring cloud' IN NATURAL LANGUAGE MODE);
MongoDB
db.articles.createIndex({ content: "text" })
db.articles.find({ $text: { $search: "spring cloud" } })
선택도를 고려한 인덱스 칼럼 선택
- 선택도: 인덱스 칼럼의 고유한 값 비율
- ✅ 선택도가 높으면 고유한 값이 많음
- ➡️ 선택도가 높을수록 조회 효율이 높아짐
- ➡️ 선택도가 낮더라도, 고빈도인 경우 반드시 사용
커버링 인덱스 활용하기
- 커버링 인덱스: 특정 쿼리를 실행하는데 필요한 칼럼을 모두 포함하는 인덱스
- ✅ 쿼리 실행 효율을 높일 수 있음
인덱스는 필요한 만큼만
- 효과가 적은 인덱스를 추가하면 오히려 성능이 나빠질 수 있음
- ⚠️ 인덱스는 조회 속도를 빠르게 해주지만 인덱스 관리 비용 및 리소스가 추가되기 때문
3. 몇 가지 조회 성능 개선 방법
미리 집계하기
- ⚠️ 1:N 관계 테이블에 대해 서브 쿼리를 통해 질의 시, N+1 문제 발생
- ➡️ denormalization
- ➡️ 집계 함수
문제) 게시글 - 좋아요 갯수
더보기
SELECT p.*,
(SELECT COUNT(*) FROM likes l WHERE l.post_id = p.id) AS likedCnt
FROM posts p
WHERE p.category = 'tech'
ORDER BY p.created_at DESC
LIMIT 20;
- 총 쿼리: 목록 + count
대안 1) denormalization
더보기
SELECT id, title, likedCnt
FROM posts
WHERE category='tech'
ORDER BY created_at DESC
LIMIT 20;
좋아요 성공
START TRANSACTION;
INSERT INTO likes(post_id, user_id, created_at)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE post_id = post_id; -- 중복이면 no-op
SET @rows := ROW_COUNT(); -- 1이면 신규 성공
UPDATE posts
SET likedCnt = likedCnt + IF(@rows > 0, 1, 0)
WHERE id = ?;
COMMIT;
좋아요 취소
START TRANSACTION;
DELETE FROM likes WHERE post_id=? AND user_id=?;
SET @rows := ROW_COUNT();
UPDATE posts
SET likedCnt = GREATEST(likedCnt - IF(@rows > 0, 1, 0), 0)
WHERE id = ?;
COMMIT;
대안 2) 집계 함수
더보기
SELECT p.*, COALESCE(t.cnt, 0) AS likedCnt
FROM posts p
LEFT JOIN (
SELECT post_id, COUNT(*) AS cnt
FROM likes
WHERE post_id IN (/* 이번 페이지의 post_id 목록 */)
GROUP BY post_id
) t ON t.post_id = p.id
WHERE p.category = 'tech'
ORDER BY p.created_at DESC
LIMIT 20;
페이지 기준 목록 조회 대신 ID 기준 목록 조회 방식 사용하기
Offset Pagination
- ⚠️ db는 정렬 순서 맞춰 offset 값만큼 행들에 접근해야 함
- ➡️ 데이터가 많아질수록 급격히 느려짐
SQL) Offset Pagination
더보기
SELECT id, title, created_at
FROM posts
ORDER BY id DESC
LIMIT 20 offset 99900;
Keyset / Cursor Pagination (스크롤 방식)
- 마지막으로 본 ID를 기준으로 다음 데이터를 요청
- ➡️ 건너뛰기 비용 없이 곧장 다음 구간으로 점프
SQL) Keyset / Cursor Pagination
더보기
SELECT id, title, created_at
FROM posts
WHERE id < :lastId
ORDER BY id DESC
LIMIT 20;
요청) Keyset / Cursor Pagination
더보기
GET /posts?limit=20&cursor=id:12345
조회 범위를 시간 기준으로 제한하기
- ✅ 날짜 단위 조회 시 유용 (ex. 뉴스 기사 목록, 회원의 주문 목록)
- ✅ 최근 데이터 조회 시 유용 (ex. 공지 사항, 개인 활동 정보)
전체 개수 세지 않기
- ⚠️ 데이터가 많아질수록 실행 시간 증가
- ‼️ 모든 데이터를 탐색해야 함
오래된 데이터 삭제 및 분리 보관하기
- ⚠️ 데이터 갯수가 늘어날수록 쿼리 실행 시간은 증가함
- ✅ 과거 데이터를 삭제하면 데이터 개수를 일정하게 유지할 수 있음
- ➡️ 성능 또한 일정 수준으로 유지됨
- ex) 로그 (로그인 시도 내역)
4. 알아두면 좋을 몇 가지 주의 사항
쿼리 타임아웃
- 쿼리 타임아웃을 설정하여 실행 시간을 제한하기
- ✅ 타임아웃 발생 시 에러 발생 (정상 종료)
- ➡️ 기능의 특성에 따라 다르게 설정해야 함
- ex) 블로그 조회 - 짧게, 상품 결제 기능 - 길게 (후속 처리 및 데이터 정합성이 복잡해짐)
상태 변경 기능은 복제 DB에서 조회하지 않기
배치 쿼리 실행 시간 증가
- 데이터를 일괄 처리함 (조회/집계/생성)
- ⚠️ 한번에 조회하고 집계하는 데이터가 많아질수록 실행 시간도 증가함
- ‼️ 데이터의 양이 특정 임계점을 넘어가면 실행 시간을 예측할 수 없을 만큼 길어질 수 있음
- ➡️ 배치에서 사용하는 쿼리의 실행 시간을 지속적으로 추적하기
- ➡️ 배치에서 사용하는 쿼리가 커버링 인덱스를 활용하도록 하기
- ➡️ 데이터를 일정 크기로 배치 처리하기
- ex) 시스템 통계, 사용자별 통계
타입이 다른 칼럼 조인 주의
- ⚠️ 타입이 다른 칼럼이 조인할 경우, 두 컬럼 중 하나는 형변환이 일어나게 됨
- ‼️ 인덱스의 타입이 일치하지 않으면 활용 못함
- ➡️ 두 칼럼의 타입을 맞춰서 비교해야 함
MySQL) 타입 변환
더보기
타입을 변환해 두 칼럼의 타입을 일치시킨 후 비교
select u.userId, u.name, p.*
from user u, pusu p
where u.userId = 145
and cast(u.userId as char character set utf8mb4) collate 'utf8mb4_unicode_ci' = p.receiverId
and p.receiverType = 'MEMBER'
order by p.id desc
limit 100;
테이블 변경은 신중하게
- 테이블 변경 시, 새 테이블을 생성하고 원본 테이블의 데이터를 복사한 뒤, 복사가 완료되면 새 테이블로 대체함
- ❌ 데이터 복사 중에는 서비스가 중단됨 (DML 작업이 차단됨)
- ➡️ Online DDL 사용하기
MySQL) Online DDL
더보기
- 서비스 중단을 줄이기 위해 MySQL(InnoDB)에서 제공하는 기능
- DDL을 수행하면서도 DML을 허용
- ✅ 일부 변경은 full copy를 피하거나 백그라운드 작업으로 수행
- ❌ 단, 모든 작업이 online은 아님 (인덱스, 컬럼 변경 등)
DB 최대 연결 개수
- 전체 애플리케이션 서버의 커넥션 수는 db에서 제공하는 커넥션수 내에서 보유해야 함
- ‼️ 초과할 경우, 연결 실패 발생
- ➡️ DB의 최대 연결 개수 늘리기
- ❌ DB 서버의 CPU 사용률이 70% 이상으로 높다면 연결 개수 늘리면 안됨
- ⚠️ 연결 수가 많아질수록 DB 부하는 증가하고 성능 저하가 발생할 수 있음
설정) MySQL
더보기
조회
SHOW VARIABLES LIKE 'max_connections'; # 최대 커넥션 수
SHOW STATUS LIKE 'Threads_connected'; # 현재 연결된 커넥션 수
설정
SET GLOBAL max_connections = 500;
설정) MongoDB
더보기
조회
db.serverStatus().connections
설정 (mongod.conf)
net:
maxIncomingConnections: 20000
5. 실패와 트랜잭션 고려하기
- 트랜잭션을 누락할 경우, 원자성이 보장되지 않음