일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- Lombok
- 캡슐화
- 객체지향
- JIT
- SRP
- 자바
- 캐싱
- 협력
- 재사용성
- clean code
- 도메인 모델
- 캐시
- 클린코드
- REST API
- Refactor
- Java
- spring boot
- JPA
- 클린 코드
- 추상화
- 쿼리 최적화
- 객체지향의 사실과 오해
- cache
- 리팩토링
- 스프링부트
- 인터프리터
- 책임
- string
- 스프링
- 객체
- Today
- Total
GO SIWOO!
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (6) - Redis를 통한 Open API 결과 캐싱(Caching) 본문
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (6) - Redis를 통한 Open API 결과 캐싱(Caching)
gosiwoo 2023. 9. 6. 03:23[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (5) - 응답 form & Global Exception
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (4) - 회원기능 구현을 위한 스프링 Security와 JWT발 📌 기존의 회원기능 기존 프로젝트의 회원 기능은 kakao API를 통해서 진행했다. 그러나, 하드코딩
gosiwoo.tistory.com
📌그래서 Redis를 사용할 것인가?
결론부터 말하자면 해당 프로젝트에서 Redis를 통한 캐싱은 사용하지 않을 예정이다. 많은 고민을 해본 결과 단일 서버에서의 캐싱을 고려했을때 Global cache인 해당 방법을 사용하는 것은 다음과 같은 단점이 있었다.
- 배포를 했을 때 Redis 서버도 띄워줘야 한다는 비용적인 문제
- 단일 서버이기에 Cache의 데이터 접근을 고려할 필요가 없다
- 네트워크 트래픽 문제
따라서 다음 글에서 해결한 방법을 포스팅 할 예정이다.
📌Redis 캐싱
1. 캐싱의 필요성
한번 처리한 데이터를 임시 저장소에 저장해 동일한 요청이 왔을 경우 서비스 로직을 타지 않고 저장소에서 바로 읽어와 응답을 하도록 하여 성능, 응답속도 향상을 불러올 수 있다.
또한 캐시 저장소는 메인 메모리보다 더욱 빠른 응답속도를 가지는 특징이 있지만 메인 메모리보다 용량에 비해 비용이 비싸므로 캐시 저장소에 저장할 데이터는 여러 가지 고려를 한 뒤 선정해야 한다.
- API의 결과 같이 접근하는 시간이 오래 걸리는 데이터의 경우
- 데이터가 자주 변경되지 않는 데이터의 경우
- 동일한 데이터의 호출이 예측되는 경우
위의 3가지 또는 추가적인 여러 요소를 고려한 후 캐싱을 사용해야 한다.
2. Redis
Redis, Remote Dictionary Server는 Key-Value 구조의 비정형 데이터를 저장하고 관리하는 오픈소스 기반의 NoSQL DBMS로 주로 DB, 또는 Cache Server로 사용이 되는 편이다.
앞서 말한바와 같이 Key-Value 구조로 데이터를 저장하기에 쿼리를 사용하지 않고 Key 만으로 데이터를 접근할 수 있다.
또한 In-memory 데이터베이스로 매우 빠르지만 서버의 재시작 시 데이터의 유실이 있다는 점이 있습니다. 따라서 중요한 정보라면 데이터의 백업을 고려해야 한다.
유연한 데이터 구조를 가지며 String, List, Set, Hash, Bitmap, JSON 등 여러 가지 다양한 데이터 타입을 지원한다.
3. 캐싱을 위해 Redis 사용하는 이유?
- 다양한 자료구조의 지원
- In-memory 데이터베이스로 인한 빠른 성능
- Spring에서 Redis 라이브러리를 지원
- Local Memory보다 느리지만 별도의 서버로 가동되어 데이터의 보존이 가능하며 여러 노드에서도 실행이 가능
📌프로젝트에서 Redis를 사용한 부분
1. Open API를 사용하는 서비스의 성능문제
리팩토링 프로젝트에서 한국어 기초사전 OpenAPI를 사용해 단어 검색을 하는 서비스가 있는데 결과 반환에 많은 시간이 걸리고 있는 상태였습니다.
복잡한 XML로 반환되는 데이터를 하나하나 찢어 원하는 데이터의 형태로 재조립하는 로직도 복잡하거니와 API를 호출하는 서비스라 상당히 많은 시간이 걸려 이를 줄이고 싶었다
2. Spring Data Redis 라이브러리를 통한 Redis 접근
Spring에서는 Spring Data Redis를 통해 2가지 방식으로 Redis를 제공한다.
2-1. Jedis
우선 Jedis는 성능과 사용 편의성을 위해 설계된 Redis 내부의 클라이언트 라이브러리 이므로 Lettuce보다 더욱 가벼운 접근이 간단하지만 동기 방식의 작동으로 blocking 이슈가 있다는 단점이 있다.
그리고 결정적으로 Spring Boot의 버전이 업그레이드되며 자연스럽게 Jedis가 deprecated 되었다는 치명적인 문제가 있다.
2-1. Lettuce
Lettuce는 반면 동기, 비동기 방식을 둘 다 지원하며 non-blocking 하게 요청을 처리하고 복잡한 추상화를 통해 Redis의 확장을 더욱 쉽게 할 수 있다는 특징이 있다.
본 프로젝트에서는 Lettuce 방식으로 Redis 클라이언트를 사용하기로 하였다.
3. 프로젝트 코드
3-1. build.gradle
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
우선 Spring Data Redis 의존성을 추가해 주었다.
3-2. Config
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// Hash를 사용할 경우 Serializer
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
Redis 설정을 해주었는데 redisConnectionFactory() 메서드를 보면 host와 post 번호를 통해 Lettuce 방식으로 연결을 하는 메서드이다. 해당 메서드를 Bean에 등록하지 않으면 기본적으로 localhost::6379로 Spring 프로젝트가 실행되는 인스턴스의 6379 포트 번호로 Redis를 찾는다.
redisTemplate() 메서드는 낮은 추상화 방식의 RedisTemplate으로 Redis를 이용했을 때 직렬화, 역직렬화를 하는 설정을 나타내는 것이다. 나는 이 포르젝트에서 높은 추상화 방식의 CrudRepository으로 Redis를 이용하므로 직렬화 / 역직렬화 방식은 Java의 기본 직렬화를 따라갈 것이다.
따라서 CrudRepository 방식만을 사용할 것이라면 redisTemplate()는 직렬화에 영향을 끼치지 않는다.
추가로 @EnableRedisRepositories을 통해서 Redis 레포지토리를 활성화하고 value, basePackages, basePackageClasses 필드를 통해 적용 범위를 적용할 수 있다.
3-3. Redis 저장 객체 & CrudRepository
@Getter
@NoArgsConstructor
@RedisHash(value = "openApi", timeToLive = 600L)
public class OpenApiCache {
@Id
private String id;
private List<SearchedWordDto> datum;
public OpenApiCache(String word, List<SearchedWordDto> datum) {
this.id = word;
this.datum = datum;
}
}
public interface OpenApiCacheRepository extends CrudRepository<OpenApiCache, String> {
}
Redis에 저장되는 객체입니다. @RedisHash 어노테이션을 통해 Hash 형태로 저장됩니다. Key(id)-Value(Hash).
value 필드를 통해 prefix로 Key의 이름이 붙으며 timeToLive를 필드를 통해 얼마나 데이터가 남아 있을지 설정합니다. 600L은 600초, 즉 10분간 데이터가 Redis에 남아있음을 나타내고 있다.
Spring Data Jpa가 JpaRepository를 상속받는 것과 같이 CrudRepository를 상속받습니다. 이를 통해 아래의 이미지와 같이 CRUD 메서드를 사용할 수 있게 된다.
3-4. Service 로직
private boolean isWordCached(String q) {
return openApiCacheRepository.findById(q).isPresent();
}
private SearchedWordsResponse getSearchedWordResponseFromCache(String q) {
List<SearchedWordDto> datum = openApiCacheRepository.findById(q).get().getDatum();
return new SearchedWordsResponse("표준 한국어 대사전 Open API를 통해 단어" + q + "의 검색결과는 다음과 같습니다.", datum);
}
OpenApiCacheRepository가 상속받은 CrudRepository를 통해 Redis의 데이터를 찾는 것을 볼 수 있습니다. findById는 key값을 바탕으로 데이터를 찾는다.
📌성능개선
개선 전
개선 후
Redis를 사용하지 않은 API 성능 테스트는 5회 평균 492 ms, 0.492초의 응답속도,
Redis를 사용하여 성능을 개선한 API는 5회 평균 114 ms, 0.114초의 응답속도를 나타내는 것을 볼 수 있다.
개선 전과 후의 확연한 성능의 차이가 있었다.
여기서 성능에 관해 추가적으로 고려해야 할 요소로는 다음과 같다.
첫째는 Local에 Redis 서버를 두지 않고 Remote 서버의 Redis에 관해서 어느 정도의 성능 향상을 이루는지에 대해서 고려를 해야 한다. Redis 서버를 배포 상황에서는 다른 서버에 두어야 하기 때문이다.
둘째는 어느 정도의 ttl, 즉 데이터가 남아있는 시간을 어느정도 설정해야 하는가이다. 이는 실제로 API가 배포된 후 중복 데이터 접근 횟수, 간격과 OpenAPI가 사용되는 빈도등을 고려해 설정해야 할 것이다.
OpenAPI에 Redis를 적용해 보았는데 추가적으로 해야될 작업은 JWT 토큰 로그인 방식에서 Refresh Token또한 Redis로 관리해 성능 향상을 이루고자 한다.
📌리팩토링 과정에서 참고 한 글들
Jedis vs. Lettuce: An Exploration | Redis
Spring Data Redis로 Redis TTL을 구성하는 방법은 무엇입니까? | Baeldung
Spring Boot 에서 Redis 사용하기 :: 뱀귤 블로그 (tistory.com)
caching - Redis 캐시와 메모리를 직접 사용하는 것 비교 - 스택 오버플로 (stackoverflow.com)
redis - WRONGTYPE Operation against a key holding the wrong kind of value php - Stack Overflow
'Develop > 팀 프로젝트, 나홀로 리팩토링' 카테고리의 다른 글
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (8) - JDBC Batch INSERT (0) | 2023.10.18 |
---|---|
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (7) - Ehcache 3를 통한 Open API 호출 서비스 응답 속도 향상 (1) | 2023.09.15 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (5) - 응답 form & Global Exception (0) | 2023.08.17 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (4) - 회원기능 구현을 위한 스프링 Security와 JWT발급 (2) | 2023.05.19 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (3) - distinct, rand, limit, 프로젝션 쿼리 작성과 에러 (0) | 2023.05.12 |