Hibernate:
select
u1_0.user_id,
****
from
`user` u1_0
where
u1_0.user_id=?
Hibernate:
select
distinct o1_0.objective_id,
****
from
objective o1_0
join
`user` u1_0
on u1_0.user_id=o1_0.user_id
where
u1_0.user_id=?
and o1_0.is_closed=0
order by
o1_0.idx
Hibernate:
select
u1_0.user_id,
****
from
`user` u1_0
where
u1_0.user_id=?
우리 서비스에서는 API 요청에 AOP를 통해 유저 id를 추출하는 부분이 존재하고 일부 API에서는 해당 유저가 자원에 접근 권한이 있는지 유저 정보를 따로 조회하는 인가 과정까지 총 2번의 유저 정보를 DB에 요청한다.
해당 프로젝트에는 Custom 어노테이션인 @LoginUser
를 사용하여 유저의 아이디를 모든 API 메소드에 주입해준다.
이 과정에서 매번 DB를 통해 데이터를 가져오기 떄문에 이 부분을 캐싱하면 성능 향상이 분명이 있을 것 같아 캐시를 적용해보기로 했다.
캐시로 Redis를 선택한 이유는 유저의 Refresh Token
을 Redis에 저장하는 로직 때문에 이미 서버에 Redis 서버가 띄워져 있어 바로 사용할 수 있기 때문이기도 하다. 또한 서비스 특성 상 Redis에 가해지는 부하가 그렇게 크지 않아 캐싱을 하게 돼도 충분히 수용 가능하다고 판단하였다.
RedisConfig를 먼저 설정해주어야 한다. application.yml에 redis.host, redis.port를 각자 상황에 맞는 정보를 넣어주고 @Value를 통해 주입한다.
@Configuration
@EnableRedisRepositories
@RequiredArgsConstructor
public class RedisConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
private final ObjectMapper objectMapper;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
@Primary
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
기존에 Redis를 Refresh Token 저장소로 사용하고 있었기 때문에 변경된 부분은 redisTemplate이었다.
Refresh Token은 그냥 String이었기 때문에 StringRedisSerializer
를 사용하여 직렬화를 통해 Redis를 저장하였다.
그런데 User 엔티티 데이터를 저장하기 위해서는 Json 형태로 저장하여야 하기 때문에 GenericJackson2JsonRedisSerializer
를 사용하였다.
근데 Json으로 저장할 수 있는 다른 직렬화 방법이 존재하는데, 여기서 어떤 방법이 있는지 알아보자.
String 값을 그대로 저장함
JSON 형태로 직접 encoding, decoding을 해줘야한다는 단점이 있지만 위의 두 개의 serializer에서 발생할 수 있는 문제가 발생하지 않는다.
종속적이지 않기 때문에
모든 쓰레드의 요청이 왔을 때 문제 없이 처리할 수 있다.Class Type을 지정하고 이를 JSON 형태로 저장함
타입의 문제가 발생하는 경우
가 발생한다.객체의 클래스 지정 없이 모든 Class Type을 JSON 형태로 저장할 수 있음
class 및 package까지 전부 함께 저장
하게 되어 다른 프로젝트에서도 package까지 일치시켜줘야 한다.일단 우리 서비스는 MSA 구조가 아니기도 하고 모든 것을 고려해봤을때 GenericJackson2JsonRedisSerializer가 문제를 해결할 수 있는 Serializer이기 때문에 기존 StringRedisSerializer를 대체하기로 하였다.
@Configuration
@EnableRedisRepositories
@RequiredArgsConstructor
public class RedisConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
private final ObjectMapper objectMapper;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
@Primary
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
Redis의 host와 port 정보를 RedisConnectionFactory 빈으로 등록한다.
여기서 중요한 것은 RedisTemplate이다.
아까 설명했던 것처럼 ValueSerializer를 GenericJackson2JsonRedisSerializer로 설정할 것이다.
@Configuration
@EnableCaching
@RequiredArgsConstructor
public class CacheConfig {
private final RedisConnectionFactory redisConnectionFactory;
private final ObjectMapper objectMapper;
@Bean
public CacheManager redisCacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofSeconds(60 * 60));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build();
}
}
또한 캐시를 적용하기 위해서 @EnableCaching 어노테이션을 붙여야 한다. SpringApplication 위에 붙이거나 필자처럼 Configuration 파일에 붙여도 된다.
여기서도 ValueSerializer로 같은 것을 채택한다.
또한 중요한 것은 TTL
인데 필자는 1시간으로 설정하였다.
서비스의 Access Token 유효 기간이 20분인데 이에 맞춰서 20분으로 설정해야 할지, 조금 더 길게 설정해야 할지에 대한 명확한 기준이 서지 않아서 일단 1시간으로 설정하였다.
운영하다가 경험이 쌓이게 되면 20분으로 수정하거나 다른 시간으로 수정할 것이다.
위 결과가 캐시를 적용하지 않았을 때의 결과고 아래가 Redis 캐시를 적용했을 때의 결과다.
먼저 10개의 쓰레드가 100번의 요청을 하도록 테스트하였다. 결과는 평균 8배 가량 빠르고, 99% 최악의 API 처리 시간을 따져보아도 2배 가량 빠른 것을 확인할 수 있다.
이 시간은 요청 횟수가 늘어날수록 벌어졌는데
캐시 적용 효과가 확실한 것을 확인할 수 있었다.
하지만 보안적으로
맞는 솔루션인지는 정확한 판단이 안된다.. 유저 엔티티에 대한 정보를 Redis에 그대로 캐시하기 때문이다.
하지만 Redis에 접근할 수 있는건 EC2 내부
에서만 가능하기도 하고 DB이기 때문에 큰 문제가 없을 것이라고 판단한다!
또한 TTL로 인해 일정 시간 이후에 지워지기 때문에 일단 적용하고 생각하고 추가적인 조사를 해보고 변경 사항을 적용하겠다.