티스토리 뷰
[ 💻 Cache ]
✔ 캐시
- 미리 데이터의 복사본을 저장함으로써 처리 속도를 향상 시킬 수 있습니다.
- 캐시는 디스크보다 용량 대비 값이 비싼 대신 빠른 응답속도를 보여줍니다.
- 캐시가 데이터가 있다면 데이터 베이스에 요청을 하지 않기 때문에 부하를 분산 시킬 수 있습니다.
✔ 적용 대상
- 동일한 입력에 대해서 항상 동일한 결과를 반환하는 기능에 적합합니다.
- 파레토 법칙에 의하여 자주 조회되는 기능을 캐시로 적용한다면 성능 향상을 기대할 수 있습니다.
- 변경이 잦은 데이터는 데이터를 동기화를 자주 시켜야 하기 때문에 적합하지 않습니다.
✔ 캐시의 일반적인 구조 : Look aside Cache
- 웹 서버는 캐시에 데이터가 존재하는지 확인합니다.
- 존재하면 데이터를 가져와서 반환하고 존재하지 않는다면 DB에 데이터를 요청합니다.
- DB에 요청해서 새로운 데이터가 있을 경우 캐시에 저장 합니다.
[ 😎 적용 배경 ]
✔ 파레토 법칙
전체 결과의 80%가 전체 원인의 20%에서 일어나는 현상
즉, 서비스에서는 전체의 20%에 해당하는 적은 기능에서 대부분 요청이 된다고 볼 수 있습니다.
예를 들어 현재 star-dabang 프로젝트에서 대부분의 요청은 메뉴를 조회하는 기능이 될 것이라고 예상을 할 수 있습니다. 그리고 메뉴 특성상 자주 바뀌지 않기 때문에 업데이트가 자주 발생하지 않습니다. 그래서 반복적인 메뉴 조회 기능에 대하여 캐싱의 적용을 고려하였습니다.
[ 👀 Local Cache vs Global Cache ]
✔ 로컬 캐시
서버 마다 캐시를 따로 적용하게 됩니다. 서버 내에서 사용하기 때문에 서버의 자원을 사용하는 대신 속도가 굉장히 빠릅니다.
하지만 캐시에 저장된 데이터가 변경된다면 모든 웹서버에 동기화를 시켜주어야 합니다. 만약 웹서버가 4개 이상이라면 동기화를 시키는데 있어서 네트워크 비용이 많이 소모될 것이고 성능이 저하 됩니다.
✔ 글로벌 캐시
여러 서버에서 하나의 외부 캐시 서버를 참조하게 되는 구조입니다. 그래서 별도로 하나의 캐시서버를 참조하기 때문에 데이터 변경에 대하여 동기화가 쉽습니다.
하지만 외부 캐시 서버를 이용해야 하기 때문에 로컬 캐시보다 속도가 느립니다.
✔ 글로벌 캐시의 선택
로컬 캐시는 속도가 빠른 반면 서버가 많을 수록 동기화를 하는 부하가 생긴다는 단점과 서버의 자원을 사용하기 때문에 자바를 사용하는 경우에는 Full GC가 일어 났을 때 더 오래 걸릴 수 있다는 단점이 생길 수 있습니다.
현재 서비스에서 트래픽이 커져서 스케일 아웃으로 확장할 경우도 고려하고 있기 때문에 새롭게 생기게 된 서버에도 데이터를 동기화를 시켜주어야 하는 면이 생기고 지속적으로 캐시 적용으로 인한 장점을 얻기엔 힘들다고 생각했습니다. 또한 메뉴의 정합성은 서비스적인 측면에서 중요한 요소로 작용하기 때문에 로컬 캐시보다 성능적으로 느리긴 하지만 데이터의 동기화가 수월하고 GC에도 영향이 없으며 서비스의 확장에 용이한 글로벌 캐시를 선택하였습니다.
[ 👀 캐시 저장소 ]
캐시 솔루션으로 자주 비교되는 Redis와 Memcached를 비교하였습니다.
둘의 공통점은 key-value 쌍으로 메모리에 저장이 되며 오픈 소스입니다.
✔ Memcached vs Redis
- 데이터 유형은 Memcached는 문자열만 지원하는 반면 Redis는 다양한 데이터 타입(list, set, hash등)을 지원합니다.
- 데이터의 크기 제한은 Memcached는 1MB, Redis는 최대 512MB를 지원합니다.
- 디스크 I/O 덤핑을 Memcached는 서드파티 라이브러리를 사용해야 하지만 Redis는 디스크 덤핑을 위해 RDB(Redis Database file), AOF(Apend-only files)의 기능을 지원합니다.
- 복제를 위해서 Redis는 Primary 스토리지를 복제하여 클러스터를 증가시키는 기능이 있어 확장성 및 가용성이 높습니다.
- Memcached는 멀티스레드, Redis는 싱글스레드입니다. Redis는 1번에 1개의 명령어만 실행할 수 있으며 그래서 모든 키를 탐색하는 명령어는 성능적인 이유로 쓰지 않는 것이 좋습니다.
- 다양한 Eviction(삭제) 정책을 Redis에서 지원하지만 Memcached에서는 LRU 정책만을 지원합니다.
✔ Redis 선택
다양한 데이터 타입을 활용할 수 있다는 점, 자체적으로 복제기능을 제공하여 확장성 및 가용성이 높다는 점, 많은 삭제 정책을 지원한다는 점, 결정적으로 스프링에서 Cache 저장소로 Redis를 지원하고 있다는 점을 고려하여 선택하였습니다.
[ ✅ 캐싱 적용하기 ]
application.yml
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
스프링 부트는 yml에 정보를 참조하여 디폴트로 Lettuce라는 클라이언트로 Redis 저장소를 연결합니다.
클라이언트 종류는 Lettuce와 Jedis가 있는데 성능적으로 비동기로 처리하는 Lettuce가 우위에 있습니다.
Jedis 보다 Lettuce 를 쓰자
Java의 Redis Client는 크게 2가지가 있습니다. Jedis Lettuce 둘 모두 몇천개의 Star를 가질만큼 유명한 오픈소스입니다. 이번 시간에는 둘 중 어떤것을 사용해야할지에 대해 성능 테스트 결과를 공유하
jojoldu.tistory.com
RedisCacheConfig.java
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
/**
* Spring Boot Default Client인 Lettuce 사용
* Cache Key Serializer는 Default인 StringRedisSerializer 사용
* Cache Value Serializer는 다양한 데이터 타입을 위해 GenericJackson2JsonRedisSerializer 사용
* disableCachingNullValues으로 Null은 캐싱에서 제외
*/
@RequiredArgsConstructor
@Configuration
public class RedisCacheConfig {
private final RedisConnectionFactory connectionFactory;
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory)
.cacheDefaults(configuration).build();
}
}
RedisConnectionFactory에서 Connection 정보를 가져와서 제가 설정한 정보화 함께 RedisCacheManager 빈으로 등록하였습니다.
자바 객체를 직렬화를 시킬때 Key Serializer는 Default 값인 StringRedisSerializer를 사용하였기에 생략을 하였고
Value Serializer는 GenericJackson2JsonRedisSerializer를 사용하였습니다.
GenericJackson2JsonRedisSerializer는 Jackson의 Default Typing을 적용합니다. Default Typing 이란 클래스의 정보를 ID로 함께 직렬화를 시켜 레디스에 저장시킵니다.
그렇다면 이 후 직렬화 시켰을 때 클래스 정보로 레디스에 있는 데이터를 역직렬화 시켜 사용할 수 있습니다.
MenuService.java
@RequiredArgsConstructor
@Service
public class MenuService {
private final CategoryRepository categoryRepository;
private final ProductRepository productRepository;
@Cacheable(value = CacheName.CATEGORY, key = CacheKey.DEFAULT)
public List<TypeCategoryData> getAllCategories() {
List<TypeCategoryData> typeCategories = new ArrayList<>();
CategoryType[] types = CategoryType.values();
for (CategoryType type : types) {
List<CategoryData> categoryData = categoryRepository.findAllByType(type.getKey());
typeCategories.add(new TypeCategoryData(type, categoryData));
}
return typeCategories;
}
@Cacheable(value = CacheName.PRODUCT, key = "#categoryId")
public List<ProductData> getProductsByCategoryId(int categoryId) {
return productRepository.findAllByCategoryIdAndActive(categoryId);
}
}
카테고리 목록을 가져오는 메소드와 해당 카테고리의 메뉴 목록을 가져오는 메소드에 캐시를 적용하였습니다.
Key값으로는 SpEL를 사용할 수 있었습니다.
위 코드에는 나오지 않지만 정합성을 위해서 메뉴 정보가 바뀌었을 시에는 아래와 같이 적용하였습니다.
@CacheEvict(value = CacheName.PRODUCT, key = "#categoryId")
public Product createProduct(int categoryId, ProductCreateCommand productCreateCommand) {
...
}
카테고리의 약 30개의 상품 기준
✔ 캐시 미적용(120ms)
✔ 캐시 적용 (15ms)
Postman을 사용해서 충분한 워밍업을 하고 테스트를 진행하였으며 120ms -> 15ms로 시간을 단축하였습니다.
[ ✨ 프로젝트 ]
github.com/f-lab-edu/star-dabang
f-lab-edu/star-dabang
스타벅스 어플을 모티브로 한 카페 서비스. Contribute to f-lab-edu/star-dabang development by creating an account on GitHub.
github.com
'Project > star-dabang' 카테고리의 다른 글
대용량 트래픽을 위해 확장성 있는 세션 구조 설계 #2 (0) | 2021.03.28 |
---|---|
대용량 트래픽을 위해 확장성 있는 세션 구조 설계 #1 (0) | 2021.02.22 |
- Total
- Today
- Yesterday