[실전 서버리스 11] 봇이 ?page=14만을 긁는다, 동적 페이지 상한과 방어

어느 날 로그를 보다 이상한 걸 발견했다. 봇이 페이지 번호 14만 같은, 존재할 리 없는 페이지를 계속 요청하고 있었다. 글은 몇천 개인데 14만 페이지라니. 이 무의미한 요청들이 데이터베이스를 두드리며 자원을 낭비하고 있었다. 이번 편은 이 봇의 무한 페이지 문제와, 동적 페이지 상한으로 방어한 실전을 다룬다.


앞서 SEO 시리즈에서 페이지네이션 무한 페이지 함정을 다뤘는데, 이번 편은 그 서버 쪽 방어를 데이터베이스 비용 관점에서 실전으로 파고든다.

1. 봇이 무한 페이지를 긁는 이유

봇은 페이지네이션 링크를 따라 다음 페이지, 그다음 페이지로 넘어가며 사이트를 훑는다. 그런데 페이지 번호 규칙을 알면, 링크가 없어도 번호를 계속 올려가며 요청할 수 있다. 마지막 페이지가 어디인지 서버가 명확히 알려주지 않으면, 봇은 끝을 모른 채 계속 다음 번호를 시도한다.


그 결과 존재하지 않는 페이지 번호까지 요청이 온다. 실제 마지막이 수백 페이지인데 봇은 수만, 수십만 페이지를 요청한다. 이 요청 하나하나가 서버에 와서 데이터베이스를 조회하려 한다. 없는 페이지라도 서버가 그걸 확인하려면 데이터베이스를 건드려야 하기 때문이다.


이게 앞서 다룬 캐시로도 잘 안 막힌다. 봇이 매번 다른 페이지 번호를 요청하니, 각 요청이 다른 캐시 키가 되어 캐시가 소용없다. 오히려 무한한 페이지 번호가 캐시 키를 폭증시킨다. 그래서 이건 캐시가 아니라 다른 방어가 필요하다.


봇이 무한 페이지를 긁는 걸 처음 봤을 때는 당황스러웠다. 왜 존재하지도 않는 14만 페이지를 요청하나 싶었다. 알고 보니 봇은 끝을 모르면 계속 다음 번호를 시도하는 것이었다. 서버가 여기가 끝이라고 명확히 안 알려주면, 봇은 무한히 파고든다. 끝을 알려주는 게 방어의 출발이다.


한 가지 더, 이 무한 페이지 문제는 봇 트래픽이 많을수록 심각하다. 사람 사용자는 그렇게 깊은 페이지를 요청하지 않지만, 봇은 기계적으로 끝없이 파고든다. 그래서 봇 트래픽이 많은 사이트일수록 이 방어의 값어치가 크다. 방어 하나로 봇이 낭비하던 자원을 통째로 회수할 수 있다.

2. 동적 페이지 상한

방어의 핵심은 존재하는 페이지 범위를 넘는 요청을 딱 잘라 막는 것이다. 전체 글 수를 페이지당 개수로 나누면 최대 페이지 수가 나온다. 그 최대를 넘는 페이지 요청에는 데이터베이스를 조회하지 않고 바로 없다고 응답한다. 범위를 벗어난 요청을 데이터베이스에 닿기 전에 차단하는 것이다.


여기서 상한을 동적으로 계산하는 게 중요하다. 글은 계속 늘어나니 최대 페이지 수도 변한다. 상한을 고정값으로 박아두면 글이 늘었을 때 정상 페이지까지 막거나, 반대로 여유를 너무 두면 방어가 느슨해진다. 그래서 전체 글 수를 주기적으로 세어 상한을 자동으로 갱신한다. 글이 늘면 상한도 자연히 커진다.


이 상한 계산도 매 요청마다 하면 그 자체가 데이터베이스 부담이다. 그래서 상한 값을 캐시해둔다. 정기 작업으로 전체 글 수를 세어 상한을 계산해 저장해두고, 페이지 요청은 그 저장된 상한과 비교만 한다. 상한 계산은 가끔, 비교는 매번. 이렇게 나누면 방어 자체가 가벼워진다.


동적 페이지 상한을 계산할 때, 상한 자체를 캐시하는 게 요령이다. 매 요청마다 전체 글 수를 세면 그게 또 부담이니, 정기 작업으로 상한을 계산해 저장해두고 요청은 그 저장값과 비교만 한다. 계산은 가끔, 비교는 매번. 이렇게 나누면 방어 로직 자체가 가벼워져 부담을 안 준다.


조금 더 짚으면, 이 문제가 캐시로 안 막히는 게 핵심이다. 봇이 매번 다른 페이지 번호를 요청하니 각각 다른 캐시 키가 되어 캐시가 소용없고, 오히려 캐시 키만 폭증한다. 그래서 앞 편의 캐시와 별개로, 범위를 넘는 요청을 데이터베이스에 닿기 전에 차단하는 전용 방어가 필요하다. 문제마다 맞는 방어가 다르다.

3. 넘는 요청은 404로

범위를 넘는 페이지 요청에는 404로 응답한다. 앞서 SEO 시리즈에서 강조한 것처럼, 없는 건 없다고 정직하게 상태 코드로 말한다. 빈 목록을 정상 응답으로 주면 봇이 다음 페이지가 있는 줄 알고 계속 파고들지만, 404를 주면 여기가 끝이라는 걸 알고 멈춘다.


