![[실전 성능 14] 사람보다 많은 봇](https://img.thenullpage.com/posts/5768/5768_1_b21112.webp)
지난 편까지 사람 사용자를 위한 성능 최적화를 두루 다뤘다. 그런데 내 사이트를 드나드는 것은 사람만이 아니었다. 어느 날 트래픽을 자세히 뜯어보고 나는 놀랐다. 사람보다 봇이 훨씬 많았고, 그 봇들이 성능과 비용에 만만찮은 부담을 주고 있었다. 이번 편은 이 봇 트래픽이 만든 문제와 내가 그것을 다스린 과정이다.
1. 트래픽의 절반은 사람이 아니었다
처음 접속자 숫자를 봤을 때 나는 실제 방문자보다 훨씬 큰 수에 어리둥절했다. 알고 보니 그 숫자에는 사람이 아닌 자동 프로그램, 즉 봇이 잔뜩 섞여 있었다. 검색 엔진 크롤러부터 각종 수집기, 스캐너까지 온갖 봇이 끊임없이 페이지를 요청하고 있었다. 인터넷 트래픽의 상당 부분이 봇이라는 말을 내 사이트에서 실감했다.
봇은 사람과 행동이 달랐다. 사람은 관심 있는 몇 페이지를 보고 떠나지만, 봇은 사이트의 모든 구석을 기계적으로 훑었다. 목록의 첫 페이지만이 아니라 깊숙한 페이지까지, 오래된 글까지 빠짐없이 요청했다. 사람은 잘 안 가는 곳도 봇은 집요하게 다녔다. 그래서 봇의 요청 패턴은 사람과 완전히 달랐고, 부담을 주는 방식도 달랐다.
이 차이를 이해하는 것이 중요했다. 성능을 사람 기준으로만 최적화하면 봇이 만드는 부담을 놓쳤다. 사람은 캐시된 인기 페이지를 주로 보니 캐시가 잘 들었지만, 봇은 캐시가 잘 안 되는 구석진 페이지까지 훑어서 캐시를 우회했다. 봇의 요청이 곧바로 무거운 조회로 이어지는 경우가 많았다. 나는 봇을 별도의 사용자층처럼 다뤄야 했다.
봇 자체가 나쁜 건 아니었다. 검색 엔진 크롤러 같은 봇은 사이트가 검색에 노출되려면 반드시 필요했다. 문제는 봇의 요청이 사람의 요청과 같은 비용을 치른다는 점이었다. 봇이 무거운 조회를 유발하면 그 부담은 고스란히 서버와 비용으로 돌아왔다. 필요한 봇은 맞이하되 그 부담은 관리해야 하는, 미묘한 균형의 문제였다. 무작정 봇을 막으면 검색 노출을 잃고, 그렇다고 방치하면 자원이 소진되니, 나는 봇을 쫓아내는 대신 그들이 와도 부담이 크지 않도록 사이트 구조를 바꾸는 쪽으로 방향을 잡았다.
2. 무한한 페이지 요청
봇이 만든 대표적인 문제는 깊은 페이지를 끝없이 요청하는 것이었다. 목록이 여러 페이지로 나뉘어 있으면, 봇은 그 페이지를 하나하나 다 요청하려 했다. 사람은 몇 페이지 넘기다 말지만 봇은 마지막까지 갔다. 그런데 깊은 페이지일수록 그것을 만드는 조회가 무거웠다. 봇이 깊이 갈수록 부담이 커지는 구조였다.
더 나쁜 건 봇이 실제로 존재하지 않는 페이지까지 요청하는 경우였다. 규칙적으로 페이지 번호를 늘려가며 요청하다 보면, 콘텐츠가 없는 빈 페이지까지 두드렸다. 그때마다 서버는 없는 걸 확인하려고 조회를 했다. 아무 성과 없는 조회가 반복되며 자원만 소모됐다. 봇의 기계적인 탐색이 서버를 헛돌게 만든 것이다.
나는 이 문제를 봇이 갈 수 있는 페이지의 깊이를 제한하는 것으로 다스렸다. 사람에게 필요한 만큼의 페이지는 열어두되, 그 너머의 무의미하게 깊은 페이지 요청은 막았다. 봇이 끝없이 깊이 파고드는 걸 차단하니 무거운 조회가 크게 줄었다. 필요한 탐색은 허용하고 무의미한 탐색만 걸러내는 선을 그은 것이다.
요청 빈도 자체를 제한하는 것도 방법이었다. 짧은 시간에 지나치게 많은 요청을 쏟아내는 대상은 잠시 속도를 늦추게 했다. 정상적인 사람이라면 그렇게 빠르게 요청할 리 없으니, 이 제한은 봇의 폭주를 걸러냈다. 정당한 사용자에게는 영향이 없으면서 과도한 요청만 눌렀다. 문을 닫는 게 아니라 문턱을 두는 방식이었다. 정상적인 이용은 그 문턱에 걸리지 않을 만큼 여유 있게 잡되, 사람이라면 도저히 낼 수 없는 속도의 요청만 걸러지도록 기준을 조정하는 것이 이 방식의 관건이었다.
3. 검색이 만든 폭격
봇이 특히 아프게 건드린 곳은 검색이었다. 검색은 콘텐츠를 뒤져야 해서 본래 무거운 기능인데, 봇이 온갖 검색어로 마구 요청하면 그 무거운 조회가 폭증했다. 사람이라면 하지 않을 방식으로 봇이 검색을 두들기니, 검색 하나가 서버 부담의 큰 원인이 됐다. 나는 검색을 봇으로부터 지켜야 했다.
먼저 검색 방식 자체를 조회에 유리하게 바꿨다. 콘텐츠 전체를 매번 훑는 방식은 봇의 폭격에 취약했다. 대신 미리 색인을 만들어두고 그 색인만 조회하는 방식으로 바꾸니, 같은 검색이 훨씬 가벼워졌다. 봇이 두들겨도 조회가 색인 안에서 끝나니 부담이 크게 줄었다. 검색의 구조를 바꿔 폭격을 견디게 한 것이다.
지나치게 짧거나 무의미한 검색어는 아예 처리하지 않았다. 봇은 종종 한두 글자짜리 검색어를 마구 던졌는데, 이런 검색은 결과가 너무 많아 조회가 무거웠다. 그래서 일정 길이 미만의 검색은 무거운 조회로 이어지지 않게 막았다. 정상적인 검색은 대개 충분히 길기 때문에 사람에게는 불편이 없었다. 무의미한 폭격만 걸러졌다.
검색 결과도 캐시로 보호했다. 같은 검색어가 반복되면 그 결과를 잠시 저장해두고 재사용했다. 봇이 같은 검색을 반복해도 조회는 처음 한 번만 일어나고 나머지는 캐시로 응답했다. 반복적인 봇의 특성이 오히려 캐시에 유리했다. 폭격을 캐시로 흡수하니 서버는 평온을 유지했다. 봇의 반복성을 역이용한 셈이었다.
4. 캐시로 봇을 흡수하기
봇 대응의 핵심 전략은 결국 캐시였다. 봇의 요청 대부분은 공개 콘텐츠를 향했고, 공개 콘텐츠는 캐시할 수 있었다. 봇이 아무리 많이 요청해도 캐시된 응답으로 처리하면 무거운 조회가 일어나지 않았다. 봇의 요청을 캐시가 흡수하는 구조를 만드니, 봇 트래픽이 늘어도 서버 부담은 크게 늘지 않았다.
봇은 조회를 유발하지 않도록 특별히 취급하기도 했다. 예를 들어 봇의 방문은 사람의 방문과 달리 조회수를 올릴 필요가 없었다. 봇 방문마다 기록을 남기면 그것도 부담이었다. 그래서 봇으로 판단되면 이런 부수적인 기록을 건너뛰게 했다. 봇에게는 콘텐츠만 보여주고 불필요한 처리는 생략하니 부담이 줄었다.
봇을 완전히 막지 않은 건 의도적이었다. 검색 엔진 크롤러를 막으면 사이트가 검색에 안 나오니, 봇을 쫓아내는 게 능사가 아니었다. 대신 봇이 와도 부담을 안 주게 만드는 쪽을 택했다. 캐시로 흡수하고, 무거운 조회를 막고, 불필요한 처리를 생략하니 봇이 많아도 견딜 만했다. 막는 대신 감당하는 전략이었다.
이렇게 하니 봇과 사람이 공존할 수 있었다. 필요한 봇은 사이트를 잘 훑어 검색 노출에 기여했고, 그러면서도 서버에는 부담을 덜 줬다. 사람 사용자는 봇 때문에 느려지지 않고 쾌적하게 이용했다. 봇을 적으로 보지 않고 관리 대상으로 보니 답이 나왔다. 공존의 조건은 부담의 관리였다. 봇을 없애야 할 골칫거리로만 여겼다면 검색 노출과 부담 사이에서 계속 갈등했겠지만, 부담만 관리하면 함께 지낼 수 있는 대상으로 바라보니 오히려 해법이 선명해졌다.
5. 안 보이는 부하를 보기
봇 문제에서 얻은 가장 큰 교훈은 안 보이는 부하를 봐야 한다는 것이었다. 사람 사용자의 경험만 보면 봇이 만드는 부담은 안 보였다. 트래픽을 뜯어보고 나서야 그 절반이 봇이고, 그들이 조용히 자원을 갉아먹고 있었다는 걸 알았다. 재보지 않았다면 영영 몰랐을 부하였다.
이 부하는 성능뿐 아니라 비용으로도 나타났다. 봇의 무거운 조회가 자원 사용량을 밀어 올려서, 무료로 쓸 수 있는 한도를 위협했다. 사람 트래픽만 보면 여유로운데 봇까지 더하면 아슬아슬했다. 성능과 비용이 맞물린 문제라, 봇을 다스리는 것이 곧 비용을 지키는 일이기도 했다. 나는 이 둘을 함께 봤다.
봇을 다루며 나는 트래픽을 사람과 봇으로 나눠 보는 습관을 들였다. 접속자 숫자 하나로 뭉뚱그리면 진실이 안 보였다. 사람은 얼마고 봇은 얼마인지, 각각이 어떤 부담을 주는지 갈라 봐야 정확한 판단이 됐다. 이 구분이 다음 편에서 다룰 측정 도구의 핵심 주제이기도 하다. 무엇을 세고 있는지 아는 것이 시작이었다.
이렇게 사람과 봇을 아우르는 성능 관리를 살펴봤다. 그런데 이 모든 것의 바탕에는 정확한 측정이 있었다. 봇을 발견한 것도, 병목을 찾은 것도 다 재봤기 때문이었다. 다음 편에서는 내가 성능을 실제로 어떤 도구로 어떻게 쟀는지, 그 측정 도구의 함정까지 포함해 구체적으로 정리하겠다.