
여러 모듈에서 빈번히 호출되는 사용자 정보 접근에 대해 효율성을 높이기 위해 Redis를 도입하여 성능 최적화를 시도한 경험을 공유합니다. 특히 travel와 pay 모듈에서는 FeignClient를 통해 사용자 정보를 자주 요청하게 되어, 매번 DB 접근을 피할 수 있는 방안을 찾고자 했습니다. Redis 캐시를 활용하여 네트워크 부하를 줄이고 빠른 데이터 접근이 가능하게끔 시스템을 개선하였습니다.
redis-server.exe 실행 시 아래와 같이 Redis 서버가 실행됩니다.redis-cli.exe를 실행하면 Redis 서버의 캐싱 내용을 조회할 수 있는 CLI가 뜹니다.
Redis CLI에서 자주 사용한 명령어는 아래 세 가지입니다:
keys *: Redis 서버에 있는 모든 key 검색type [키 값]: 해당 key의 데이터 타입 조회FLUSHALL: 모든 데이터 삭제주의: Redis 서버나 Kafka 서버가 다운될 경우 모듈 서비스가 중단되는 경우가 종종 발생합니다.
user 모듈의 build.gradle 파일에 Redis 관련 의존성을 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
user 모듈의 application.yml 파일에 Redis 서버 설정을 추가합니다.
spring:
data:
redis:
host: localhost
port: 6379
Redis의 연결과 직렬화를 설정하기 위해 RedisConfig 클래스를 작성합니다.
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 키 직렬화 설정
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// 값 직렬화 설정
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
UserController에서 사용자 정보 조회 API를 구현하고, UserService에서는 Redis 캐시에서 데이터를 가져오거나 없을 경우 DB에서 조회 후 캐싱하도록 구성했습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
@GetMapping("/{userId}")
public ResponseEntity<?> getOtherUserInfo(@PathVariable Long userId) {
return ResponseEntity.status(HttpStatus.OK)
.body(ResponseMessage.success(userService.getOtherUserInfo(userId)));
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
private final ProfileRepository profileRepository;
public UserInfoResponseDTO getUserInfo(UserEntity currentUser) {
String cacheKey = "userInfo:" + currentUser.getId();
// Redis에서 캐시된 데이터 조회
Object cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
log.info("Cache hit for user ID: {}", currentUser.getId());
return (UserInfoResponseDTO) cachedData;
}
ProfileEntity profileEntity = profileRepository.findByUserId(currentUser.getId())
.orElseThrow(() -> new BizException(PROFILE_NOT_FOUND_ERROR));
UserInfoResponseDTO userInfo = UserInfoResponseDTO.builder()
.id(currentUser.getId())
.username(currentUser.getUsername())
.nickname(currentUser.getNickname())
.phone(currentUser.getPhone())
.email(currentUser.getEmail())
.status(currentUser.getStatus().getUserStatus())
.introduction(profileEntity.getIntroduction())
.mbti(profileEntity.getMbti())
.gender(profileEntity.getGender().getGender())
.smoking(profileEntity.getSmoking().getSmoke())
.birth(profileEntity.getBirth())
.ratingAvg(profileEntity.getRatingAvg())
.build();
// Redis에 캐시 저장 (만료 시간 1시간 설정)
redisTemplate.opsForValue().set(cacheKey, userInfo, 1, TimeUnit.HOURS);
log.info("Cache miss for user ID: {}. Data cached.", currentUser.getId());
return userInfo;
}
}
UserInfoResponseDTO 클래스의 LocalDate 타입 필드를 직렬화할 때 문제가 발생하여 jackson-datatype-jsr310 의존성을 추가하고 RedisConfig의 직렬화 설정을 수정했습니다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
Redis 캐시를 도입하여 사용자 정보에 대한 빠른 접근과 효율적인 네트워크 부하 분산을 달성할 수 있었습니다. Redis에 데이터를 캐시할 때 GenericJackson2JsonRedisSerializer을 사용하여 클래스 정보 출력 문제도 해결했습니다.

