웹 애플리케이션의 성능을 위해 고려해야할 부분은 정말 많습니다.
WS 관점에선 이중화, 로드밸런싱, 캐싱, 커넥션 등,
WAS 관점에선 스레드풀, 커넥션, 억셉트카운트, DB커넥션풀 등,
DB 관점에선 커넥션 수와 인덱스 등이 있습니다.
그 중에서도 가장 드라마틱한 성능 개선을 만들어주는 것은 바로 캐시입니다.
가장 큰 병목을 유발하는 지점에 대해 수행하지 않고 재사용하거나,
수행하더라도 아주 짧은 시간 내에 처리될 수 있게 개선해주기 때문입니다.
캐시도 CPU에서부터 WAS, WS까지 정말 여러 영역에 걸쳐있는데요
첫번째로 WAS에서 사용되는 로컬 캐시에 대해 이해해보고자 합니다.
거기서도 다시 좁혀서 스프링에서 캐싱이 어떻게 제공되는지 살펴보겠습니다.
이를 위해 스프링 공식 문서를 통해 스프링이 제공하는 캐시 추상화에 대해 살펴보겠습니다.
캐시 추상화
Since version 3.1, the Spring Framework provides support
for transparently adding caching to an existing Spring application.
Similar to the transaction support,
the caching abstraction allows consistent use of various caching solutions with minimal impact on the code.
스프링이란, 엔터프라이즈급 개발에 필요한 기능들을 제공함으로써
개발자로 하여금 비즈니스 로직에 집중할 수 있게 해주는 프레임워크입니다.
핵심 가치로는 IoC, DI, AOP를 꼽을 수 있는데요,
AOP 기능을 통해 제공되는 트랜잭션에 대해 이미 알고 계시다면
이번 캐시 추상화에 대해 이해하는 것도 어렵지 않으실 겁니다.
제가 가장 중요하게 생각하는 스프링이 제공하는 핵심 가치는 바로 비침투적이라는 점입니다.
비즈니스 코드에 영향을 최소화하면서도 엔터프라이즈급 개발에 꼭 필요한 기능들을 제공해주는 것이죠.
구현체에 따라 트랜잭션을 가져오고 커밋하고 롤백하는 코드가 달랐기 때문에,
스프링은 이를 일관된 방식으로 제어할 수 있게 트랜잭션을 추상화해서 이를 제공하고 있는데요,
마찬가지로 캐시 역시 추상화된 형태로 제공하고 구현체들을 개발자들이 선택할 수 있게 되어있습니다.
추상화에서 제공하는 확장성은 캐시를 어디에 저장할 것인지,
얼마 동안 보관하다가 자동삭제할 것인지 등에 대해 폭넓게 열려있습니다.
반대로 말하자면, 캐싱하고 재사용하는 핵심 로직에 대해서는 추상화되어 있지만,
저장 위치나 삭제 알고리즘이나 TTL 등의 설정은 구현체의 내용을 스스로 살피고 선택해 사용해야함을 뜻합니다.
캐시와 버퍼
The terms, “buffer” and “cache,” tend to be used interchangeably.
Note, however, that they represent different things.
캐시와 버퍼는 상호 교체 가능한 것처럼 사용되기도 하지만 엄연히 다른 의미입니다.
전통적으로 버퍼는 속도가 다른 두 매체 사이에서 성능 향상을 목적으로 사용되는 임시 저장공간을 의미합니다.
버퍼가 없다면 속도가 빠른 매체가 느린 매체를 기다리는 동안 계속 block이 발생할 텐데 이를 해소해주는 것이죠.
보통 버퍼는 한 번 쓰기, 읽기가 수행됩니다.
또한 버퍼는 최소 두 매체 중 한 매체는 버퍼의 존재를 인식합니다.
캐시는 반대로 숨겨져 있습니다.
두 매체 모두 캐싱이 발생하는지 알지 못합니다.
클라이언트는 타겟에게 요청을 보내는 것으로 인식하지만 중간에 프록시가 요청을 가로채고,
캐시 미스가 발생해 타겟으로 요청이 필요할 때 프록시가 클라이언트인 것처럼 타겟에게 요청을 보내기 때문입니다.
캐시도 성능 개선이라는 역할을 수행하지만, 보통 같은 데이터를 여러 번, 빠른 속도로 읽게 함으로써 역할합니다.
스프링에서의 캐싱 대상과 주의사항
At its core, the cache abstraction applies caching to Java methods,
thus reducing the number of executions based on the information available in the cache.
스프링에서 제공되는 캐시 추상화는 메서드 단위에 캐싱을 걸게 됩니다.
스프링과 마찬가지로 메서드에 캐싱 관련 애너테이션을 선언하게 되면,
해당 클래스를 타겟 객체로 갖는 프록시 객체가 빈으로 등록되는 식입니다.
그리고 타겟 객체로 요청이 올 경우, 이를 인터셉트하여 캐시 존재 유무를 판별하고,
없을 경우 타겟 객체를 호출한 뒤, 결과를 캐싱하고 반환하는 것입니다.
클래스 이름도 TransactionInterceptor, CacheInterceptor로 일관적입니다.
트랜잭션 기능과 유사하게 AOP를 통해 제공되며, 기본적으로 프록시 방식으로 동작합니다.
따라서 public 메서드에 대해서만 설정이 가능하고,
타겟 객체 내부에서 다시 타겟 객체를 호출할 경우 캐싱 기능이 동작하지 않습니다.
트랜잭션과 거의 동일하죠? 같은 AOP라는 원리에 의해 부가기능을 제공하기에 그렇습니다.
트랜잭션과 마찬가지로 public이 아닌 메서드에 캐싱 기능을 적용하려면 AspectJ를 고려해야 합니다.
메서드 레벨에 캐싱을 한다 함은,
메서드의 파라미터로 구분하여 반환값들을 캐싱해두는 것입니다.
매개변수가 1이었으면, 그때의 반환값을 1 : 반환값 이렇게 저장해두는 것입니다.
이후에 2로 요청이 오면 캐시가 없기 때문에 2로 수행한 결과도 2 : 반환값으로 저장해둡니다.
따라서, 여기서 메서드의 멱등함이 중요해집니다.
같은 매개변수로 요청했을 때 같은 결과가 반환되는 것이 보장될 때, 캐싱을 사용해야 합니다.
캐싱 애너테이션
- @Cacheable: Triggers cache population.
- @CacheEvict: Triggers cache eviction.
- @CachePut: Updates the cache without interfering with the method execution.
- @Caching: Regroups multiple cache operations to be applied on a method.
- @CacheConfig: Shares some common cache-related settings at class-level.
스프링에서 제공하는 캐싱 애너테이션입니다.
캐싱 대상이 되는 메서드에 Cacheable을 선언해서 사용할 수 있고,
특정 메서드 실행시 캐시를 삭제해야할 경우 CacheEvict를 사용할 수 있습니다.
CachePut은 특정 메서드 실행 시 반환값을 이용해 캐시를 업데이트해야 할 때 사용합니다.
Caching은 여러 캐시 설정들을 동시에 선언할 때 사용합니다.
가령 한 메서드 실행 시 여러 캐시를 Evict 해야할 때 사용할 수 있습니다.
CacheConfig는 클래스레벨에 선언되는 캐싱 설정이라고 생각하시면 됩니다.
캐싱 대상 메서드 동기화
The caching abstraction has no special handling for multi-threaded and multi-process environments,
as such features are handled by the cache implementation.
캐시를 다룰 때 고려해야하는 영역중 하나가 동시에 요청이 몰릴 때에 대비한 동기화입니다.
일순간에 대규모 I/O를 유발하는 요청이 마구 몰린다켠 캐시 설정이 제역할을 하지 못할 수 있습니다.
이는 단순한 성능 하락 정도에 그칠 수도 있지만,
Write Through 등의 설정에 따라 DB, Redis, WAS 등 전 영역에 부하를 전파하는 결과를 만들 수도 있습니다.
이와 같은 현상에 대해 이전에 NHN FORWARD 2020 캐시 성능 향상을 위한 시도 에서 학습했으며,
이를 대비해 당시 영상에선 상태값을 이용해 처리중일 경우 대기하도록 하여,
첫번째 요청만 처리하고, 이후 요청들은 대기하다가 첫번째 요청의 처리 완료 시점과
거의 동일한시점에 처리가 완료되게 구현했었습니다.
스프링 공식문서에서 말하듯 스프링의 캐시 추상화는 동기화에 대해선 보장하지 않습니다.
멀티 스레드 환경에 대해 핸들링하지 않는다는 것은,
여러 스레드가 동시에 해당 메서드를 호출했을 때 여러 스레드 모두 연산을 수행할 수도 있다는 의미입니다.
멀티 프로세스는 이중화 등으로 WAS가 여러 대일 때 싱크를 맞추는 것을 의미하는데
이 부분은 이번 포스팅에서 다루진 않겠습니다.
다시 스프링의 캐시 동기화로 돌아와서,
@Cacheable 애너테이션에서는 sync=true 라는 옵션을 제공합니다.
이는 구현체에 따라 제공할 수도 있고 제공하지 않을 수도 있습니다.
그래서 스프링 공식문서에서는 핸들링하지 않는다고 표현했습니다.
각 캐시 구현체는 CacheManager 인터페이스를 구현하는데 그 구현에 따라 동기화를 제공할 수도 있는 것입니다.
요약
스프링이 제공하는 캐시 추상화에 대해 간략히 알아본 내용을 요약합니다.
- 스프링은 AOP를 통해 비즈니스 코드에 영향을 최소화하며 캐시 기능을 제공한다
- 개발자는 CacheManager 라는 일관된 인터페이스를 이용해 캐시를 제어할 수 있다
- 버퍼는 두 매체 사이 속도차로 인한 성능 저하를 개선하기 위해 한 번 읽고 쓰이는 임시 중간 저장소다
- 캐시는 두 매체가 인지하지 못하게 동작하며 빠른 속도로 여러 번 읽게 함으로써 성능을 개선한다
- 스프링에선 메서드 단위로 캐싱을 선언할 수 있으며 프록시 모드가 기본이어서 접근제한자가 public이어야 한다
- 메서드마다 공간이 생기고 그 공간에 파라미터를 key로 리턴값을 value로 저장해두는 개념이다
- 메서드가 멱등하지 않다면 캐시 적용을 다시 고려해봐야 한다
- 캐시 구현체에 따라 sync 속성을 통해 동기화 기능을 제공할 수도 있다
다음 포스팅에선 구현체의 선택과 실제 코드를 통해
스프링에서 로컬 캐시를 어떻게 적용할 수 있는지 살펴보겠습니다.
학습 출처
https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache
'Java & Spring' 카테고리의 다른 글
Nested Class (0) | 2023.03.26 |
---|---|
응답 DTO, N+1, Open Session In View (2) | 2022.11.27 |
Spring 외부 설정 파일 import 및 프로퍼티 리팩터링 (2) | 2022.11.17 |
🙌 쓰레드풀을 이용해 두 비동기 작업 처리 완료 후 후속 처리하기 (0) | 2022.09.29 |
🥄 쓰레드풀 한 스푼 (2) | 2022.09.29 |