카테고리 없음

0.5GB 메모리 환경, 로컬 캐시로 성능 최적화

땍땍 2026. 2. 8. 15:32

이번 사이드 프로젝트에서는 한정된 리소스 환경에서 서비스를 운영하며 발생한 성능 저하 문제와, 이를 해결하기 위해 로컬 캐시(Local Cache)를 도입한 과정을 정리한다.


1. 배경 및 문제점

현재 프로젝트는 0.5GB 메모리를 가진 서버 2대(Spring Boot 1대, DB 1대)만을 활용해 운영해야 하는 상황이었다.

이러한 제약은 Spring Boot와 DB 모두에 상당한 부담을 주었고, 특정 데이터를 조회하고 가공하는 과정에서 초 단위의 지연 시간이 발생하는 것을 확인할 수 있었다.

원인을 분석한 결과, 이는 쿼리 자체의 비효율성보다는 순수 DB에 접근하는 I/O 과정에서의 오버헤드가 가장 큰 문제인 것으로 확인되었다. 때문에 DB 접근 자체를 최소화하거나, 아예 DB를 사용하지 않고 최적화할 수 있는 방안을 고민하게 되었다.


2. 해결 방안 모색

[ 아키텍처 변경 고려: FastAPI + SQLite ]

가장 먼저 떠오른 방법은 프레임워크와 DB를 경량화하는 것이었다. Spring Boot 대신 FastAPI와 같은 가벼운 프레임워크를 사용하고, SQLite 같은 로컬 파일 DB를 활용하는 방안이었다.

하지만 다음과 같은 현실적인 제약으로 인해 해당 방법은 채택하지 않기로 했다.

  • 개발 공수 및 러닝 커브 이미 Spring Boot를 기반으로 많은 기능이 구현된 상태였기에, 새로운 언어와 프레임워크로 마이그레이션 하는 비용이 너무 컸다. 또 Spring Boot 에 대한 지식 및 생산성이 다른 언어에 비해 압도적으로 높은 상황이었다.
  • 배포 환경의 제약 현재 서버 환경은 인스턴스에 영구적인 볼륨이 보장되는 형태가 아니라, 특정 Image만을 띄우는 컨테이너 환경에 가까웠다. 따라서 데이터의 영속성을 보장해야 하는 로컬 DB(SQLite)를 활용하기에는 부적절한 상황이었다.

결국 기존 아키텍처를 유지하면서 DB 접근(I/O)을 최소화하기 위해 로컬 캐시(Local Cache)를 도입하기로 결정하였다.


3. 기술적 의사결정

로컬 캐시를 도입하며 가장 중요하게 고려한 기준은 다음과 같았다.

  1. 정합성 문제 : 데이터 불일치를 최소화해야 함.
  2. 경량성 : 0.5GB 메모리 환경에 부담을 주지 않아야 함.
  3. 최소한의 부하 : 캐시 자체가 서버에 주는 오버헤드가 적어야 함.

 

3.1. 캐시 전략 수립 (TTL 활용)

캐시를 활용할 때 가장 중요시 해야하는 부분은 데이터의 정합성 문제라고 생각한다.

일반적으로는 데이터 변경 시마다 캐시를 갱신하거나 무효화하는 등의 방식을 활용하곤 하지만, 이번 프로젝트는 데이터 변경 빈도가 낮았기 때문에 매번 갱신 조건을 검사하는 복잡한 로직보다는 일정 시간마다 데이터를 갱신 하는 방식이 효율적이라 판단했다.

또한, 아직 실 배포 전인 프로젝트 특성상 24시간 내내 트래픽이 발생하지 않을 것으로 예상되었다.

미사용 시간대에도 데이터를 캐싱하고 있는 것은 리소스 낭비이므로, 조회 시점에만 캐싱하고 일정 시간이 지나면 만료(TTL)시키는 전략을 통해 메모리 점유를 최소화하기로 했다.

하지만 Spring Boot에 기본 내장된 캐시 기능(ConcurrentMapCache)을 사용할 경우, 이러한 TTL 기능을 직접 구현해야 한다는 제약이 있었다. 물론 구현 자체가 어렵진 않지만, 추후 기능 확장이나 디버깅 시 내부 저장소(Map)의 한계로 인해 불필요한 공수가 들어갈 수 있다는 문제가 존재하였고, 최종적으로 TTL 기능이 자체적으로 잘 구현되어 있는 로컬 캐시 라이브러리를 도입하기로 결정했다.

 

3.2. 라이브러리 선정 (Caffeine Cache)

라이브러리를 선정하기에 앞서, Spring Boot 에서 주로 활용되는 로컬 캐시 라이브러리인 Ehcache, Guava, Caffeine 3가지 찾아보고 비교 검토하였다.

[ Guava ]

다양한 정보와 기능을 제공하긴 하나, Guava를 기반으로 성능을 대폭 개선하여 나온 Caffeine 라이브러리가 존재하였기 때문에 현시점에서 굳이 Guava를 선택할 이유 없다 판단하였고 제외하였다.

[ Ehcache ]

Ehcache는 Heap 메모리뿐만 아니라 Off-Heap, Disk에 데이터를 저장하여 대용량 처리가 가능하고, 클러스터링 기능에도 강점이 있었다.

하지만 0.5GB 메모리의 소규모 서버에서 사용하기에는 라이브러리 자체가 무겁고, 서버 확장을 고려하지 않는 현재 프로젝트 구조상 Ehcache의 이러한 기능들은 오버 스펙이라 판단하여 제외하였다.

[ Caffeine ]

최종적으로는, 기존 필요로 했던 TTL 을 포함해 구현 자체가 쉽고 비교적 가벼운 Caffeine 를 최종적으로 선택하게 되었다.


3. 마무리

결과적으로, TTL 설정과 Cache-Aside 패턴을 활용하여 데이터 정합성을 어느 정도 보장하면서도 캐시 히트율을 높일 수 있었다. 덕분에 0.5GB라는 열악한 메모리 환경에서도 DB I/O를 최소화하고 응답 속도를 ms 로 단축하는 등 유의미한 성능 개선을 확인할 수 있었다.

하지만 로컬 캐시 역시 결국 메모리를 점유하는 기술인만큼, 트래픽이 증가하여 캐시 데이터가 커질 경우, 메모리가 고갈되어 서비스 전체의 장애를 유발할 위험이 있다.

그렇다고 메모리 안정성을 위해 캐시 용량을 타이트하게 제한하자니, 캐시 히트율이 낮아져 다시 DB 접근 횟수가 늘어나고 응답 속도 개선 효과가 미미해지는 딜레마가 존재했다.

이러한 문제를 해결하기 위해서는 단순히 캐시를 도입하는 것에서 끝내지 않고 어떤 데이터를 캐시해야 히트율을 극대화하면서 메모리를 효율적으로 사용할 것인가 접근이 필수적이었다.

이러한 대비는 현시점에서도 대략적으로 예상을 하고 조치할 수 있겠지만, 실제 배포 전까지는 실제 트래픽 데이터에 기반한 통계를 통해 의사결정을 내리는 것이 가장 확실한 방법이라 판단했다.

때문에 현재는 모든 요청을 로깅하여 데이터를 축적하고 있다. 추후 이러한 로그를 분석하여, 실제 트래픽 패턴에 맞는 캐시 전략을 도출하고 개선해 나가는 과정도 별도의 포스팅으로 정리해보려 한다.