본문 바로가기
개발 이야기

Redis로 장바구니 구현하기: 추상화의 편리함과 함정

by 신재권 2024. 8. 10.

우리 앱의 장바구니 기능은 기존에 클라이언트 캐시로 관리되고 있었다. 해당 방식은 서버 부하를 줄일 수 있다는 장점이 있지만, 몇 가지 심각한 단점이 있었다.

 

1. 캐시로 관리되다 보니 장바구니를 한 개밖에 가질 수 없었다.

-> 우리 앱은 사용자가 여러 마트를 이용할 수 있는데, 마트를 옮길 때마다 기존 장바구니 데이터를 지워야했다.

2. 클라이언트에서 장바구니 관련 오류가 발생하면 추적이 어려웠다.

-> 오류를 재현할 정보가 부족하여 디버깅에 많은 시간이 소요됐었다.

 

이러한 문제들을 해결하기 위해 장바구니를 서버로 이전하기로 결정했었다.

 

장바구니 서버 이전 시, 가장 먼저 고민한 것은 저장소 선택이였다.

RDB와 NoSQL 중 어느 것을 사용할지 고민했어는데, 다음 이유로 Redis를 선택했다.

1. 클라이언트에서 빈번한 호출이 예상

2. 우리의 RDB는 단일 DB라서 부하 분산 필요

 

Redis 자료 구조 중에는 Hash를 선택하였다.

1. String으로 저장할 경우 별도의 직렬화/역직렬화 코드를 작성해야 한다.

2. Spring Data Redis에서 제공하는 CrudRepository를 활용하면 빠른 개발이 가능할 것으로 판단했다.

 

CrudRepository를 활용해 데이터 처리 로직을 별도로 구현하지 않고, 비즈니스 로직에 집중 해 장바구니 기능을 빠르게 개발할 수 있었다.

 

그런데 개발 완료 후 테스트 과정에서 문제가 발생했다. 클라이언트에서 여러 기능을 연속으로 실행했을 때, 장바구니가 사라지는 현상이 나타났었다.

 

조회에 발생하는 명령어 들이다. HGETALL -> findById() + DEL + SET -> save() 메서드에 발생하는 실제 명령어이다.

문제의 원인을 파악하기 위해 Redis CLI 모니터링을 통해 특정 키를 감시하며 작업을 진행하였다. 그 결과 CrudRepository의 save() 메서드 호출 시 실제로는 DET + SET 방식으로 동작하는 것을 확인했다.

이로 인해 빠르게 여러 번 호출될 경우, 명령어가 의도한 순서대로 실행되지 않아 장바구니가 사라지는 버그가 발생했던 것이다.

또한, 수정 기능의 끝마다 save를 호출하고 있었는데, 실제 변경이 없는 경우에도 불필요하게 호출되는 문제도 추가로 파악하였다.

 

이 문제들을 해결하기 위해 다음 방법을 적용했다.

1. CrudRepository대신 RedisTemplate을 활용해 DEL + SET 연산에서 SET 연산으로 전환

-> 이를 통해 짧은 시간 내에 여러 번 호출해도 데이터가 사라지는 버그를 해결할 수 있었다.

2. AOP + ThreadLocal 기술을 활용해 더티 체킹 기능을 구현

-> 이를 통해 장바구니에 실제 변화가 있을 때만 save를 호출하도록 수정하였고, 불필요한 명령어 호출을 제거했다.

 

이번 경험을 통해 가장 크게 느낀 점은 Spring에서 제공하는 추상화된 기술을 제대로 학습하지 않고 사용하면 안 된다는 것이다.

편리함에 이끌려 내부 동작을 제대로 이해하지 않고 사용했다가 예상치 못한 문제를 마주하게 되었던 것 같다.

 

이를 계기로 어떤 기술을 사용하기 전에 그 기술의 실제 동작 방식을 철저히 파악하는 습관을 기르게 되었다.

또한, 성능 최적화를 위해서는 추상화 계층을 벗어나 더 낮은 수준의 제어가 필요할 수 있다는 것도 깨달았다.

 

이번 프로젝트를 통해 기술적으로 많이 성장할 수 있었다. 단순히 기능 구현에 그치지 않고, 성능과 안정성을 모두 고려한 개발의 중요성을 다시 한번 깨달았다.

새로운 기술을 도입할 때 반드시 기술의 내부 동작 방식을 충분히 이해하고 사용하자.