728x90

서버 조회 성능을 향상하는 방법으로는 Caching을 선택할 수 있는데,

Caching이 무엇이고, 어떤 상황에 도입하는 것이 적합한지 그리고 마지막으로 적용 안 했을 때와 적용했을 때의 성능 차이가 얼마나 차이 나는지 검증해 보겠습니다.

 

Caching이란?

캐싱은 자주 접근하는 데이터를 캐시라는 고속 데이터 저장소에 저장하고 접근하여 이후 동일한 데이터를 요청할 시 고속 데이터 저장소에 접근하여 데이터를 사용하는 방법입니다.

 

이는 많은 리소스를 요구하는 데이터베이스 커넥트 비용을 줄여주게 되어 서버와 데이터베이스에 가해지는 부하를 분산할 수 있다는 장점이 있습니다.

 

그럼 단점은 무엇일까??

 

우선 데이터의 일관성에 문제가 발생할 수 있습니다.

캐싱이란 것은 데이터베이스에 존재하는 데이터를 가져와 메모리에 올려두고 다음 요청 시 메모리에 존재한다면 메모리에서 해당 데이터를 가져오는 방식을 사용하게 되는데, 이때 데이터베이스에 존재하는 원본 데이터가 추가, 수정, 삭제와 같은 작업이 이루어진다면, 메모리상에 존재하는 데이터와 데이터 일관성이 깨져 유저에게 저퀄리티 즉 상한 데이터를 제공하게 됩니다.

 

이는 서비스 신뢰도의 저하를 야기할 수 있으며, 데이터의 일관적이지 않은 상황으로 인해 큰 오류로 이어질 수 있는 위험한 상황이라 생각합니다.

 

또한, 캐싱을 한다는 것 자체가 메모리에 데이터를 띄워놓고 사용한다는 의미인데, 이는 또 다른 메모리 자원을 사용하는 것이므로 무분별한 캐시 사용보다는 적합도를 추측 및 성능 검증을 통해 조심하게 사용해야 한다 생각합니다.

 

그리고, 요즘 많이 사용하는 k8s를 적용할 경우 하나의 서버를  여러 개의 Pod로 인스턴스를 할당해 운영할 수 있습니다.

이때, Local Cache를 사용하게 된다면 Pod들마다 저장하는 Local Cache의 정합성이 깨질 수 있습니다.

 

이렇듯 본인의 서버가 단일/복수의 인스턴스로 실행할 것인지, 수정이 많이 이뤄지는 데이터인지 등에 따라 제대로 사용한다면 유의미한 성능향상을 이뤄낼 수 있습니다.

 

Concert서비스 어디에 적용하는 것이 적절할까?

위에서 보았듯 여러 상황을 고려하며 어디에 적용할지 분석해 보겠습니다.

 

Local Caching VS Global Caching

Spring을 사용하시는 분이라면 Spring에서 제공해 주는 @Cacheable 어노테이션을 사용하여 캐싱을 적용해 보셨을 것입니다.

 

Spring에서 제공하는 캐싱은 AOP기반으로 작동하며, 캐시 데이터는 ConcurrentHashMap 기반의 저장소를 제공하고 있습니다. 이는 무엇인가? ConcurrentHashMap은 Multi-Thread에서 사용가능하며, ThreadSafe 합니다. 이 말은 하나의 서버 인스턴스 내에 생성된 스레드들은 해당 캐시를 공유하고 있다는 의미입니다.

 

물론 Spring에서 제공하는 어노테이션을 활용한다면 간단하게 캐싱을 적용할 수 있지만 이는 멀티 인스턴스 구조 즉 분산환경에서 Local Cache를 적용하는 것이 적합한가?? 는 다시 한번 고민해 볼 필요가 있습니다.

 

Global Cache는 이러한 관점에서 본다면 확실한 장점이 존재합니다.

