출시한 스타트업 서비스가 인기를 끌어 사용자가 폭증하게 되면 비즈니스의 실마리를 찾았다는 생각에 기쁘면서도, 동시에 트래픽 부하나 서버 비용 증가 등을 감내해야 하기에 머리가 아픕니다. 우리가 알고 있는 유명한 스타트업들은 대게 이런 사용자 폭증의 과정을 거쳐왔고 이때 서비스가 느려지거나 접속이 안 되는 과정을 사용자로서 지켜봐 왔기에 내가 만드는 서비스가 저렇게 성공하기를 바라면서 동시에 안정적으로 많은 사용자들을 감당할 수 있기를 바랍니다.
라고 생각하실 수 있겠지만 사용자 증가에 따라 서버를 늘리는 것은 의외로 꽤 나중의 대응책입니다. 데이터베이스 인덱스 추가, 이미지 리사이징, 비효율적인 쿼리 개선, 데이터 캐싱, 페이지 캐싱 등을 순차적으로 적용하는 소프트웨어적인 최적화를 먼저 하고, 그래도 서비스가 느리다면 그때부터 서버의 사양을 올리거나 대수를 늘리는 하드웨어적인 확장을 해야 합니다.
가장 먼저 처리할 부분은 데이터베이스에 인덱스를 추가하는 것입니다. 인덱스를 추가한다는 말은 데이터베이스 상의 데이터들을 자주 사용하는 칼럼 중심으로 정렬해두는 것을 말합니다. 개발자들이 적절한 칼럼에 인덱스를 추가하는 스크립트를 한 번 실행하면 되는데, 이 작업만으로도 데이터베이스 읽기 속도가 개선되어 서비스가 빨라집니다.
그다음 처리할 부분은 이미지 사이즈를 최적화하는 것입니다. 느린 사이트의 원인 중 상당히 높인 비율이 큰 이미지 사이즈 때문에 발생하고 있습니다. 10메가가 넘는 이미지를 리사이징 없이 저장하고 그대로 로딩하면 전체 페이지가 느리게 표시됩니다. 사용자는 몇 메가가 넘는 이미지를 스마트폰 카메라로 찍어서 그대로 업로드할 수 있지만 이를 서버에서 저장할 때는 적절한 버전의 이미지로 리사이징 해서 저장해야 합니다.
목록에서 보여줄 작은 썸네일 이미지, 상세 화면에서 보여줄 스마트폰 가로 사이즈(혹은 해상도에 따라 가로 사이즈의 2~3배, 파일 크기는 원본의 1/20 )정도의 이미지, 그리고 확대해서 보여줄 필요가 있을 때 그에 맞는 사이즈의 이미지까지 몇 개의 리사이징 된 버전을 저장해 두고 해당 페이지에서 적절한 사이즈의 이미지를 표시하면 페이지 로딩 속도를 몇 배까지도 개선할 수 있습니다.
10개의 이미지를 화면의 절반 정도 크기로 표시하는 경우, 10메가의 원본 이미지 그대로 사용하면 총 100메가의 이미지를 다운로드하여야지 페이지가 표시됩니다. 페이지가 표시되는데 10~20초가 걸릴 수 있습니다. 10메가의 고해상도 이미지라고 해도 표시되는 영역이 작으면 오히려 이미지가 깨져 보이게 됩니다. 이를 1024px 정도의 사이즈로 줄이게 되면 10메가인 원본 이미지보다 부드럽게 보이면서도 용량은 500K, 즉 10메가의 20분의 1에 불과하므로 총 5메가의 이미지만 다운로드하면 1초 이내에 페이지에 모든 이미지가 바로 표시될 수 있습니다.
그다음은 비효율적인 코드를 개선하는 것입니다. 특히 개발 초중반에 서비스를 빈번하게 수정하다 보면 데이터베이스에서 데이터를 가져오는 코드나 연산을 하는 코드, 화면에 표시하는 코드 등이 비효율적으로 작성되어 있을 수 있습니다. 몇 개의 데이터만 가져오면 되는데 전체 데이터를 로드하고 있는 경우도 있고, 일부 칼럼 데이터만 필요한데 전체 칼럼 데이터를 로드해오거나 재사용할 수 있는 데이터를 매번 다시 계산하고 있을 수도 있습니다. 비효율적인 코드 한 두 줄의 변경만으로 몇 배의 속도를 개선할 수 있습니다
한창 플랫폼이 개발 중일 때는 코드 최적화가 최우선 사항은 아닙니다. 출시까지 많은 기획 변경과 구조 변경이 있을 텐데 최적화부터 고려하다 보면 구현 및 변경이 어려워질 수 있습니다. 기획적인 의사결정이 어느 정도 완결되고 개발이 마무리되어 구조적인 변경이 더 이상 없을 때 최적화를 진행하는 것이 효과적입니다. 심지어 당장 속도가 느리지 않다면 일단 출시한 후 사용량 증가와 페이지 속도를 관찰하면서 지속적으로 코드를 개선해나가도 된다고 생각합니다. 모든 부분을 최적화하기보다는 많이 접속되면서도 느린 부분을 우선적으로 최적화하는 것이 개발 리소스와 비용을 줄일 수 있습니다.
통계나 정산 기능의 경우 주문 데이터 등 많은 데이터를 한 번에 꺼내와서 계산하고 가공해서 표시하게 됩니다. 과거 데이터의 경우 수정될 일이 적기 때문에 지난달 통계나 정산 데이터도 크게 변동이 없는 경우가 많은데 통계/정산 페이지를 열 때마다 수만 개의 데이터를 가져와 연산을 하다 보면 페이지 접속이 느려지게 됩니다. 주기적으로 통계/정산 데이터를 생성해낸 후 이를 별도의 데이터베이스에 저장해두었다가 꺼내오면 수많은 연산을 반복할 필요가 없어서 속도 개선이 됩니다.
또 다른 사례로, 상품에 대한 찜 개수나 리뷰 평균 점수를 매번 계산하게 되면 상품 목록 페이지를 표시할 때마다 목록 상의 상품들의 모든 찜 개수를 세고 모든 리뷰 데이터를 가져와서 연산을 해야 합니다. 데이터베이스에 많은 질의를 보내야 하기도 하지만 가져와서 개수를 세고 평균을 내는 연산도 필요하게 됩니다. 데이터베이스에 질의할 때 찜 개수나 리뷰 평균을 계산하는 쿼리를 작성할 수도 있지만 쿼리가 복잡해지고 웹 프레임워크의 ORM을 벗어나는 코드를 작성해야 합니다(코드 관리 난이도 증가).
좋은 해결책은 특정 상품에 새로운 찜이 일어날 때마다 해당 상품의 찜 개수를 데이터베이스에 저장해 두고, 리뷰도 새로 작성되거나 수정되었을 때만 리뷰 평균 점수를 계산해서 저장해두면 상품에 대한 찜과 리뷰 데이터를 매번 쿼리로 가져와 연산할 필요 없이 상품에 함께 저장되어 있는 찜 개수와 리뷰 평균 점수를 표시만 하면 됩니다.
사용자에게 자주 보이고 변경이 자주 일어나지 않는 페이지는 페이지 캐싱을 하면 역시 속도가 몇 배 개선됩니다. 뭐만 하면 속도가 몇 배 개선된다는 이야기가 현실성 없게 들릴 수도 있지만 실제로 비효율적인 페이지는 로딩되는 데 10초가 넘게 걸릴 수 있지만 여러 가지 소프트웨어적인 튜닝을 한 이후에 0.1초 이하로 응답속도가 개선되는 사례가 흔합니다.
페이지 캐싱은 웹 서버 또는 캐시 서버 단계에서 미리 저장해둔 페이지를 브라우저나 모바일로 보내게 되어 데이터베이스 쿼리나 연산, 웹 프레임워크의 여러 가지 처리들을 건너뛰게 됩니다. 단, 페이지 캐싱은 사용자별로 페이지나 데이터가 달라지는 개인화 영역 등에는 적합하지 않고, 상품 페이지나 콘텐츠 페이지처럼 빈번한 수정이 없고 접속량은 많은 페이지에 적용하면 효과적입니다.
캐싱은 사용자가 늘어 서비스가 느려지는 시점까지 최대한 미루었다가 적용하는 것이 나을 수 있는데, 일단 캐싱이 동작하는 상태에서는 개발과 테스트가 어려워집니다. 캐싱이 적용되어 있으면 코드를 수정한 페이지가 수정되지 않은 이전 상태로 보이기 때문에 한창 개발 중일 때는 캐싱이 개발 및 테스트 과정을 방해하게 됩니다. 또 캐싱을 적용한 후에 언제 캐싱 데이터를 갱신해야 하는지에 대한 만료 및 갱신 조건 등을 설정하는 것이 매우 까다롭기 때문에 데이터 수정/변경 정책이 확정되었을 때 적용되어야 안전합니다.
이와 같은 다양한 소프트웨어적인 성능 개선이 일차적으로 진행된 후에, 서버의 스펙을 높이거나 데이터베이스 서버, 웹 서버, 애플리케이션 서버, 에셋 스토리지, 캐시 서버, 채팅 서버, 큐 서버, CDN 서버 등의 시스템 레이어를 분리하고 이를 하드웨어적으로도 분리한 다음에서야 서버 대수를 늘리는 확장을 하게 됩니다. 마스터/슬레이브로 복사하고 데이터베이스를 쪼개는 등의 조치를 취하고요. 저희는 데이터베이스와 웹서버를 자동으로 확장해주는 클라우드를 사용하여 사용자의 급증에도 안정적으로 대응하고 있습니다. 사용자가 많지 않은 초기에는 몇 만 원 수준의 기본요금만 납부하면 됩니다.