본문 바로가기
WEB

캐싱적용해서 읽기 작업 성능 개선하기.

by jinukix 2021. 12. 21.

Cache란 자주 사용하는 데이터를 미리 복사해 놓는 임시공간으로서 다시 이 데이터를 필요로 할 때 보다 빠르게 참조할 수 있습니다. 현재 진행 중인 프로젝트에서 성능 개선을 위해 캐시를 도입하기로 했습니다. 대규모 트래픽을 처리해야 하는 상황에서 데이터베이스 서버에 동일한 요청을 여러 번 처리하게 하는 것은 비효율적이라고 생각했습니다. 

Local Cache vs Global Cache

캐시를 사용하기 전에 먼저 어떤 캐싱 전략을 사용할지 고민했습니다. 캐싱 전략에는 크게 Local Cache, Global Cache가 있습니다.

Local Cache는 서버 내부 저장소에서 데이터를 관리하는 것입니다. 서버 내에서 동작하기 때문에 속도가 빠르지만 각 서버 간의 데이터 공유가 안된다는 단점이 있습니다. 이외에도 서버의 자원을 사용하기 때문에 자원적인 이슈가 발생할 수 있습니다.

Global Cache는 서버 내부 저장소가 아닌 별도의 캐시 서버를 구성하는 방법입니다. 별도의 캐시 서버를 구성함으로써 각 서버에서는 캐시 서버를 참조하게 됩니다. 네트워크를 타야 해 Local Cache에 비해 속도는 느리지만, 각 서버 간의 데이터를 공유할 수 없다는 단점은 해결할 수 있습니다.  저희는 다수의 서버 환경을 구성하고 있습니다. 따라서 Global Cache 전략을 사용하기로 했습니다.

Redis Session, Cache 서버의 분리

저희는 이미 Session 데이터 관리를 위한 Redis 서버를 사용하고 있습니다. 트래픽이 증가함에 따라 하나의 서버로는 부담이 될 수 있다고 판단했으며, Cache와 Session은 엄연히 사용 목적이 다르기 때문에 객체지향에서 이야기하는 관심사의 분리를 적용시켜야 한다고 생각했습니다. 그리고 이후 확장을 고려해서라도 이를 분리해야 한다고 생각해 하나의 Redis 서버로 관리하는 것이 아닌 Session 서버와 Cache 서버를 별도로 관리하기로 했습니다.

@EnableCaching 어노테이션으로 캐싱 기능을 활성화합니다. 캐싱 기능이 활성화되면 CacheManager 타입의 빈이 호출되어 캐시 어노테이션이 붙은 컴포넌트를 스프링이 관리하게 됩니다.

다음은 Cache 사용을 위한 RedisConnectionFactory와 Session 사용을 위한 RedisConnectionFactory 분리하겠습니다. RedisConnectionFactory은 Redis 서버 연결을 위한 Connection을 관리하는 역할을 수행합니다.

RedisCacheManager를 등록하겠습니다. 스프링에서는 CacheManager라는 인터페이스를 통해 캐시 추상화를 지원하고 있습니다. RedisCacheManager는 CacheManager의 구현체입니다. RedisCacheManager를 통해 우리는 Redis 캐시 기능을 쉽게 사용할 수 있습니다.

현재 RedisConnectionFactory 타입의 Bean이 2개이기 때문에 RedisConnectionFactory타입의 빈을 주입하려 할 때 충돌이 발생할 수 있니다. 따라서 @Qualifer를 통해 Bean 이름을 명시해줘야 합니다.


이제 캐싱을 적용해야 하는데 어떤 데이터에 캐싱을 적용하면 좋을까요?? 먼저 캐시 사용 구조에 대해 알아봅시다.

1. Server는 클라이언트로부터 요청을 받습니다.
2. Cache에 데이터가 있는지 확인합니다. 만약 존재한다면 해당 데이터를 클라이언트에게 반환합니다.
3. Cache에 데이터가 없다면 DBMS를 통해 데이터를 가지고 옵니다.
4. Cache에 데이터를 등록하고, 클라이언트에게 반환합니다.

만약 캐시 서버에 데이터를 캐싱했지만 데이터베이스 서버에서 업데이트가 발생하면 데이터 불일치가 발생할 수 있습니다. 따라서 캐시를 적용할 데이터는 업데이트가 자주 발생하지 않는 데이터, 즉 반복적이고 동일한 결과 값을 반환하는 기능에 적용하는 것이 캐시를 사용하기 적합한 조건이라고 볼 수 있습니다.

캐시 어노테이션

스프링의 캐시 추상화에서 제공하는 어노테이션을 통해 캐시 사용을 쉽게 할 수 있습니다. 

@Cacheable

@Cacheable은 해당 메서드를 실행하기 전 캐시 저장소를 확인합니다. 캐싱 데이터가 있다면 메서드를 실행하지 않고 캐시 데이터를 반환하고, 캐싱 데이터가 없다면 메서드를 실행하고 결괏값을 캐시저장소에 저장합니다.

@CacheEvict

@CacheEvict는 해당 키에 해당하는 캐시 데이터를 삭제합니다. 캐싱 대상 데이터의 쓰기 작업이 일어나는 경우 적용하는 것이 좋습니다.


스터디카페 목록 조회에 캐시를 적용하고 테스트를 해보겠습니다. 성능 차이를 확실히 비교할 수 있게 총 1만 개의 데이터를 넣고 테스트를 진행했습니다. 

첫 번째 요청입니다. 아직 캐시 저장소에 캐싱되지 않은 상태라 응답 시간이 434ms 가 걸린 것을 확인할 수 있습니다. 요청이 끝나면 캐시가 저장된 것을 확인할 수 있습니다.

두 번째 요청입니다. 응답 시간이 48ms로 빨라진 것을 확인할 수 있습니다.

스터디카페를 등록 (캐시를 삭제) 해보겠습니다. @CacheEvict 어노테이션이 적용되어 캐시 데이터가 삭제된 것을 확인할 수 있습니다.

댓글