안녕하세요, 커널360 백엔드 2기 크루 김민주입니다. 🎀 오늘 제가 여러분과 함께 나누고자 하는 주제는 최근 E2E 프로젝트에서 많이 언급된 Cache와 Redis에 대한 것입니다. 특히, 스프링부트에 어떻게 적용할 수 있는지까지도 자세히 설명드릴 예정이에요.

Cache의 개념과 필요성

여러분, 거대한 도서관을 한번 상상해보세요. 이 도서관에는 수십만 권의 책이 보관되어 있는데, 이 중에는 수백 년 된 고서부터 최신 신간까지 다양하게 포함되어 있습니다. 그런데 이렇게 많은 책들은 도대체 어떻게 보관되고 있을까요? 대부분의 사람들은 “그냥 책꽂이에 꽂혀 있겠지”라고 생각할 수 있지만, 사실 이 도서관은 굉장히 정교한 시스템을 갖추고 있어요.

사람들이 자주 찾는 책이나 신간들은 도서관 입구 근처, 즉 쉽게 눈에 띄는 곳에 배치됩니다. 그 책들의 표지가 잘 보이도록 꺼내 놓거나 눕혀놓기도 하죠. 반면, 사람들이 잘 찾지 않는 책이나 오래된 책들은 별도의 서고에 보관됩니다. 이 서고는 많은 책을 효율적으로 저장할 수 있는 구조로 되어 있으며, 실제로 매우 큰 도서관에는 일반 사람들이 접근할 수 없는 서고가 수십 개씩 존재하기도 해요.

이와 마찬가지로, 컴퓨터 시스템에서도 데이터 저장 공간은 계층 구조로 나누어져 있어요. 가장 빠른 접근 속도를 가진 공간에서부터 비교적 느린 공간까지 다양한 종류의 저장소가 존재합니다.

컴퓨터의 저장 공간 구조

CPU 레지스터: 가장 빠른 저장 공간으로, 현재 실행 중인 명령어나 데이터를 저장합니다. 이 공간은 매우 작은 용량이지만, 데이터를 가져오는 속도는 매우 빠릅니다.

캐시 메모리: 프로세서와 메인 메모리 사이에 위치하며, 자주 사용하는 데이터를 저장해두는 역할을 합니다. 캐시 메모리는 메인 메모리보다 작고, 비용이 비싸지만, 데이터 접근 속도를 크게 향상시킵니다.

메인 메모리: 캐시 메모리보다 느리지만, 상대적으로 큰 용량을 갖고 있으며, 디스크나 원격 서버에 있는 데이터를 저장하고 필요에 따라 CPU에 전달합니다.

Cache란 무엇인가?

이제 캐시(Cache)에 대해 조금 더 깊이 알아보겠습니다. 캐시는 컴퓨터의 저장 공간 중 하나로, 자주 사용하는 데이터나 값을 미리 복사해 놓는 임시 저장 공간입니다. 캐시는 저장 공간이 작고 비용이 비싼 대신, 매우 빠른 성능을 제공합니다. 그래서 시스템의 속도 향상을 위해 사용됩니다.

여러분이 어떤 웹사이트를 자주 방문한다고 가정해봅시다. 매번 동일한 데이터를 서버에서 불러온다면, 불필요한 시간이 소모될 수 있습니다. 하지만 캐시가 이 데이터를 미리 저장해 두면, 다음 번에 같은 웹사이트를 방문할 때 훨씬 빠르게 로드할 수 있습니다. 이처럼 캐시는 반복적인 데이터 요청을 효율적으로 처리하는 데 중요한 역할을 합니다.

Cache의 효율성: 파레토 법칙

캐시가 효율적으로 동작할 수 있는 이유는 바로 파레토 법칙에 근거하고 있습니다. 파레토 법칙에 따르면,

80퍼센트의 결과는 20퍼센트의 원인으로 인해 발생합니다.

이 법칙을 서비스에 적용해보면, 전체 시스템의 80% 활동은 20%의 유저에 의해 발생하게 됩니다. 그렇기 때문에 전체 데이터의 20%만 캐시해도 대부분의 사용자 요청을 커버할 수 있습니다. 이로 인해 캐시를 적절히 활용하면, 시스템 성능을 크게 향상시킬 수 있는 것이죠.

예를 들어, 여러분이 자주 사용하는 20%의 기능에 캐시를 적용하면, 시스템의 리소스 사용량을 대폭 줄일 수 있어요. 이렇게 하면 서비스 응답 속도가 빨라질 뿐만 아니라, 서버 부하를 줄일 수 있어서 더 안정적인 서비스를 제공할 수 있습니다.

캐시의 작동 원리와 데이터 저장

