안녕하세요 이번글은
Home-Delivery프로젝트에서 주문을 하는과정 중 반복적인 읽기 요청을 하는 부분에 대해서 캐시를 적용하여 성능을 향상시킨 과정을 적었습니다.
문제상황
주문까지의 유저들의 사용과정을 예상한다면
앱에접속 -> 카테고리 선택 -> 매장리스트 조회 -> 매장 상세조회 메뉴,메뉴그룹 조회 -> 메뉴옵션 조회 -> 장바구니담기 -> 주문의 과정이 이루어집니다.
이와같은 작업들은 주문하기전까지 한번에 이루어지는경우도있지만, 여러사이클이 반복될거라 생각됩니다.
동일한 데이터에 대한 반복적인 DB 요청이 발생합니다.
DB요청이 많으면 발생하는 문제는 DB접근은 커넥션 뿐만 아니라 DB자체에서의 연산도 느리니 DB요청을 적게하면 좋습니다.
요구사항
- 매장목록, 메뉴목록, 메뉴그룹목록, 메뉴옵션목록에 대한 정보를 어디에 캐싱할 것인지 ?
- 사장님이 수정한다면 정합성이슈가 발생 이에따른 대책은?
생각한 방법
- 변화가 적은 데이터들은 캐싱을 한다면 성능도 개선이 되고 db접근을 줄일수있습니다.
- 주문과정 까지의 데이터들에서는 "조회" 라는 공통점이 존재합니다.
그래서 Redis를 캐시저장소로 사용하여 성능을 개선하였습니다.
결과 (RAMP UP을 진행하고 나온 결과입니다.)
적용 전
적용 후
88 ms -> 30 ms (193%)로 성능을 개선되었습니다.
🤣사용하면서 겪었던 문제
Redis 저장소 문제
저희는 레디스에 Session저장과 Cache저장 장바구니까지 등 Redis에 저장을 많이하게 됩니다. 이에따라 규모가 커진다면
캐싱되는 데이터와 많은 요청을 한대의 Redis에 저장이 되면 용량과 레디스의 싱글 스레드 특징으로 CPU코어를 하나밖에 사용하지 못하기때문처리 문제가 생깁니다. 따라서 만약 Redis가 다운이 되는상황이오면 SPOF문제가 생깁니다.
생각한 방법
Redis에 저장된 각각의 데이터들이 분리되어도 영향이 가지않을거라고 생각했습니다.
또한 용량문제가 있기때문에 Redis를 여러개 사용하자 라는 생각을하였습니다.
Redis저장소 여러개 사용한다면 CPU코어를 하나밖에 사용하지못하는 문제를 CPU를 여러개 사용하는 것과 같은 상황을 만들수있다고생각했습니다. 왜냐하면 저장소가 다르기떄문에 요청분산이 되기떄문입니다.
따라서 비용적 문제가 발생하지만, SPOF문제로 서버가 멈추는문제를 해결하는 이득이 더 많다고생각하여
Redis저장소를 분리하여 문제를 해결하였습니다.
캐시 데이터 Update이슈
캐시저장소에 존재하는 데이터가 있는데 사장님이 가격을 바꾸거나 등 주문과정에서의 조회목록에 있는 정보가 변경될 수 있습니다. 따라서 캐시된 데이터도 변경시켜주지않으면 잘못된 캐시정보를 가지고있는 문제가 발생합니다.
생각한방법
방법1. TTL시간 짧게하기
TTL을 시간을 매우짧게한다면 캐시로인한 이점을 살라지못합니다. 왜냐하면 캐시된 데이터가 1초면 1초마다 DB에 접근해서 캐시데이터를 채워야하기때문입니다. 이는 캐시를 사용하지않는것과 같다고 생각했습니다.
방법 2. 스케줄링을 통한 지속적 업데이트.
캐시정보를 지속적으로 업데이트 한다면, 캐시를 위해 스케줄링을 해야합니다.
스케줄링 서버를 한대만 돌린다면 거기에 만약 트래픽이 간다면 부하가 생기는 문제가 발생하며 이를또 해결하기위해 스케줄링 서버를 증설하는방법인데 비용적문제가 발생하며, 짧게 캐시데이터를 업데이트를 한다면 Redis캐싱을 하는 이유와 적합하지않다고 판단됬습니다. 왜냐하면 다시 업데이트된 정보를 넣어줘야하는데 싱글스레드인데 스케줄링을 3초로한다면 3초마다 Redis에 대량의 데이터가 들어가서 처리하는데 시간이 추가적으로 걸리기때문에 이는 문제를 해결할수없다고 판단했습니다.
방법 3. 변경이 일어날때만 다시 데이터를 캐싱하자.
변경이 일어날때만 변경된 데이터만 변경을 해주는 방법을 선택한다면 방법2에서 해결하기위해 스케줄링을 돌릴필요도없으며 Redis에 대량데이터를 처리하는 일이 빈번하게 발생하지않을것입니다.
따라서 방법3을 통해 해결했습니다.
▶ 방법3의 따른 문제 @CacheEvict vs @CachePut
- @CacheEvict은 캐시를 모두 날리고 @Cacheable 에서 다시 메뉴에 대한 DB 조회해서 다시 캐싱하는 작업을합니다.
- @CachePut을 사용한다면 변경이 일어난 부분만 업데이트입니다.
코드로 두개다 실험해보았습니다. 저는 맨처음에 CachePut을 사용하는게 효율적이라고 생각했습니다.
그러나, 코드와같이 CachePut을 사용하면 값을 return해주어야합니다. 그렇지않으면 캐싱된 key값의 데이터가 null로 변경이 됩니다. 이에 CachePut의 문제는update로직에 데이터 변경 + 데이터 조회 2가지가 들어가게됩니다.
이문제는 하나의 메서드가 여러책임을 갖는것과 같다고생각했습니다. 따라서 객체지향 설계 원칙인 단일책임원칙을 지키지않은것입니다.
또한 메서드명은 updateMenu인데 조회 로직이 들어가게되어 메서드명과 부합하지않으며, 조회구문이 오류가 생긴다면 update부분까지 오류가 발생되는 문제가 발생됩니다.
이러한 문제보다 @CacheEvict을 사용하여 업데이트가 일어나면 캐시된 데이터를 지우고 다른유저가 조회할때 캐싱작업을 하는게 문제를 해결할수있다고생각하여 @CacheEvict을 선택해 해결했습니다.
장바구니에 담았을때 정합성이슈
캐시데이터를 이용하면 만약 사장님이 가격을 바꾸는 경우가 존재합니다.
이에따라 장바구니에 저장되는것은 가격이 바뀌기전에 가격이 저장됩니다.
그렇게된다면 만약에 유저가 장바구니에 1000원을 담아두고 물가상승으로 2000원이 되어 주문이 이루어진다면 사장님은 손해를 보게되는 상황이 발생합니다.
생각한방법
※ 장바구니 로직은 장바구니 목록에서 -> 주문하기 -> 결제하기(주문생성)로 이루어집니다.
방법1. 장바구니 목록을 조회할때마다 담은 가격과 DB에 저장된 가격을 비교하는 로직을 만들자.
그러나 이방법을 사용하게된다면 DB에 요청을 줄이기위한 목적으로 장바구니를 Redis저장소로 사용하는 목적과는 달라지게 된다고생각하여서 제외했습니다.
방법2. 장바구니 목록에서 -> 주문하기로 갈때 검사하자 (주문하기창)
이방법의 문제점은 바로 주문하기에서 주문정보를 입력하는 경우가존재합니다. 따라서 이러한 경우나 이창을 오래두고 나중에 주문하는 경우가 발생하면 정합성이 맞지않는 문제가 발생합니다. 그래서 제외했습니다.
방법3. 주문하기 -> 결제하기 로 갈때 검사하자
주문하기에서 결제하기로 간다면 최종적이기때문에 이과정에서 정합성검사를 한다면 문제가 발생하지않을 것이라고생각했습니다. 또한 방법1과같이 매번확인하는게 아니라 최종적 한번만 확인하면 되기때문입니다.
따라서 방법3을 선택하여 해결하였습니다.
느낀점
긴글 읽어주셔서 감사합니다.
이번에 캐시를 적용하면서 느낀것은 어느 기술을 선택하면 그에따른 문제점과 고려해야할 부분이 많이 존재합니다.
이러한부분을 잘 파악해서 기술 적용을 해야한다는것을 몸소 느끼게 되었습니다.
'Delivery' 카테고리의 다른 글
FCM 푸시알림 구현 이슈 - 비동기 처리(성능개선) (0) | 2022.08.23 |
---|---|
레디스 테스트코드 문제점 - TestContainer도입 (0) | 2022.08.19 |
Redis 장바구니 - 2 (Redis pipelining) (0) | 2022.08.18 |
Redis를 이용한 장바구니 - 1 (0) | 2022.08.15 |
리펙토링 디비조회 (0) | 2022.08.09 |