1. 성능의 핵심인 DB
데이터베이스(DB)의 성능은 시스템 전체에 직접적인 영향을 미친다.
단순히 서버 성능을 높이는 것보다, 인덱스와 쿼리를 효율적으로 설계하는 것이 DB 성능 향상에 훨씬 더 중요하다.
같은 데이터를 조회하더라도 쿼리 최적화와 인덱스 설계에 따라 속도가 몇 배 이상 차이 날 수 있다.
2. 조회 트래픽을 고려한 인덱스 설계
DB 테이블을 설계할 때는 조회 패턴과 트래픽 규모를 반드시 고려해야 한다. 조회 시 어떤 컬럼으로 검색할지 파악한 뒤, 그에 맞춰 인덱스를 설정해야 한다.
인덱스는 크게 단일 인덱스(Single Index) 와 복합 인덱스(Composite Index) 로 구분된다.
- 단일 인덱스: 하나의 컬럼만 인덱스로 지정
- 복합 인덱스: 여러 컬럼을 묶어 인덱스로 지정
트래픽이 적을 때는 인덱스가 많지 않아도 큰 문제가 없으며, 삽입·수정 시 인덱스 관리에 드는 비용이 적다는 장점이 있다.
하지만 매일 수만 건의 데이터를 조회하는 서비스라면, 적절한 인덱스 설계 없이는 쿼리 성능이 급격히 저하될 수 있다.
2.1. 선택도가 높은 컬럼 사용
인덱스 컬럼을 선택할 때는 선택도(Selectivity) 가 높은 컬럼을 사용하는 것이 좋다.
선택도는 특정 컬럼 값의 다양성을 의미하며, 중복이 적을수록 선택도가 높다.
예를 들어, 작업 큐(jobqueue) 테이블의 status 컬럼이 대기, 처리중, 완료 세 가지 값만 가진다면 선택도는 낮다.
이 경우 status 컬럼에 인덱스가 없으면 전체 데이터를 풀 스캔해야 하므로 조회 성능이 저하된다.
2.2. 커버링 인덱스 활용
커버링 인덱스(Covering Index) 는 쿼리에서 필요한 모든 컬럼을 인덱스에 포함하는 방식이다. 이 경우 실제 데이터 파일을 읽지 않아도 인덱스만으로 결과를 반환할 수 있어 성능이 크게 향상된다.
3. 조회 성능 개선 방법
3.1 미리 집계하기
조회수, 좋아요 수, 응답자 수와 같은 값은 실시간 계산보다 미리 저장하는 것이 효율적이다. 새로운 테이블을 만들어 매번 집계 쿼리를 수행하면 성능이 급격히 저하된다.
기존 테이블에 컬럼을 추가해 누적값을 저장하면 조회 시 즉시 응답이 가능하다.
3.2 페이지 기준 목록 조회 대신 ID 기준 조회 방식
페이지 번호(page)를 기준으로 데이터를 조회하면, 오프셋(offset) 연산으로 인해 성능이 떨어질 수 있다.
대신 마지막 조회된 ID를 기준으로 다음 데이터를 조회하는 방식이 효율적이다.
-- 비효율적인 OFFSET 방식
SELECT * FROM post ORDER BY id DESC LIMIT 10 OFFSET 10000;
-- 효율적인 ID 기반 방식
SELECT * FROM post WHERE id < 10000 ORDER BY id DESC LIMIT 10;
ID 기반 조회는 큰 테이블에서도 빠르게 다음 페이지 데이터를 가져올 수 있다는 장점이 있다.
3.3 시간 범위 제한 조회
최근 데이터만 필요한 경우, 전체 테이블을 조회하지 말고 시간 기준으로 범위를 제한한다. 예를 들어 점검 이력 조회 시 최근 1개월만 기본으로 조회하도록 설정하면 성능을 안정적으로 유지할 수 있다.
또한, 자주 조회되는 구간은 메모리 캐시에 저장해 응답 속도를 높일 수 있다.
3.4 전체 개수 세지 않기
게시판 등에서 전체 게시글 수를 항상 계산하면 성능이 저하된다.
COUNT(*) 연산은 데이터가 많을수록 비용이 커지므로, 가능하다면 전체 개수를 생략하거나 주기적으로 별도 테이블에 저장하는 방식을 고려한다.
3.5 오래된 데이터 삭제 및 분리 보관
데이터가 지속적으로 쌓이면 쿼리 실행 속도가 느려진다. 주기적으로 오래된 데이터를 삭제하거나 별도의 테이블로 옮기는 것이 좋다.
Delete로 데이터를 삭제할 수도 있다. 이럴 경우에도 쿼리 실행시간은 유지될 수 있다.
그러나 디스크 용량은 늘릴 수 없는데 데이터가 삭제 되었다는 표시만 되고 데이터가 들어가 있던 공간은 향후 재사용된다.
이런 과정이 반복되면 단편화 현상이 발생할 수 있다. 이럴 경우 데이터를 재배치하는 최적화를 통해서 단편화를 줄이고 디스크 사용량까지 줄일 수 있다.
3.6 캐시 서버 구성
DB에 집중되는 트래픽을 완화하기 위해 Redis, Memcached 등의 캐시 서버를 활용할 수 있다.
자주 조회되는 데이터(예: 인기글 목록, 로그인 사용자 정보 등)는 캐시에 저장하면 DB 부하를 크게 줄일 수 있다.
4. 알아두면 좋은 주의사항
4.1 쿼리 타임아웃 설정
동시 접속이 증가하면 특정 쿼리가 지연될 수 있다.
이때 타임아웃을 설정하지 않으면 클라이언트에서 재시도를 하게 되어 오히려 서버 부하가 커진다.
중요하지 않은 요청은 짧은 타임아웃을, 결제나 정산처럼 중요한 요청은 여유 있는 타임아웃을 설정해야 한다.
4.2 복제 DB에서 상태 변경 조회 금지
주 DB와 복제 DB 간의 데이터 동기화는 트랜잭션 커밋 이후 이루어진다.
따라서 커밋 직후 복제 DB를 조회하면 아직 반영되지 않은 데이터가 조회될 수 있다.
상태 변경과 관련된 작업은 반드시 주 DB를 사용해야 한다.
4.3 배치 쿼리 실행 시간 증가
데이터가 많아질수록 배치 쿼리 실행 시간이 비례적으로 늘어난다.
이럴 경우 커버링 인덱스를 이용하거나 시간 단위로 데이터를 나눠 집계한 뒤 통계 테이블에 합산 저장하는 방법을 사용할 수 있다.
4.4 타입이 다른 컬럼 조인 주의
다른 타입의 컬럼을 조인하면 인덱스를 사용할 수 없다. 예를 들어, INT와 VARCHAR 타입을 조인하면 인덱스가 무효화되므로 조인 컬럼의 타입을 일치시켜야 한다.
4.5 테이블 변경은 신중하게
ALTER TABLE은 실행 시 DML(INSERT, UPDATE, DELETE)을 차단한다.
따라서 실서비스 중에 테이블 구조를 변경하면 서비스 중단이 발생할 수 있다.
테이블 변경은 반드시 점검 시간을 확보한 뒤 수행해야 한다.
4.6 DB 최대 연결 개수 관리
DB CPU 사용률이 이미 70% 이상인 상태에서 연결 개수를 늘리면 오히려 부하가 증가한다.
DB 부하가 높다면 단순히 연결 수를 늘리는 대신, 쿼리 최적화나 캐시 분산을 우선 고려해야 한다.
5. 실패와 트랜잭션 고려
결제, 예약, 신청 등 데이터 추가가 포함된 기능에서는 예외 발생 시 롤백이 필요하다. 하지만 메일 전송이나 알림 발송처럼 부가적인 작업에서 오류가 발생한 경우에는 DB 트랜잭션을 롤백하지 않고 별도로 처리하는 것이 합리적이다.
트랜잭션은 원자성(Atomicity)을 보장하기 위한 수단이지만 그 범위를 과도하게 넓히면 불필요한 락(lock) 점유와 자원 낭비로 이어질 수 있다.
이처럼 하나의 로직에 들어있는 DB 처리를 전부 하나의 트랜잭션에 넣어야 한다는 생각보다는 업무 단위에 따라 트랜잭션의 경계를 명확히 구분하는 것이 중요하다.
'책 리뷰 > 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식' 카테고리의 다른 글
| 7장. IO병목, 어떻게 해결하지 (0) | 2026.01.18 |
|---|---|
| 6장. 동시성, 데이터가 꼬이기 전에 잡아야 한다 (0) | 2025.12.15 |
| 5장. 비동기 연동, 언제 어떻게 써야 할까 (0) | 2025.12.07 |
| 4장. 외부 연동이 문제일 때 살펴봐야 할 것들 (0) | 2025.11.29 |
| 2장. 느려진 서비스, 어디부터 봐야 할까 (0) | 2025.10.11 |