그러면 캐시에는 어떤 정보를 담아야 할까요? 캐시의 저장 공간은 매우 작기 때문에, 모든 데이터를 저장할 수는 없습니다. 따라서 소수의 데이터를 선별하여 저장하는 것이 중요해요. 이때 고려해야 할 것이 지역성(Locality)입니다.

지역성(Locality)의 세 가지 유형

시간적 지역성(Temporal Locality): 한 번 접근된 데이터는 가까운 미래에 다시 접근될 가능성이 높아요. 예를 들어, 동일한 데이터에 여러 차례 쓰기가 수행될 때 캐시를 사용하는 것이 매우 효율적입니다.

공간적 지역성(Spatial Locality): 특정 데이터와 가까운 주소가 순서대로 접근될 때 발생합니다. 예를 들어, CPU가 특정 메모리 주소를 접근할 때, 해당 주소와 가까운 다른 주소들도 함께 가져오면 캐시를 효율적으로 사용할 수 있습니다.

순차적 지역성(Sequential Locality): 데이터가 순차적으로 접근되는 경향이 있습니다. 프로그램 내의 명령어가 순차적으로 구성되어 있을 때 캐시는 매우 효율적으로 작동합니다.

캐시 교체 알고리즘(Cache Replacement Algorithm)

캐시에 어떤 데이터를 담을지 결정한 후에는, 캐시에 모든 데이터를 담을 수 없기 때문에 기존 데이터를 어떤 방식으로 교체할지에 대한 전략이 필요합니다. 이를 위해 여러 가지 캐시 교체 알고리즘이 사용됩니다.

FIFO(First in First Out)

가장 먼저 들어간 캐시를 교체하는 방식입니다. 캐시 내부에서 가장 오래 머물렀던 데이터를 교체하게 됩니다.

LFU(Least Frequently Used)

사용 횟수가 가장 적은 캐시를 교체합니다. 캐시 내부에서 가장 적게 참조된 데이터를 교체하는 방식입니다.

LRU(Least Recently Used)

가장 오랫동안 사용되지 않은 캐시를 교체하는 방식입니다. 참조되지 않은 채로 가장 오래 머물렀던 데이터를 교체하며, 결과적으로 마지막 참조 시점이 가장 오래되었던 데이터를 교체하게 됩니다.

Random

특정 기준 없이 임의로 데이터를 선택해 교체하는 방식입니다. 간단하지만, 성능이 다소 떨어질 수 있습니다.

캐시의 사용 구조와 전략

캐시가 사용되는 일반적인 방식은 다음과 같습니다:

클라이언트로부터 데이터를 요청받습니다. 캐시에서 데이터를 찾습니다. 만약 캐시에 데이터가 있다면, 캐시에서 데이터를 가져옵니다. 캐시에 데이터가 없다면, 데이터베이스나 서버에서 데이터를 가져와 캐시에 저장합니다. 이후 동일한 요청이 들어오면, 캐시에서 데이터를 제공합니다. 이렇게 캐시를 사용하면 서비스 응답 시간을 크게 단축시킬 수 있어요.

캐싱 전략(Caching Strategies)

캐시를 사용할 때는 다양한 캐싱 전략이 사용됩니다. 몇 가지 주요 전략을 소개할게요:

Look-Aside

데이터 요청 시, 먼저 캐시에서 데이터를 찾고, 캐시에 없다면 DB에서 데이터를 가져옵니다. 캐시에 데이터가 없을 경우 캐시 미스(Cache Miss)가 발생하며, 이때 DB에서 데이터를 가져와 캐시에 저장합니다. 이 전략은 반복적인 읽기가 많은 상황에 적합합니다.

Read Through

데이터 요청 시 캐시에서 데이터를 읽어오며, 만약 캐시 미스가 발생하면 DB에서 데이터를 검색해 캐시를 업데이트하고 다시 캐시에서 데이터를 읽어옵니다. 캐시와 DB의 일관성을 유지할 수 있습니다.

Write Through

데이터를 저장할 때, 데이터베이스와 캐시에 동시에 데이터를 저장합니다. 데이터 저장 시, 먼저 캐시에 저장하고 바로 DB에 저장하는 방식입니다. DB와 캐시가 항상 동기화되어 있어 캐시 데이터가 최신 상태로 유지됩니다. 그러나 저장할 때마다 두 번의 Write가 필요해 성능이 다소 느릴 수 있습니다.

Write Back(Write Behind)

데이터를 먼저 캐시에 저장하고, 일정 주기나 크기에 도달하면 DB에 저장합니다. 이 방식은 쓰기 쿼리 회수와 부하를 줄일 수 있지만, 캐시 장애 시 데이터 유실 가능성이 있습니다.

Write Around

모든 데이터를 DB에 저장하고, 읽어온 데이터만 캐시에 저장하는 방식입니다. 속도가 빠르지만, 캐시 미스가 발생하기 전 데이터베이스 내 데이터가 수정될 경우 데이터 불일치가 발생할 수 있습니다.

