최근 같이 스터디를 진행하는 개발자분께 내 프로젝트의 상품 목록을 불러오는 시간이 매우 길다는 피드백을 받았다.
이전에도 나는 화면 로딩이 느리다는걸 인지하고 있었지만, 무료로 배포했기때문에 느린건 어쩔수 없다고 생각하고 있었다. 하지만 이렇게 직접적으로 피드백을 받으니 이 문제가 이전과는 다르게 다가왔다.
이를 개선하기 위해 다양한 방법을 조사하여 캐싱, 이미지 최적화, 코드 개선 등의 방법을 고려하게 되었고, 캐시를 적용하기로 결정했다. 캐시는 자주 사용되는 데이터를 임시로 저장하여 액세스 속도를 향상시키는 기술로, 매우 효과적이고 즉시 적용할 수 있기 때문에 현 상황에서 가장 적절한 방법이라고 생각했기 때문이다.
캐시는 간단하게 말하면 DB에 자주 접근해서 가져오는 데이터를 임시로 저장해둔 뒤, 이후 동일한 데이터 요청이 들어왔을 때, DB에 접근하지 않고 저장해둔 데이터를 주는 방식이다.
이러한 캐시를 이용한 서비스는 성능적인 측면에서 많은 이점을 얻을 수 있다. DB에 접근하지 않기 때문에 불필요한 쿼리 작성을 줄일 수 있기 때문이다.
캐시는 아무 데이터에나 적용한다고 좋은것이 아니다. 그럼 어떤 데이터에 적용하는게 좋을까?
캐싱은 자주 사용하는 데이터에 가장 효과적이다. 자주 사용되는 데이터의 복사본을 저장함으로써 이후 데이터의 요청이 올 때마다 원본의 데이터를 주기 위해 DB에 접근하는 것을 막아 성능 향상을 할 수 있다.
데이터를 반환하기 위해 복잡한 로직이나, DB에서 데이터를 가져오기 위해 쿼리문이 복잡한 경우에도 캐싱을 적용하면 성능 향상을 기대할 수 있다. 캐싱을 적용하면 다시 복잡한 계산을 할 필요가 없기 때문이다.
만일 데이터가 자주 업데이트 된다면 캐싱하기 적절한 데이터가 아니다. 자주 업데이트 되는 데이터에 캐싱을 적용하면 데이터 적합성 문제가 발생하게 된다.
데이터 정합성이란?
데이터 정합성은 같은 데이터임에도 불구하고 캐시와 DB에 있는 데이터의 정보가 다른 상황을 말한다.
예를 들면 상품 조회를 하는데 처음 상품 금액이 10,000원일때 조회하였는데 금액을 변경하면 DB에는 상품 금액이 9,000원으로 저장이 된다. 만일 이 상황에서 또다시 상품 조회를 하게 되면 캐시를 반환하기 때문에 사용자는 10,000원을 조회하게 된다.즉, 10,000(캐시) != 9,000(DB) 데이터가 일치하지 않는 데이터 정합성 문제가 발생하게 된다.
캐시 관리 전략을 선택할 때 가장 먼저 고려해야 할 요소는, 캐시 데이터를 저장할 스토리지를 서버가 자체적으로 소유하고 있을지, 외부 서버에 캐시 저장소를 따로 둘 지에 대한 부분이다.
캐시 저장소를 서버에 두는 방식을 Local Cache, 외부 캐시 저장소를 두는 방식을 Global Cache라고 한다.
저장 위치의 차이로 인해, Local Cache와 Global Cache는 다양한 상황에서 성능적 차이를 보인다.
Local Cache와 Global Cache의 특징을 각각 알아보겠다.
캐시 데이터를 서버 메모리상에 두는 것의 가장 큰 장점은, 무엇보다도 속도가 빠르다는 점이다. 캐시를 내부 저장소에 저장하면 네트워크 통신을 통해 캐시 저장소에 접근하고, 데이터를 가져오는 과정 등의 오버헤드가 없기 때문에 Local Cache의 데이터 읽기 속도는 현저히 빠르다.
오버헤드
실제 데이터를 저장하는 데 사용되는 메모리 양보다 추가로 필요한 메모리 양.
보통 다음과 같은 요인에 의해 발생한다.
- 키와 값의 크기
- 만료 설정
- 데이터 구조
오버헤드는 캐시 서버의 메모리 사용량을 증가시킬 수 있으므로 고려해야 할 중요한 요소이다.
Local Cache는 단일 서버 인스턴스에 캐시 데이터를 저장하기 때문에, 서버의 인스턴스가 여러 개일 경우 서버 간 캐시 데이터가 일치하지 않아 신뢰성을 떨어뜨릴 수 있다.
캐시 일관성을 유지하기 위해 동기화를 한다고 하더라도, 추가적인 비용이 발생한다. 더군다나 서버의 개수가 늘어날수록, 자신을 제외한 모든 인스턴스와 동기화 작업을 해야 하기 때문에 비용의 크기는 서버의 개수의 제곱에 비례하여 증가한다.
Global Cache는 외부 캐시 저장소에 접근하여 데이터를 가져오기 때문에, 이 과정에서 네트워크 I/O 비용이 발생한다. 하지만 서버 인스턴스가 추가될 때에도 동일한 비용만을 요구하기 때문에, 서버가 고도화될수록 더 높은 효율을 발휘한다.
Global Cache는 모든 서버의 인스턴스가 동일한 캐시 저장소에 접근하기 때문에, 데이터의 일관성을 보장할 수 있다. 데이터의 일관성이 깨지기 쉬운 분산 서버 환경에 적합한 구조이다.
Local Cache와 Global Cache의 특성을 고려했을 때, 어떤 기술을 선택해야 할지에 대한 기준은 "데이터의 일관성이 깨져도 비즈니스에 영향을 주지 않는가?"라고 생각한다.
예를들어 카테고리처럼 정보가 늦게 반영된다고 해서 큰 문제가 발생하지 않는 경우는 서버 자체적으로 로컬 캐싱을 하는 것이 좋다. 하지만, 상품 데이터 같이 가격 변동이 서비스 신뢰를 손상시키고, 법적문제까지 이어질 수 있는 경우는 속도보다 동기화가 중요하기에 동기화가 확실하게 보장되는 Global Cache를 사용하는 것이 좋다.
내 프로젝트의 경우에는 쇼핑몰이기 때문에 상품데이터나 고객 정보 및 주문 내역과 같은 데이터를 저장하고 사용할때 일관성이 있는게 중요하다. 따라서 Global Cache를 사용할 것이다.
사실 내 프로젝트는 단일 서버이기 때문에 로컬 캐시를 사용하는 것이 더 효율적이다. 하지만 포트폴리오는 내가 원하는걸 만들어보고 경험해볼 수 있다는 특성이 있으니 그 이점을 이용하여, 현재 프로젝트가 확장될 가능성이 있다고 가정하고 작업을 하였다.
이제 캐싱을 프로젝트에 적용해보자.
나는 어떤 데이터에 캐싱을 적용하는 것이 좋을까 생각해 보았는데, 쇼핑몰 상품목록과 카테고리를 요청하는 메서드에 캐싱을 적용하면 괜찮을 것 같다는 생각을 했다.
카테고리는 화면이 바뀌어도 대부분의 경우 화면에 상시 노출되는 요소이다. 또한 변경될 일이 거의 없고 바뀐다해도 크게 문제가 되지 않는다.
상품 목록은 사실 고민이 좀 됐다. 가격이 바뀌거나 상품이 추가되는 경우에는 페이지마다 캐싱을 했기에 페이지마다 생성 시점이 다르면 중복 상품이 노출되는 문제가 발생할 수도 있기 때문이다.
하지만 사실상 내 프로젝트 상품이 바뀌거나 가격이 바뀔 일이 없기 때문에 현재 상황에 맞게 빈번하게 사용되는 상품목록 조회 기능에 캐싱을 적용하게 되었다.
만약 이러한 사항들을 고려하게 된다면 가격이 바뀌는 경우는 데이터가 바뀔 때마다 캐쉬를 업데이트 하고, 중복상품 노출 문제는 동일한 조건의 상품목록을 한 시점에 생성하게 하여 해결할 수 있을 것 같다.
우선 캐시 적용 전 상품 조회 기능의 성능을 확인해 보았다.
성능 테스트는 JMeter를 이용하였다.
JMeter 사용법
우선 캐시를 적용하기 전 응답 속도를 측정해 보았다.
JMeter는 아래와 같이 설정을 해주고 테스트를 해보았다.
10개의 Thread를 생성하고 Thread의 반복 횟수를 100으로 설정해 총 1000번 api 요청을 하도록 설정해 주었다.
응답 속도가 일정하지는 않지만 평균적으로 415ms로 응답하는 것을 확인할 수 있었다.
JMeter 위와 동일한 설정으로 테스트를 진행해 보았다.
이번에는 평균적으로 206ms으로 응답하는 것을 확인할 수 있다.
어느 정도 성능을 개선했는지 %로 수치화 했을 때 ((206ms - 415ms) / 415ms) * 100 = -50%이라는 결과가 나온다.
즉, 캐시를 적용함으로써 약 50% 성능 개선을 했다고 볼 수 있다.
이번 경험을 통해 단순히 기능적인 부분만을 개선하는 것뿐만 아니라 성능적인 부분도 중요하다는 것을 배웠다.
캐시를 적용한 것만으로 페이지 로딩 속도가 50% 향상되어 훨씬 빠른 속도로 페이지에 액세스할 수 있었다. 지속적으로 사용하는 사용자의 입장에서는 체감상 차이가 더 클거라는 생각이 든다. 이러한 부분에서 사용자 경험을 개선하는 것이 중요하다는 것을 알게 되었다.
앞으로도 이러한 지식들을 늘려나가며, 사용성을 개선시켜 좋은 개발자가 되려고 노력해보자