GenericJackson2JsonRedisSerializer와 Jackson2JsonRedisSerializer의 차이점은 직렬화 방식의 유연성과 클래스 정보 포함 여부에서 나타납니다.
Jackson2JsonRedisSerializer와 클래스 정보(@class) 포함Jackson2JsonRedisSerializer는 객체를 JSON 형태로 직렬화할 때 클래스 정보를 함께 포함합니다. 기본적으로 이 직렬화기는 객체를 직렬화할 때 클래스 타입 정보를 JSON에 @class 필드로 추가하여 역직렬화 시 이 정보를 참고할 수 있도록 합니다. 예를 들어, 다음과 같이 UserInfoResponseDTO 객체를 JSON으로 직렬화하면 @class 필드가 자동으로 추가됩니다.
{
"@class": "com.example.dto.UserInfoResponseDTO",
"id": 1,
"username": "exampleUser",
"nickname": "exUser"
}
장점:
단점:
@class 필드가 포함됨에 따라 데이터 크기가 증가할 수 있습니다.
Jackson2JsonRedisSerializer는 특정 클래스에 종속적이라 복잡한 데이터 구조에서@class정보가 필요하지 않은 경우, 또는 클래스 구조가 자주 변경되는 프로젝트에서는 다소 불편할 수 있습니다.
GenericJackson2JsonRedisSerializer와 클래스 정보 제외GenericJackson2JsonRedisSerializer는 Jackson2JsonRedisSerializer와 달리 클래스 정보를 JSON에 포함하지 않습니다. 기본적으로 일반적인 JSON 포맷으로 직렬화하고 클래스 정보는 제외하므로, 데이터에 @class 필드가 포함되지 않습니다.
예를 들어, UserInfoResponseDTO 객체를 직렬화하면 다음과 같이 순수한 JSON 데이터로 저장됩니다.
{
"id": 1,
"username": "exampleUser",
"nickname": "exUser"
}
장점:
단점:
결론적으로,
GenericJackson2JsonRedisSerializer는 더 유연하고 간단한 JSON 직렬화를 제공하나, 타입 정보가 누락되므로 일반적인 객체가 아닌 복잡한 객체 간 변환이 필요한 경우Jackson2JsonRedisSerializer를 사용하는 것이 더 좋을 수 있습니다.
GenericJackson2JsonRedisSerializer와 Jackson2JsonRedisSerializer 외에도 다양한 직렬화 방식이 있습니다. 몇 가지 주요 옵션을 소개하겠습니다.
StringRedisSerializerJdkSerializationRedisSerializerSerializable 인터페이스를 이용해 직렬화합니다.JVM에 종속적이며, JSON처럼 언어 간 호환이 어려워 데이터가 길어질 수 있습니다.KryoSerializerProtobufSerializer (Google Protobuf)| 직렬화 방식 | 특징 및 장점 | 단점 |
|---|---|---|
Jackson2JsonRedisSerializer | 클래스 정보를 포함하여 타입 안전성 제공 | 데이터 크기 증가, 클래스 변경 시 오류 가능 |
GenericJackson2JsonRedisSerializer | 클래스 정보 제외하여 간단한 JSON 제공 | 역직렬화 시 명시적 타입 지정 필요 |
StringRedisSerializer | 단순 문자열로 직렬화 | 객체 직렬화 불가 |
JdkSerializationRedisSerializer | 자바 기본 직렬화로 객체 그대로 저장 | JVM 종속적, 데이터 길이가 길어짐 |
KryoSerializer | 빠른 바이너리 직렬화로 성능 우수 | 설정 복잡, 특정 버전 호환성 문제 |
ProtobufSerializer | 크로스 플랫폼 호환 가능하며 데이터 크기 작음 | 설정 및 관리 복잡 |
선택 요령:
StringRedisSerializer나 GenericJackson2JsonRedisSerializer가 적합합니다.Jackson2JsonRedisSerializer가 유리합니다.KryoSerializer나 ProtobufSerializer와 같은 바이너리 직렬화 방식을 고려해 볼 수 있습니다.이 차이점을 바탕으로 시스템 요구 사항과 객체의 구조에 따라 적합한 직렬화기를 선택하면, Redis를 효율적으로 사용할 수 있습니다.