개요
현재 진행중인 Goods-For-You 프로젝트에서, Redis를 도입해 서버가 여러 대로 구성된 분산 시스템 환경에서 사용자의 로그인 정보를 저장하는 세션 저장소로 사용하고 있습니다. 하지만 Redis는 세션 저장소로 사용할 수도 있지만, 캐싱을 통해 성능 향상을 도모할 수 있는 장점도 있습니다. 따라서 현재 프로젝트에서는 어떤 부분에 캐싱을 적용하면 좋을지에 대해 고민해보고 그에 앞서 관련 개념들을 알아보도록 하겠습니다
캐시란?
캐시는 사용 빈도가 높은 데이터또는 값 비싼 연산 결과를 빠른 속도로 접근할 수 있는 위치에 두는 것을 의미합니다. CPU의 1차 캐시나, 2차 캐시, 저장소 캐시, OS 페이지 캐시, 데이터베이스 버퍼 캐시, KVS(데이터를 메모리에 캐시하는 것)등 광범위하게 캐시 기술이 존재합니다.
캐시에는 다음과 같은 특징이 있습니다.
- 일부 데이터를 데이터 출력 위치와 가까운 지점에 일시적으로 저장한다.
- 데이터 재사용을 전제로 한다.

현재 굿즈포유 서버에서는 단일 데이터베이스 서버를 두고 있습니다. Memory를 사용하는 데이터베이스인 Redis를 사용하고 있지만 Session Server로의 역할만으로 사용하고 있씁니다. 이렇게 된다면 사용자가 굿즈 포유 서비스를 새로 호출 할 때 마다 사용자에게 보여줄 데이터를 가져오기 위해 한 번 이상의 데이터베이스 호출이 발생하게 됩니다. 애플리케이션의 성능은 데이터베이스(Disk 기반의 데이터베이스)를 얼마나 자주 호출하느냐에 따라 크게 좌우되는데, 캐시를 사용한다면 그런 문제를 완화할 수 있을 것 입니다.
그렇다면 디스크 기반의 데이터베이스는 왜 느린것 일까요?
디스크 기반의 데이터베이스
디스크 기반의 데이터베이스는 데이터를 Disk에 저장하여 관리합니다, 여기서 Disk는 하드 디스크를 기준으로 설명하겠습니다. 하드 디스크는 CD나 LP판과 비슷하게 작동합니다.

위 그림에서 동그란 원판이 실질적으로 데이터가 저장되는 곳 입니다. 이를 플래터라고 합니다.
그리고 이 플래터를 회전시키는 구성 요소를 스핀들 이라고 합니다(플래터 가운데에 있는 바퀴모양의 장치를 말합니다).
플래터를 대상으로 데이터를 읽고 쓰는 구성 요소는 헤드 입니다.(헤드는 플래터위에 보이는 바늘처럼 보이는 부품입니다)
헤드는 디스크 암을 통해 원하는 위치로 이동을 할수 있습니다.

추가로 데이터가 Disk에 저장되는 과정을 알아보겠습니다. 플래터는 트랙과 섹터라는 단위로 데이터를 저장합니다. 그림처럼 플래터를 여러 동심원으로 나누었을 때 그 중 하나의 원을 트랙이라고 부릅니다. 하나의 트랙은 여러개의 섹터로 나누어지는데 이 한 조각을 섹터라고 부릅니다.

또한 하드 디스크는 많은 양의 데이터를 저장해야 하므로 여러 겹의 플래터로 이루어 질수 있습니다. 이때 여러 겹의 플래터 상에서 같은 트랙이 위치한 곳을 모아 연결한 논리적 단위를 실린더라고 부릅니다.
한 플래터를 동심원으로 나눈 공간은 트랙, 같은 트랙끼리 연결한 원통 모양의 공간은 실린더 입니다.
연속된 정보는 보통 한 실린더에 기록이 됩니다. 예를 들어 두 개의 플래터를 사용하는 하드디스크에서 네 개 섹터에 걸쳐 데이터를 저장할 때는 첫 번째 플래터의 윗면, 뒷면과 두 번째 플래터 윗면 ,뒷면에 데이터를 저장합니다. 연속된 정보를 하나의 실린더에 기록하는 이유는 디스크 암을 움직이지 않고도 바로 데이터에 접근할 수 있기 때문입니다.
이어서 디스크에 저장된 데이터에 접근하는 과정을 알아보겠습니다. 하드 디스크에 저장된 데이터에 접근하는 시간은 크게 탐색 시간, 회전 지연, 전송 시간으로 나뉩니다.
- 탐색 시간 : 탐색 시간은 접근하려는 데이터가 저장된 트랙까지 헤드를 이동시키는 시간을 의미합니다.
- 회전 지연 : 헤드가 있는 곳으로 플래터를 회전시키는 시간을 의미합니다.
- 전송 시간 : 전송 시간은 하드 디스크와 컴퓨터 간에 데이터를 전송하는 시간을 의미합니다.
위 시간들은 별 것 아닌것 같아도 데이터를 쓰고 읽는 작업의 성능에 큰 영향을 끼치는 시간들 입니다.
L1 캐시 참조 시간 0.5ns
L2 캐시 참조 시간 | 5ns |
메모리 참조 시간 | 7ns |
메모리에서 1MB를 순차적으로 읽는 시간 | 250.000ns |
(하드)디스크 탐색 시간 | 10.000.000ns |
(하드)디스크에서 1MB를 순차적으로 읽는 시간 | 30.000.000ns |
한 패킷이 캘리포니아에서 네덜란드까지 왕복하는 시간 | 150.000.000ns |
위 표는 ‘프로그래머가 꼭 알아야 할 컴퓨터 시간들’의 일부를 옮겨 적은 내용 입니다.
이렇듯 디스크 기반의 데이터베이스를 사용하게 된다면 메모리 기반의 데이터베이스에 비해
엄청난 속도차이를 보인다는 점을 알 수 있습니다.
그렇다면 메모리 기반의 데이터베이스를 적용해 모든 데이터를 캐싱하는게 옳은 방법일까요?
그에 앞서 캐시 히트에 대해 알아보겠습니다.