Cache 사용 시 주의사항

캐시를 사용할 때는 몇 가지 주의할 점이 있습니다. 캐시에 저장할 데이터는 자주 접근되며, 잘 바뀌지 않는 데이터가 적합합니다. 예를 들어, 자주 변경되는 데이터에 캐시를 적용하면 데이터 정합성 문제가 발생할 수 있습니다.

데이터 정합성이란?

같은 데이터임에도 불구하고 캐시와 DB 내 데이터의 정보가 다른 상황을 말합니다. 예를 들어, 계좌 조회 시 처음 조회할 때는 10000원이었는데, 송금 후 계좌 DB에 9000원이 저장되었다면, 다시 조회할 때 캐시를 반환하여 여전히 10000원을 보여줄 수 있습니다. 이는 캐시와 DB 간 데이터 불일치로 인한 문제입니다.

Redis란 무엇인가?

이제 캐시와 연관된 Redis에 대해 알아보겠습니다. Redis는 Remote Dictionary Server의 약자로, 외부에 사전 형태로 데이터를 저장하는 서버입니다. 간단히 말해, Redis는 데이터를 메모리에 저장하고, 같은 페이지의 요청이 있을 때 메모리에 저장된 데이터를 빠르게 제공하는 시스템입니다.

메모리는 하드디스크나 SSD 같은 저장장치보다 훨씬 빠르기 때문에, Redis를 통해 데이터를 고속으로 제공할 수 있습니다. 이는 웹 서버의 부담을 획기적으로 줄여주며, 사용자에게 훨씬 더 빠르게 데이터를 제공할 수 있습니다.

Redis의 구조와 특징

Redis는 데이터를 키-값(Key-Value) 형태로 저장하며, 이 키는 문자열 형태로 고정되어 있지만, 값은 다양한 형태로 저장할 수 있습니다. Redis는 List, Set, Sorted Set, Hash 같은 다양한 자료 구조를 지원하며, Single Thread 방식으로 동작해 데이터의 원자성을 보장합니다.

Redis의 구조

Redis는 다양한 구조로 구축할 수 있습니다:

StandAlone:

Redis 서버 하나로 서비스를 운영하는 방식입니다. 간단하지만, 장애가 발생하면 서비스가 중단될 수 있습니다.

Master/Replica:

하나의 Master 서버와 여러 개의 Replica 서버로 구성됩니다. Master 서버에 장애가 발생하면, Replica 서버가 Master 서버로 승격됩니다.

Cluster:

여러 개의 Master 서버가 데이터를 샤딩하여 분산 저장하고 처리하는 방식입니다. 이 구조는 대량의 데이터를 효율적으로 처리할 수 있습니다.

Redis와 캐시의 관계

Redis는 자주 캐시로 사용되며, 데이터나 계산 결과를 미리 저장해 빠르게 액세스할 수 있습니다. Redis 외에도 Memcached, Local Memory Cache 등이 캐시로 사용될 수 있습니다. 또한, Redis는 캐시 외에도 임시 작업 큐, 실시간 채팅, 메시지 브로커 등 다양한 용도로 사용될 수 있습니다.

Spring Boot에서 Redis로 캐싱하기

이제 Spring Boot에서 Redis를 사용해 캐싱을 적용하는 방법을 알아보겠습니다. build.gradle 파일에 Redis 스타터 라이브러리를 추가한 후, application.properties 파일에서 Redis의 설정을 추가합니다. Redis의 기본 포트는 6379번이며, 이 설정을 통해 Spring Boot 애플리케이션이 Redis 서버와 연결됩니다.

Config 클래스에서 Redis를 사용하기 위한 Bean을 등록해주고, Entity를 만들며, Repository를 생성합니다. 인기 게시글 데이터를 캐시에 적용하기 위해 해당 메서드에 @Cacheable 어노테이션을 추가합니다. 캐시의 유효기간을 설정해두면, 일정 시간 이후 캐시된 데이터가 자동으로 갱신됩니다.

캐시 성능 개선 확인

캐시를 적용한 후 성능 테스트를 진행해, 성능이 얼마나 개선되었는지 확인할 수 있습니다. 캐시를 적용함으로써 웹 애플리케이션의 응답 속도가 크게 향상되고, 서버 부하를 줄여 시스템 안정성을 높일 수 있습니다.

마지막으로 Redis를 이용해 캐싱을 적용한 후 성능이 어떻게 개선되었는지 살펴보았습니다. 이처럼 캐시와 Redis를 적절히 활용하면 시스템 성능을 크게 향상시킬 수 있습니다. 발표를 마치며, 질문이 있으시면 언제든지 물어봐 주세요! 감사합니다! 🎀