외부 Storage를 사용하여 서버 인스턴스들이 해당 Storage를 접근함으로써 모든 인스턴스들은 캐시를 공유함으로써 데이터 정합성이 보장되게 됩니다.

 

또한 Local Cache의 경우 저장 하는 메모리가 JVM상에 존재하는 메모리를 활용하여 캐시가 많아질수록 서버 성능에 영향을 미칠 수 있는 반면 Global Cache는 외부에 존재함으로 서버 성능에 악영향은 없을 것으로 판단됩니다.

 

다만 따로 메모리 케쉬 서버를 사용하는 만큼 추가적인 비용과 캐싱 구현이 Local Caching보다 복잡한 점을 염두하여 어떤 캐시를 사용할 것인지 선택하면 될 거 같습니다.

 

Caching 적용 API 적합도 분석

캐싱은 보통 조회가 자주 일어나는 부분, 연산이 이뤄진 데이터를 자주 조회하는 경우에 사용한다고 생각합니다.

그럼 콘서트 서비스를 기준으로 한번 적합도를 분석해 보도록 하겠습니다.

1. 콘서트

콘서트는 콘서트 정보를 관리하는 Concert, 콘서트의 실질적인 open일 등 자세한 정보를 관리하는 ConcertSeries, 좌석정보를 관리하는 ConcertSeat 이렇게 3개로 분리하여 관리 중입니다.

 

위 3가지의 항목에 대해 적합도를 분석하겠습니다.

  • Concert
    적합도:
    판단 근거:  
    CRUD에 의거하여 생각해 보자면, 콘서트는 CUD의 빈도가 크지 않을 것이라 판단되는 도메인입니다.
    그 이유는 현재 CR API만을 제공하고 있고, 콘서트의 Title 등만을 관리할 뿐 실질적인 데이터는 ConcertSeries에서 관리하고 있기 때문입니다.

    하지만 유저가 가장 많이 접근하는 API일 것을 생각해 보면 Cache를 적용하여 조회성능을 향상하고, 생성될 때 Cache를 재업로드 하는 방식은 매우 적절할 것으로 판단됩니다.

    그러므로 Caching을 적용하고 콘서트 데이터를 생성할 때 Cache정보를 최신정보로 갱신하도록 하겠습니다. 
  • ConcertSeries
    적합도:
    판단 근거:
    ConcertSeries는 콘서트의 상세 정보, 신청 기간 등을 관리합니다.
    이는 CRUD 중 CU의 기능을 활용 중이며, 이는 자주 Update 된다면 Cache를 거는 것이 오히려 악조건이 될 수 있습니다.

    하지만, ConcertSeries의 경우 그렇게 자주 수정이 이루어질 것 같지 않아 Caching을 적용하되, 생성과 수정 시 갱신하는 방향으로 적용하겠습니다.
  • ConcertSeat
    적합도:
    판단 근거:
    콘서트 좌석의 정보를 관리하고 있는 도메인입니다.
    좌석의 예약 여부를 상태로 관리하고 있으며, 이 상태 데이터로 인해 Caching을 ConcertSeat에는 적용하지 않겠습니다.


2. 포인트

  • Point
    적합도:
    판단근거:

    포인트는 자주 변경되는 사항이 아닐 수 있지만.
    금전적인 부분을 다루는 만큼 데이터의 일관성이 무엇보다 중요하다 생각합니다.
    이로 인해 Point에는 Caching을 적용하지 않겠습니다.

3. 임시예약

  • TemporaryReservation
    적합도:
    판단근거:
    임시예약 API입니다.
    해당 도메인의 경우 5분 안에 결제가 되지 않는다면 취소되는 요구조건으로 인해 주기적으로 많은 Update가 발생할 수 있습니다. 또한 결제가 될 경우에도 상태가 변경되므로 Cache를 적용하는 것은 적합하지 않다 생각하였습니다.