캐시 히트 : 메모리는 자주 사용될 것으로 예측한 데이터가 실제로 들어맞아 캐시 메모리 내 데이터가 CPU에서 활용될 경우를 캐시 히트라고 합니다.
캐시 미스 : 자주 사용될 것으로 예측하여 캐시 메모리에 저장했지만, 예측이 틀려 메모리에서 필요한 데이터를 직접 Disk 등에서 가져와야 하는 경우를 캐시 미스 라고 합니다.
만약 메모리 기반 데이터베이스를 사용하고, 캐싱을 사용한다 하더라도, 캐시 미스가 자주 발생한다면 해당 데이터를 계속 디스크 기반 데이터베이스에서 가져와야 하기 때문에 캐시 데이터베이스의 이점을 활용할 수 없습니다. 그리고 성능 또한 저하되게 될 것 입니다.
따라서 이런 캐시 메모리를 이용하는 메모리 기반 데이터베이스의 이점을 제대로 활용하려면 CPU가 사용할 법한 데이터를 제대로 예측해서 캐시 적중률을 높여야 합니다. 이런 데이터를 알 수 있는 방법으로 **참조 지역성의 원리(principle of locality)**를 적용할수 있습니다.
참조 지역성의 원리는
- CPU는 최근에 접근했던 메모리 공간에 다시 접근하려는 경향이 있다.
- CPU는 접근한 메모리 공간 근처를 접근하려는 경향이 있다.
가 있습니다. 먼저 ‘최근에 접근했던 메모리 공간에 다시 접근하려는 경향’에 대해 알아보겠습니다.
프로그래밍 언어를 사용하다보면 변수를 사용하게 될 것 입니다. 변수에 값을 저장하고 나면 언제든 변수에 다시 접근하여 변수에 저장된 값을 사용할 수 있습니다. 이는 CPU가 변수에 저장된 메모리 공간을 언제든 다시 참조할 수 있다는 것을 의미합니다. 그리고 변수에 저장된 값은 한번만 사용되지 않고 프로그램이 실행된느 동안 여러번 사용 됩니다. 즉, CPU는 최근에 접근했던 (변수가 저장된) 메모리 공간을 여러 번 다시 접근할 수 있습니다.
public class Main{
public static void main(String args[]){
int num = 2;
for(int i=1; i<=9;i++){
System.out.println(num+"X"+i+"="+num*i);
}
}
}
/**
* 2 X 1 = 2
* 2 X 2 = 4
* .....
**/
위 예시 코드는 구구단 2단을 출력하는 코드 입니다. 코드에서 변수는 num과 i가 있습니다. 이 과정에서 변수들이 여러번 사용되고 있습니다. 이렇게 num과 i처럼 최근에 접근했던 메모리 공간(주소)에 다시 접근하려는 경향을 시간 지역성 이라고 합니다.
그렇다면 접근한 메모리 공간 근처를 접근하려는 경향은 무엇일까요?
CPU가 실행하려는 프로그램은 보통 관련된 데이터들끼리 모여 있습니다. 예를 들어 메모리 내에 워드 프로세서 프로그램, 웹 브라우저 프로그램, 게임 프로그램이 있다고 가정해 보겠습니다. 이 세 프로그램은 서로 관련 있는 데이터들 끼리 모여서 저장됩니다. 워드 프로세서 프로그램은 워드 프로세서 관련 데이터들이 모여 저장되고, 웹 브라우저 프로그램은 웹 브라우저 관련 데이터들이 모여 저장되고, 게임 프로그램은 게임 관련 데이터들이 모여 저장될 것 입니다.
그리고 하나의 프로그램 내에서도 관련 있는 데이터들은 모여서 저장됩니다. 워드 프로세서 프로그램을 기준으로 자동 저장 기능, 입력 기능, 출력 기능이 있다고 했을 때 각각의 기능과 관련한 데이터는 모여서 저장됩니다. CPU가 워드 프로세서 프로그램을 실행할 때에는 워드 프로세서 프로그램이 모여 있는 공간 근처를 집중적으로 접근할 것이고, 사용자가 입력을 할 때에는 입력 기능이 모여 있는 공간 근처를 집중적으로 접근할 것입니다. 이렇게 ‘접근한 메모리 공간 근처를 접근하려는 경향’을 공간 지역성 이라고 합니다.
그렇다면 Redis와 같은 캐시 데이터베이스를 두게 되면 어떻게 작동하게 될까요?

