본 글은 하루 매일메일의 “RDB에서 페이징 쿼리의 필요성을 설명해 주세요”를 읽고 심화학습하여 정리한 내용입니다.
매일메일 - 기술 면접 질문 구독 서비스
기술 면접 질문을 매일매일 메일로 보내드릴게요!
www.maeil-mail.kr
RDB란?
RDB의 정의
RDB는 Relational Database의 약자이다. 관계형 데이터베이스라고도 한다. 이는 데이터를 table형태로 저장하고, 테이블 간의 관계를 column을 통하여 정의하는 데이터베이스이다. 오늘날 대부분의 대규모 서비스에서 사용되고 있는 데이터베이스 구조이다.
RDB에서 가장 중요한 것은 테이블과 그 관계라고 생각한다.
RDB에서는 하나의 테이블이 하나의 entity를 표현하는 구조를 갖는다. 사용자 userEntity → users 테이블, postEntity → posts테이블처럼 말이다. 각 테이블을 primary key를 갖고 다른 table의 foreign key를 통하여 관계를 맺을 수 있다.
내가 RDB를 사용하는 주된 이유는 정형화된 스키마와 그 관계이다. 그리고 그 일관성은 서비스를 유지하고 정보를 보관하는데 뛰어난 효율성을 자랑한다. 그리고 테이블간의 관계를 이용하여 새로운 정보들을 얻어낼 수 있다.
왜 RDB를 사용하는가?
또 왜? 라는 질문을 갖고 왔다.
왜? 는 기술을 선택하는 데 있어서 가장 중요한 기준이라고 생각한다.
관계형 데이터베이스를 사용하는 주된 이유는 앞서 설명한 정형화된 스키마다. 일례로 프로젝트를 진행하다가 보면 mysql(RDB)를 사용할지 MongoDB(Nosql)를 사용할지 고민하는 경우가 많았다. 한 예시를 살펴보자.
일례로 옛날에 프로젝트를 진행할 때 user repository를 nosql로 구현해 간 적이 있다. nosql을 선택한 가장 주된 이유는 그저 쉬워서이다. 내가 데이터를 마음대로 도큐먼트 형태로 저장하고 이를 빼어 파싱 할 수 있어 편리하고 구현하기 쉽다고 생각했다. 그런데 그 쉬움이 오히려 독이 되었다. 사용자 정보처럼 모든 서비스에서 사용하는 데이터가 정형화되어있지 않으면 그 정보의 신뢰를 얻지 못할 것 같았다. 그리고 다른 테이블과 연동할 때 join 같은 기능과 트랜잭션의 기능을 원활히 사용하지 못하는 것도 RDB를 선택한 이유다.
그리고 RDB는 다음과 같은 대표적인 기능을 지원한다.
- 데이터 정합성 보장: PK/FK 제약조건을 통해 무결성을 유지할 수 있다.
- 복잡한 관계 표현 가능: JOIN 연산을 통하여 다중 테이블에 대한 질의가 가능하다.
- SQL 기반 질의 언어 지원: 선언적 쿼리 언어로 유연한 쿼리를 작성할 수 있다.
- 트랜잭션 지원: ACID 속성을 통해 신뢰할 수 있는 트랜잭션을 지원한다.
언제 RDB를 사용하는가?
앞의 예시를 통하여 언제 RDB를 사용하는지 정리해보자
- 데이터 구조가 정형적일 때
- 관계가 중요한 데이터를 다룰 때
- 데이터의 정합성과 보안이 중요할 때:
보안? → 정형화된 스키마와 권한 제어를 통해 체계적인 데이터를 다룰 수 있다는 의미. 특정 칼럼에 대한 암호화(데이터 암호화 가능) - 복잡한 조건, 정렬, JOIN이 필요한 경우
페이징은 무엇이고 왜 필요한가?
페이징 쿼리는 RDB에서 전체 데이터를 부분 적으로 나눠 데이터를 조회하거나 처리할 때 사용한다.
데이터를 상대적으로 더 작은 단위로 나눠 처리하기 때문에, 데이터베이스나 애플리케이션의 리소스 사용 효율이 증가한다. → 만약 row가 1억 인 테이블에서 select * from table_name; 을 통하여 테이블에 존재하는 모든 열을 가져오게 된다면 상당히 비효율적일 것이다.
그래서 MySQL에서 페이징 쿼리는 일반적으로 LIMIT, OFFSET구문을 사용하여 처리한다.
select *
from subscribe
limit 500
offset 0;
이렇게 되면 0부터 500개의 열을 가져오라는 의미이다.
→ 구현이 간단하고, 페이지 수 기반으로 조회할 수 있다는 장점이 있다.
LIMIT, OFFSET기반 페이징의 단점
불행히도 장점만 있는 것은 아니다. OFFSET기반 페이징은 단점이 존재하는데, 그 단점이 무엇인지 아래에서 알아보자.
limit, offset 방식의 페이징 쿼리는 뒤에 있는 데이터를 읽을수록 점점 응답 시간이 길어진다는 단점이 있다.
왜냐하면 DBMS(Database management System)은 지정된 offset 수만큼 레코드를 읽은 이후에 데이터를 가져오기 때문이다.
또한 삽입/삭제 시 페이지 밀림 현상도 존재한다. 만약 데이터셋에 삽입 또는 삭제가 발생하면 기존의 기준 위치가 변경되어서 누락된 데이터가 생길 수 있다.
그래서 해결 방법은?
커서 기반 페이징
이전 페이지의 마지막 데이터(row)를 기준으로 그 이후의 데이터를 조회하는 방식이다. OFFSET 없이 데이터를 연결된 흐름으로 빠르게 조회 가능하다는 장점이 있다
예를 들어서 첫 페이지를 다음과 같이 조회하였다고 생각해 보자
SELECT *
FROM subscribe
WHERE deleted_at BETWEEN '2024-01-01' AND '2024-04-01'
ORDER BY deleted_at, id
LIMIT 10;
그리고 첫 페이지 이후의 페이지는 조회된 데이터의 마지막 값을 기반으로 조회한다고 하였다.
만약 마지막 페이지의 deleted_at이 2024-01-01이고 식별자가 78이라면 다음과 같은 쿼리를 작성할 수 있다.
SELECT *
FROM subscribe
WHERE (
(deleted_at = '2024-01-01' AND id > 78)
OR (deleted_at > '2024-01-01' AND deleted_at < '2024-04-01')
)
ORDER BY deleted_at, id
LIMIT 10;
이때 클라이언트는 다음 데이터를 next_cursor형태로 전달하여 데이터를 받는다.
GET /subscribe?deleted_at=2024-01-01&id=78&limit=10d
이렇게 말이다.
부가 질문) 정렬을 한다면 그것 만으로도 오버헤드 아닌가?
→ 정렬 자체는 오버헤드가 맞다. 하지만 복합 인덱스를 사용하여 인덱스 탐색만으로도 처리할 수 있다면 offset보다 효율적이다.
커서 기반 페이징을 왜 사용하는가?
그럼 왜 커서 기반 페이징을 사용해야 하는지 표로 정리해 보았다.
비교 항목 | OFFSET 방식 | 커서 방식 |
성능 | OFFSET 커질수록 느림 | 항상 빠름 |
중간 삽입/삭제 | 페이지 밀림 발생 | 안정적 |
인덱스 사용 | 효율 낮음 | 범위 스캔 가능 |
구현 난이도 | 쉬움 | 상대적으로 복잡 |
구현 난이도는 확실히 offset기반 방식보다는 복잡하지만, 성능과 일관성 면에서 큰 이점이 있으므로 커서 방식을 추천한다.
게시판 프로젝트에서 커서 기반의 페이징 시스템으로 마이그레이션 해봐야겠다.
'Database' 카테고리의 다른 글
어떻게 Phantom Read를 방지할 것인가! (0) | 2024.12.24 |
---|---|
QueryDSL의 HQL injection 방지하기 (1) | 2024.12.20 |