4. 예약

  • Reservation
    적합도:
    판단근거:
    예약 데이터의 경우 생성하고 나면 환불하지 않는 한 수정 및 삭제가 이루어지지 않는다고 생각하여 구현하였습니다.
    즉 캐시 갱신이 이뤄지는 시점은 생성과 삭제 부분만 있다고 생각합니다. 하여 캐싱이 적용되기 합당하다 생각합니다.

성능테스트

성능 테스트 도구: K6

vus: 100

duration: 60s

콘서트 조회

Concert 1000건의 데이터를 가지고 테스트를 진행하였습니다.

캐시 미적용

캐시 적용

  캐시 없음 캐시 있음 차이
수신 데이터 730 MB (12 MB/s) 775 MB (13 MB/s) +45 MB (+1 MB/s)
송신 데이터 481 KB (7.9 KB/s) 511 KB (8.4 KB/s) +30 KB (+0.5 KB/s)
HTTP 요청 수 5532 5876 +344
반복 기간 평균=1.09초, 중간=1.01초, 최대=1.84초 평균=1.03초, 중간=1.01초, 최대=2.775초 -0.06초, 0초, +0.935초
HTTP 요청 대기 중 평균=91.12ms, 중간=12.16ms, 최대=840.43ms 평균=28.47ms, 중간=5.28ms, 최대=1.766초 -62.65ms, -6.88ms, +925.57ms

 

동일한 환경 100명의 가상 유저가 1분 동안 요청을 한경우 캐시를 사용할 경우 평균 HTTP요청 대기시간이 크게 줄고, 요청 수를 더 많이 처리한 것으로 보아 요청한 데이터를 응답받는 속도가 빠르단 것을 알 수 있었습니다.

 

콘서트 시리즈 조회

ConcertSeries데이터 1000건을 사용하여 데이터를 저장하고 있으며, ConcertId로 검색하여 조회합니다.

캐시 미적용

캐시 적용

  캐시 없음 캐시 있음 차이
수신 데이터 2.8 MB (45 kB/s) 2.9 MB (47 kB/s) +0.1 MB (+2 kB/s)
송신 데이터 748 KB (12 KB/s) 773 KB (13 KB/s) +25 KB (+1 KB/s)
HTTP 요청 수 5710 5900 +290
반복 기간 평균=1.05초, 중간=1.01초, 최대=1.38초 평균=1.02초, 중간=1.초, 최대=1.42초 -0.03초, 0.01초, +0.6초
HTTP 요청 대기 중 평균=52.88ms, 중간=12.32ms, 최대=382.91ms 평균=24ms, 중간=1.65ms, 최대=423.54ms -28.88ms, -10.67ms, +40.63ms

 

100명의 유저가 1분동안 콘서트 아이디로 시리즈를 조회하였을 때를 환경으로 설정하여 테스트해 보았습니다.

캐시를 적용하기 전보다 290건의 트래픽을 수용할 수 있는 것으로 보아 서버 성능이 향상되었음을 확인할 수 있었습니다.

 

위 2건의 캐시 적용사례를 확인해 보면 동일한 유저와 동일한 시간 동안 요청을 보냈을 때 더 많은 요청을 수용할 수 있는 것을 확인할 수 있었습니다.

 

단순 Query로 인해 성능적 차이가 크게 보이지 않았지만, 복잡한 검색기능을 도입할 시 큰 차이를 보이게 될 것이라 생각됩니다.

 

추후 다이내믹한 검색 조건을 추가하여 캐싱을 적용하였을 때 복잡한 Query에 대한 성능 분석을 추가적으로 작성하도록 하겠습니다. 


Reference

https://docs.spring.io/spring-boot/reference/io/caching.html

https://docs.spring.io/spring-data/redis/reference/redis/redis-cache.html
https://www.baeldung.com/spring-cache-tutorial

https://www.baeldung.com/spring-boot-redis-cache

 

 

728x90

+ Recent posts