만약 데이터가 캐시에 있으면 캐시에서 데이터를 읽습니다.(캐시 히트) 그렇지 않다면 데이터베이스에서 해당 데이터를 읽어 캐시에 쓰게 됩니다(캐시 미스).
그렇다면 캐시를 사용할 때 유의할 점을 정리해보고 그에 따라 캐시를 적용할만할 포인트를 생각해보겠습니다.
캐시 사용시 유의할 점
- 캐시는 데이터 갱신은 자주 일어나지 않지만(CRUD의 CUD) 참조는 빈번하게 일어난다면 고려해볼만 하다.(CRUD의 R)
- 캐시는 데이터를 휘발성 메모리에 두기 때문에, 영구적으로 보관되어야 하는 중요한 데이터를 캐시에 두는것은 바람직하지 않다.
- 캐시에 보관된 데이터는 어떻게 만료되게 할 것인가? 만료된 데이터는 캐시에서 삭제되어야 한다. 만약 만료 정책이 없으면 데이터는 캐시에 계속 남게 되어 결국 메모리 용량이 부족해지는 상황이 오게 될 수도 있습니다. 그럼 만료 기한을 너무 짧게 잡으면 어떻게 될까요? 그렇다면 캐시에 데이터가 캐싱 되어 있지 않기 때문에 데이터베이스에서 데이터를 너무 자주 읽게 될 것입니다. 이로 인해 성능 저하가 발생하게 될 것 입니다. 반면에 만료 기한을 길게 잡으면 데이터베이스에 존재하는 원본 데이터와 차이가 날 가능성이 높아질 수도 있습니다. 따라서 캐시 만료에 대한 정책을 세우는 것도 중요합니다.
- 캐시 데이터 방출(eviction) 정책은 어떻게 할것인가? 캐시가 꽉 차버리면 추가로 캐시에 데이터를 넣어야 할 경우 기존 데이터를 내보내야 합니다. 이것을 캐시 데이터 방출 정책이라 하는데 이때 자주 사용되는 것은 LRU(Least Recently Use - 마지막으로 사용된 시점이 가장 오래된 데이터를 내보내는 정책)입니다. 다른 정책으로는 LFU(Least Frequently Used - 사용된 빈도가 가장 낮은 데이터를 내보내는 정책)나 FIFO(First In First Out - 가장 먼저 캐시에 들어온 데이터를 가장 먼저 내보내는 정책)같은 것도 있으며, 경우에 맞게 캐시 데이터 방출 정책을 적용해야 할 것 입니다.
그렇다면 현재 진행중인 굿즈 포유 어플리케이션에서는 언제 캐싱을 적용하는게 좋을까요?
- 제가 생각 했을 때 캐싱 하기에 가장 적합한 데이터는 상품 데이터라고 생각했습니다, 상품 데이터는 사용자가 한번 상품 등록을 하면 데이터의 갱신이 자주 일어나지는 않지만, 상품의 조회는 굿즈 포유 서비스에서 가장 많이 일어날 것이라고 생각이 들어 캐싱을 사용하기에 적합하다고 생각했습니다. 또한 이런 상품 데이터를 캐싱함으로써 데이터베이스에 직접 접근해 상품 데이터를 조회해오는 부하를 줄일 수 있고, 캐싱 솔루션을 사용해, 데이터를 조회해오는 응답 시간을 줄여 서비스 전반적인 성능을 개선할 수 있다고 생각했습니다.(추가로 상품 데이터 중에 현재 프로젝트의 주요 기능은 아니지만 장바구니 또는 찜하기 기능에 사용하는 것이 적절할것이라고 생각합니다. 앞서 설명한 상품 데이터 전체에 캐싱을 적용하게 되면 캐시에서 사용되는 메모리의 특성상 서버가 다운되었을 시에, 해당 데이터가 사라지기 때문에, 상품 데이터 중에서도, 장바구니 또는 찜하기 기능에 사용하는 것이 적절할 것이라고 생각합니다
'프로그래밍 > 프로젝트' 카테고리의 다른 글
테스트 커버리지를 70% 이상 유지하면서 느낀점 (0) | 2023.03.21 |
---|---|
도커 컴포즈 사용 시 DB 초기화 문제 해결 과정 (0) | 2023.03.18 |
CAP 이론을 바탕으로 NoSQL 을 적용 할 만한 포인트 고려 (0) | 2023.03.02 |
GoodsForYou 패키지 구조에 대한 고민(포트와 어댑터) (0) | 2023.02.25 |
인증 방식으로 세션 VS 토큰 어떤걸 선택해야 할까? (0) | 2023.01.17 |