그리고 이 404 응답은 오래 캐시되게 한다. 같은 없는 페이지를 봇이 또 요청하면, 서버까지 오기 전에 캐시된 404가 응답한다. 게다가 검색엔진에는 이 페이지를 색인하지 말라는 표시도 함께 준다. 없는 페이지가 검색에 잡히지 않게 하는 것이다. 상태 코드와 캐시, 색인 표시를 함께 써서 완전히 차단한다.


이렇게 하니 봇의 무한 페이지 요청이 데이터베이스에 거의 닿지 않게 됐다. 상한을 넘는 요청은 데이터베이스 조회 없이 즉시 차단되고, 그마저 캐시되어 반복 요청도 서버 부담을 안 준다. 무의미한 요청에 쓰이던 자원이 회수된 것이다.


넘는 요청을 404로 주고 오래 캐시하는 조합이 효과적이다. 404로 끝을 알려 봇을 멈추게 하고, 그 404를 캐시해 반복 요청도 서버까지 안 오게 한다. 여기에 검색엔진에는 색인하지 말라는 표시까지 더하면, 없는 페이지가 검색에 잡히지도 않는다. 세 가지를 함께 써서 완전히 차단한다.


또 하나, 이 방어를 넣으면서 봇 트래픽 전반을 이해하게 됐다. 로그를 보며 봇이 어떤 패턴으로 사이트를 긁는지, 어디서 자원을 낭비하는지가 보였다. 무한 페이지는 그 한 예일 뿐, 봇은 여러 방식으로 자원을 소모한다. 그래서 로그를 주기적으로 살피며 새로운 낭비 패턴을 찾아 막는 게 운영의 일부가 됐다.

4. 정상 페이지는 열어둔다

주의할 점은 방어를 하되 정상 페이지는 막지 않는 것이다. 상한을 너무 낮게 잡으면 실제로 존재하는 페이지까지 막아서, 사용자나 봇이 정상 콘텐츠를 못 보게 된다. 그래서 상한은 실제 글 수에 정확히 맞춰야 한다. 동적으로 계산하는 이유가 여기 있다.


또 방어 로직에 실수가 없어야 한다. 나도 처음 이 방어를 넣을 때, 특정 페이지 구간에서 가드가 빠지는 실수를 했다가 다시 고쳤다. 방어 조건을 잘못 짜면 막아야 할 걸 안 막거나, 열어야 할 걸 막는다. 그래서 방어 로직은 여러 경우를 테스트해 정확히 동작하는지 확인해야 한다.


정상 사용자 경험을 해치지 않으면서 봇의 낭비만 막는 게 목표다. 사람이 볼 페이지는 다 열려 있고, 존재하지 않는 페이지만 막힌다. 이 경계를 정확히 지키는 게 좋은 방어의 조건이다. 과하면 정상을 막고, 느슨하면 낭비를 못 막는다.


정상 페이지는 막지 않는 게 방어의 전제다. 상한을 너무 낮게 잡아 실제 페이지를 막으면 콘텐츠를 못 보게 되니, 상한은 실제 글 수에 정확히 맞춰야 한다. 그래서 동적으로 계산하는 것이다. 방어는 봇의 낭비만 막고 사람의 정상 접근은 열어두는 정밀함이 필요하다.


조금 더 실무적으로, 방어 로직에 실수가 없는지 여러 경우를 테스트해야 한다. 나도 처음 이 방어를 넣을 때 특정 페이지 구간에서 가드가 빠지는 실수를 했다가 고쳤다. 조건을 잘못 짜면 막을 걸 안 막거나 열 걸 막는다. 그래서 경계 값들을 하나씩 테스트해 정확히 동작하는지 확인하는 게 필수다.

5. 방어도 측정한다

이 방어의 효과도 측정으로 확인했다. 데이터베이스 읽기량이 방어 전후로 얼마나 줄었는지, 봇의 무한 페이지 요청이 얼마나 차단됐는지를 봤다. 앞 편의 캐시와 이 페이지 상한 방어가 함께 작동해, 봇 트래픽이 데이터베이스에 주는 부담이 크게 줄었다.


이 모든 게 앞서 다룬 감이 아니라 데이터라는 원칙 위에 있다. 봇이 무한 페이지를 긁는다는 걸 로그로 발견하고, 방어를 넣고, 효과를 측정해 확인했다. 문제를 데이터로 찾고 데이터로 검증하는 이 방식이 서버리스 운영의 기본이다. 특히 비용이 걸린 데이터베이스에서는 더욱 그렇다.


정리하면 봇의 무한 페이지 요청은 동적으로 계산한 페이지 상한으로 막고, 넘는 요청은 404와 캐시, 색인 차단으로 완전히 차단하되, 정상 페이지는 열어두고 효과를 측정한다. 여기까지가 D1 파트다. 다음 편부터는 파일을 다루는 R2 스토리지로 넘어가, egress 0의 오브젝트 스토리지가 뭐가 다른지를 다룬다.


방어의 효과도 측정으로 확인하는 게 원칙이다. 방어 전후로 데이터베이스 읽기가 얼마나 줄었는지를 봐서 효과를 검증한다. 감으로 이제 됐겠지 하는 대신, 데이터로 무한 페이지 요청이 차단됐는지 확인한다. 문제를 데이터로 찾고 데이터로 검증하는 이 방식이 서버리스 운영의 기본이다.


다음 편부터는 데이터베이스를 떠나 파일을 다루는 R2 스토리지로 넘어간다. 이미지와 동영상이 많은 커뮤니티에서 파일을 어떻게 저렴하고 효율적으로 다루는지, egress 0이라는 결정적 이점부터 살펴본다.