LIMIT 50을 붙였다.

그래서 50개만 읽고 빨리 끝날 줄 알았다. 그런데 화면은 한참 생각 중이고, API 호출은 계속된다. 분명 50개만 달라고 했는데, 데이터는 왜 창고 끝까지 찾아보는 걸까?

이 장면은 DynamoDB를 처음 만났을 때 가장 자주 나오는 오해다. SQL 문법은 익숙하다. 하지만 데이터를 찾는 방식은 익숙한 관계형 데이터베이스와 꽤 다르다.

핵심은 하나다.

DynamoDB에서 빠른 조회의 시작점은 조건의 개수가 아니라, 어디서 찾을지 정확히 알고 있느냐다.

주소가 있는 부탁과 없는 부탁

큰 물류 창고를 떠올려 보자.

“B동 3층 12번 선반에서 상자 하나 꺼내 주세요”라고 말하면 직원은 바로 움직인다. 이게 DynamoDB의 Query에 가깝다. 먼저 Partition Key라는 주소를 지정하고, 그 주소 안에서 필요한 데이터를 찾는다.

반대로 “파란 상자를 50개 찾아 주세요”라고 말하면 어떨까?

파란 상자는 어느 선반에든 있을 수 있다. 직원은 상자를 하나씩 열어 보고, 파란 것만 따로 담아야 한다. 이게 Scan과 필터의 조합이다.

-- 주소가 있다. 특정 사용자 활동 안에서 찾는다.
SELECT *
FROM UserActivity
WHERE user_id = 'user-1'
  AND memo IS NOT NULL;
-- 주소가 없다. memo가 있는 항목을 넓게 찾아야 한다.
SELECT *
FROM UserActivity
WHERE memo IS NOT NULL
LIMIT 50;

두 쿼리에는 모두 WHERE가 있다. 하지만 첫 번째 쿼리는 어디로 갈지 알고 있고, 두 번째 쿼리는 무엇을 찾을지만 안다.

이 차이가 성능을 가른다.

필터는 입구 경비원이 아니라 출구 심사대다

여기서 중요한 단어가 FilterExpression이다.

이름만 보면 필터가 데이터를 미리 걸러 줄 것 같다. 하지만 DynamoDB에서는 보통 그렇지 않다. 필터는 많은 경우 데이터를 읽은 적용된다.

창고 비유로 바꾸면 이렇다.

  1. 상자를 꺼낸다.
  2. 파란 상자인지 확인한다.
  3. 아니면 다시 내려놓는다.
  4. 맞으면 결과 바구니에 넣는다.

즉, 결과 목록에 안 보인다고 해서 읽지 않은 것은 아니다.

이건 운영 화면을 볼 때도 중요하다. 결과가 0건이라고 해서 데이터베이스가 아무 일도 하지 않았다는 뜻은 아니다. 오히려 많은 후보를 읽고 모두 탈락시킨 결과일 수 있다.

LIMIT은 “결과의 마감선”이지 “탐색 거리 제한”이 아니다

LIMIT 50의 직관은 이렇다.

50개면 끝.

하지만 필터가 있는 DynamoDB 조회는 종종 이렇게 움직인다.

첫 페이지에서 후보 10개를 읽음
→ 조건 통과 1개

다음 페이지에서 후보 10개를 읽음
→ 조건 통과 0개

다음 페이지에서 후보 10개를 읽음
→ 조건 통과 2개

… 결과 50개를 채울 때까지 반복

그래서 LIMIT 50은 “최종 결과를 50개까지만 보여 달라”는 뜻에 가깝다. “50개만 읽고 멈춰라”는 약속은 아니다.

필터를 통과하는 비율이 낮으면, 결과 50개를 채우려고 수백 개, 수천 개의 후보를 읽을 수 있다. 필요한 물건 50개를 찾기 위해 창고를 여러 번 왕복하는 셈이다.

DynamoDB는 다음 페이지가 남아 있음을 LastEvaluatedKey로 알려 준다. 이것은 에러가 아니다. “아직 창고 뒤쪽이 남았습니다”라는 정상적인 안내다.

문제는 애플리케이션이 결과를 꼭 채워야 한다고 판단하고, 이 안내를 너무 오래 따라갈 때 생긴다.

NULL과 ‘아예 없음’은 같은 빈칸이 아니다

또 하나의 함정이 있다. DynamoDB에서는 다음 세 상태가 다르다.

상태
값 있음 속성이 있고 실제 값이 있음
NULL 속성은 있지만 값이 NULL임
MISSING 속성 자체가 없음

관계형 데이터베이스에 익숙하면 마지막 둘을 같은 빈칸으로 취급하고 싶어진다. 하지만 DynamoDB의 유연한 스키마에서는 새 필드를 추가했을 때 기존 데이터 대부분이 MISSING일 수 있다.

그래서 IS NULL, IS NOT NULL, “속성이 존재하는 데이터”는 서로 같은 질문이 아니다.

이 차이를 놓치면 두 가지 일이 동시에 생긴다.

  • 기대한 결과가 나오지 않는다.
  • 결과가 적어서 다음 페이지를 더 많이 찾게 된다.

정확성 문제와 성능 문제가 손을 잡는 순간이다.

느린 조회를 만났을 때, 먼저 볼 다섯 가지

화면이 멈춘 것처럼 보일 때 “DynamoDB가 느리다”라고 결론 내리기 전에 이 다섯 가지만 확인해도 대화가 훨씬 빨라진다.

  1. Partition Key를 정확히 지정했는가?
  2. 지금 요청은 Query인가, Scan인가?
  3. 조건 중 어떤 것이 실제 탐색 조건이고 어떤 것이 후처리 필터인가?
  4. 페이지마다 읽은 후보 수와 실제 반환 수의 차이는 얼마나 큰가?
  5. 다음 페이지를 언제 멈출지에 대한 timeout, 취소, 페이지 제한이 있는가?

특히 네 번째가 좋다. 반환 수보다 읽은 후보 수가 훨씬 많다면, 시스템은 결과를 찾기보다 대부분의 시간을 버릴 데이터를 확인하는 데 쓰고 있을 가능성이 크다.

데이터 모델은 저장 구조가 아니라 길찾기 지도다

DynamoDB를 쓸 때 “어떤 데이터를 저장할까?”만 먼저 생각하면 나중에 곤란해진다.

더 좋은 첫 질문은 이것이다.

사용자는 어떤 조건으로, 얼마나 자주, 얼마나 빨리 이 데이터를 찾아야 할까?

그 질문에 답하는 것이 Partition Key, Sort Key, 그리고 필요한 경우 보조 인덱스의 역할이다. DynamoDB의 데이터 모델은 창고 배치도이자 길찾기 지도다.

자주 찾을 길을 미리 만들어 두면 빠르다. 찾을 일이 생긴 뒤에야 “파란 상자는 어디 있지?”라고 물으면, 창고를 걷는 시간이 길어진다.

마지막으로 기억할 한 문장

LIMIT은 브레이크가 아니다. 결과 바구니의 크기다.

DynamoDB에서 조회가 느릴 때는 결과 수만 보지 말고, 어디서 출발했는지, 무엇을 나중에 버렸는지, 다음 페이지를 왜 또 열었는지를 보자.

그 세 가지를 보면 “50개만 달랬는데 왜 이렇게 오래 걸리지?”라는 질문이 조금 더 정확해진다.

그리고 정확한 질문은, 대개 빠른 해결의 시작이다.