4. 캐싱
주니어를 위한 소소한 백엔드 개발 이야기의 4번째 주제는 캐싱입니다. 누군가 저에게, 백엔드 개발에서 가장 중요한 기술이 무엇이냐고 질문한다면, 저는 고민하지 않고 바로 "캐싱"이라고 대답합니다. 캐싱으로 대답한 이유는 바로 캐싱은 소프트웨어의 성능, 품질에 매우 중요한 백엔드 기술이면서, 장애가 가장 많이 발생하는 원인이 바로 캐싱이기 때문입니다.
허접한 필자의 글은 가볍게 재미로 읽으시고, 아래 링크의 글을 정독하는걸 추천드립니다.
https://docs.microsoft.com/ko-KR/azure/architecture/best-practices/caching
추가로, 이 글에서 나오는 그림은 제가 직접 그린 그림입니다. 가끔 구글링을 하다보면 제 블로그의 그림을 무단으로 가져가셔서 사용하시는 분이 너무 많습니다. 급하게 그린 그림이라서 부끄럽습니다.ㅠㅠ 제 그림 무단으로 사용하지 않으셨으면 좋겠습니다. 제 글은 가벼운 마음으로 읽어주세요~
"주니어를 위한 소소한 백엔드 개발 이야기 97" 이라는 주제로 잡다하고 가벼운 글을 작성해서 공유합니다. 매주 주말 1편씩 글을 쓰겠다는 당찬 포부로 시작하였지만, 생업으로 인해서 많이 늦어졌습니다. 97개를 채울수 있을지도 의문입니다.
이 글은 시니어를 위한 글은 아닙니다. 취준생 또는 주니어 개발자들에게 조금이라도 도움이 되었으면 좋겠습니다. 다음 주제에 대해서 추천 받습니다. 주니어 개발자분들은 어떤 이야기를 나누고 싶으신지요?
001. Java equals(), hashCode()
다음 주제는 미정, 언제 쓸지도 미정
- 쓰레드풀 (병렬 프로그래밍)
- 리프레시 토큰이 왜 필요한가?
- 뭘 테스트해야 할지 모르겠어요
- 함수형 프로그래밍 vs 절차적 프로그래밍
- 비동기 논블록킹
.......
등등 주제 고민 중 (추천 받음)
캐싱에 대해서 간략하게 알아보자.
캐시 : 캐시는 데이터를 미리 복사해놓은 스토리지 계층이다.
캐싱 : 캐시를 사용하는 것(행위, 행동)을 캐싱이라 한다. 데이터 조회 시 원본 저장소에서 조회하는 것보다 캐시에서 조회하는 것이 훨씬 더 빠르게 찾을 수 있다. 이런 작업을 캐싱이라 한다.
캐싱은 매우 다양한 분야에서 사용된다. 필자가 지금 당장 생각나는 것만 설명해보겠다.
가장 먼저 생각나는 건 DNS 캐시일 것이다. 인터넷은 DNS(Domain Name System) 을 사용하여 퍼블릭 웹사이트 및 IP 주소를 관리한다. DNS에 의해서 사용자는 웹사이트 의 IP 주소를 기억할 필요가 없다. 이때, DNS 서버는 DNS 조회 수를 줄이기 위해서 DNS 캐시를 지원한다.
CPU 캐시 메모리는, CPU 가 메모리에 저장된 데이터를 읽어오면서, 자주 사용하는 데이터는 캐시 메모리에 저장한다. 그리고, 다시 사용할때 캐시 메모리에서 데이터를 가져온다.
브라우저(크롬, 파이어폭스 등) 을 사용해서 웹사이트를 방문시 리소스를 브라우저 캐시에 저장된다.
캐시에 의해서 웹사이트가 빠르게 로드 되었다. 다들 알고 있겠지만, 개발자는 임의로 캐시를 사용하지 않도록 설정할 수 있다. 아래와 같이 Disable cache 체크박스를 설정하자. 웹사이트를 조회해보면 모든 리소스를 캐시가 아닌, 서버에서 새로 조회한다는 것을 알수 있다.
CDN 도 캐싱 중 대표적인 사례이다. AWS CloudFront 같은 서비스를 주로 사용한다.
웹서버에서도 캐싱을 많이 사용했었다. (최근에는 잘 모르겠지만) 필자가 아주 오래전에 사용했던 방법이다. 최근에는 잘 사용하지 않는다. 당시, 웹서버는 WAS 앞단에 구성했었다. 웹서버의 대표적인 기술은 Nginx 이다. Nginx 는 리버스 프록시 역할을 한다. 이때 Nginx 리버스 프록시 서버에서 정적 리소스를 WAS 대신에 응답해줄 수 있다. 대표적인 리소스로는 자바스크립트, CSS, 이미지 파일 같은 리소스이다. 웹사이트에 접속하는 수많은 사용자가 필수로 호출해야 하는 리소스이지만, 모든 사용자에게 같은 리소스이면서 자주 변경되지 않으므로 해당 데이터는 웹서버에서 캐시 데이터를 대신 응답해주는 방법이다. 하지만, 위에서 설명했듯이 최근에는 CDN 기술을 주로 사용하며, 대표적으로 AWS Cloud Front 같은 기술을 사용하게 된다.
암튼, 자세한 내용은 생략하겠다. 이외에도 다양한 분야에서 캐싱은 사용된다. 이 글에서는 백엔드 개발자가 주로 경험하게 되는, 백엔드 서버에서의 캐싱에 대해서 이야기 해보도록 하자.
캐싱은 소프트웨어의 성능을 위해서 사용한다. 백엔드 API 에서 데이터를 빠르게 응답하기 위해서 캐싱을 사용해야 한다. 자세한 내용은 아래 문서를 참고하길 바란다.
https://aws.amazon.com/ko/caching/
(필자는) 백엔드 개발에서는 보통 크게 두가지 캐싱 전략을 사용한다.
- 애플리케이션 메모리 캐싱
- 별도의 서버에 캐싱
쉽게 생각해보면 아래와 같다.
상품 검색 키워드에 대한 결과를 응답해주는 API 백엔드 서버가 있다고 가정하자. 예를 들어서, "운동화" 라는 검색 키워드에 "나이키", "뉴발란스", "프로스펙스" 등 운동화 브랜드의 고객 선호도 순서를 응답해주는 API 가 있다고 가정해보자. 이때, 해당 데이터를 애플리케이션에서 캐싱을 해보자. 필자의 허접한 그림을 보자..
위와 같이 애플리케이션 메모리 캐싱은 매우 심플하게 구성할 수 있다. 하지만, 애플리케이션 캐싱 전략은 몇가지 단점이 존재한다.
1)힙메모리 부족
JVM 환경의 애플리케이션에 메모리 캐시를 사용하면 힙메모리 부족이 발생하는 경우를 매우 많이 경험하였다. 흔치 OOM(Out Of Memory) 라고 부른다. Java 에서의 기본 자료구조인 Set, Map 등은 캐시 솔루션(예:레디스) 와 비교해서 매우 큰 비용이 든다. OOM 이 발생하게 되면, Heap 덤프를 떠서 어떤 객체에서 메모리를 많이 사용하는지, 메모리 누수가 있는지 확인할 수 있다. 이때, Set, Map 등 자료구조에서 OOM 이 발생하고 있을 가능성도 크다. 어쨋든, 애플리케이션 메모리를 사용하게 되면 공용 캐시 에 비해 더 빠르게 응답할 수 있다는 장점이 있지만, 힙메모리 부족이라는 치명적인 오류가 발생할 가능성도 염두해 두도록 하자. 물론, 이런 경우 대표적인 해결책은 더 큰 메모리를 할당해주면 바로 해결되긴 한다. 전형적인 스케일업(스케일 아웃이 아닌) 환경이다.
2)비활성 메모리
애플리케이션이 재시작할때 메모리 데이터는 모두 사라진다.
3)분산 환경에서의 데이터 불일치
API 서버를 1대로 운영한다면 이슈가 되지 않는다. 하지만, 일반적으로 우리는 애플리케이션 서버를 다수의 서버로 운영한다. 이때, 아래 그림과 같이 두개의 인스턴스에서 서로 다른 캐시 데이터를 사용자에게 제공하게 되는 상황이 발생할 수 있다.
위와 같은 상황을 필자의 허접한 그림으로 다시 이해해보면 아래와 같다.
해당 이슈를 잘 해결하기 위해서는 데이터 동기화 처리를 잘 해줘야 한다. API 3대의 서버에 Pub/Sub 기반의 이벤트 기반 아키텍처로, 캐시 업데이터 요청을 전송하는 방법으로 해결할 수 있다.
필자도 위와 같은 애플리케이션 메모리 캐싱 방법을 많이 운영해봤는데, 가장 큰 장점은 API 서버에서 매우 빠른 성능으로 응답할 수 있었다. 하지만, 데이터 양이 많아지고 API 서버에서 너무 많은 캐시 데이터를 들고 있는 상황이 발생하면서 장애가 발생하기 시작하였다. 힙메모리가 부족해서 애플리케이션이 아웃오브 메모리 익셉션이 발생하거나, 애플리케이션 재실행 시 캐시 데이터를 생성하기 위해서 API 서버가 너무 느리게 되는 상황 등이다. 이 경우를 해결하기 위해서는 두번 째 전략인 공유 캐싱을 사용해야 한다.
두번째 전략은, 별도의 캐시 장비에 저장하고, 애플리케이션에서는 캐시 서버에서 데이터를 조회하면 된다.
필자의 그림으로는 아래와 같다.
몇가지 생각해볼 주제에 대해서 간략하게 소개하겠다.
크게 두가지 전략이 있다.
- 미리 모든 데이터를 캐시에 저장해둔다.
- 요청이 있을때마다 저장해서 사용한 후, 너무 오래동안 사용하지 않는 캐시는 지운다.
미리 모든 데이터를 저장하는 전략은 아래 그림과 같다.
1. 주기적으로 실행하는 배치 서버에서 원본 데이터베이스를 조회한다.
2. 원본 데이터를 캐시 데이터로 변환해서 저장한다.
3. API 서버는 캐시 서버를 조회해서 사용한다.
이 경우에는, 자주 사용하지 않는 데이터도 모두 저장해야 한다. 즉, 히트율이 낮은 데이터도 캐시로 저장해야 한다는 얘기다. 이런 단점에도 API 서버에서는 원본 DB 를 호출할 필요가 없어서 매우 빠르게 응답할 수 있다.
다른 전략으로는 요청이 있을때마다 저장하는 방법이다.
처음 사용자가 요청했을 때, 캐시 스토리지에는 아무 데이터도 없는 상황이다.
1 .애플리케이션은 먼저 캐시 저장소에 데이터가 있는지 조회한다. 하지만 데이터가 없다.
2. 애플리케이션은 원본DB에서 데이터를 조회하고 사용자에게 제공한다.
3. 애플리케이션은 원본DB에서 가져왔던 데이터를 캐시 저장소에 저장한다.
다음 사용자가 요청했을 때는 이미 캐시 저장소에 데이터가 있는 상황이다.
1. 애플리케이션은 먼저 캐시 저장소에 데이터가 있는지 조회한다.
이때, 캐시 저장소에 저장되어있는 데이터를 그대로 사용하면 된다. 원본 DB 를 조회할 필요가 없다.
별도의 캐시 스토리지 인프라 구축하는 경우 관리포인트가 증가한다. 대부분 회사에서는 AWS ElasticCache 를 사용하거나 레디스를 자체 구축해서 사용할 것이다.
캐시에 저장해야 하는 데이터인지, 신중히 선택한다. 모든 데이터를 캐싱할 필요가 없다. 이때 확인해볼 수 있는 지표는 바로 캐시 히트율이다. 자주 읽는 데이터를 캐싱하는 것이 좋다. 단, 단순히 히트율이 높으면 좋은건 아니고, 자주 읽으면서 자주 변경되지 않는 경우에 효과적이다. 간혹 쓰기 카운트가 너무 많은 경우에는 캐시 저장소에 부담이 클 수 있다. 간혹, 중요한 데이터를 캐싱하지 않는다는 개발자가 있다. 오해가 좀 있는것 같다. 중요한 데이터인 경우에도 캐싱을 사용하면 좋다. 단, 반드시 백업이 되어있어야할 것이다. 원본 데이터가 따로 있고, 읽기를 제공하기 위해서 캐싱을 하는 것이다.
캐싱을 하기로 결정하였다면, TTL 에 대해서 고민해야 한다. TTL 은 개발자 스스로 결정할 수 없다. 반드시 도메인 전문가와 함께 논의해서 결정해야 한다.
매우 자주 발생하는 장애이다. 동일한 Key 에 서로 다른 자료구조를 저장하게 되면, 역직렬화 또는 직렬화 시 장애가 발생할 수 있다. 이런 경우에는 새로운 Key 를 설정해서 신규 데이터를 저장하는 방식으로 해결하는게 좋겠다.
필자가 오래전에 다녔던 회사에서 기술부채를 해결했던 사례를 공유한다. 애플리케이션 캐시 기반의 아키텍처를 별도의 공유 캐시로 전환한 사례이다. 개인적으로 필자가 그동안 10년 넘게 회사생활하면서 가장 힘들었고 기억에 남는 사례이다.
https://brunch.co.kr/@springboot/173
이번 글에서는 캐싱에 대해서 가볍게 이야기해보았다. 이 글에서 다루지 못한 내용에 대해서 각자 좋은 의견이 있다면 댓글로 남겨주길 바란다.
[1] aws : https://aws.amazon.com/ko/caching/
[2] Microsoft : https://docs.microsoft.com/ko-KR/azure/architecture/best-practices/caching
[3] 위키백과 : https://ko.wikipedia.org/wiki/%EC%BA%90%EC%8B%9C