Redis 적용기

seokseungmin·2024년 11월 12일

Today I Learned

목록 보기
16/20
post-thumbnail

Redis를 통한 효율적인 데이터 접근과 네트워크 부하 감소 도전기

프로젝트 개요

여러 모듈에서 빈번히 호출되는 사용자 정보 접근에 대해 효율성을 높이기 위해 Redis를 도입하여 성능 최적화를 시도한 경험을 공유합니다. 특히 travelpay 모듈에서는 FeignClient를 통해 사용자 정보를 자주 요청하게 되어, 매번 DB 접근을 피할 수 있는 방안을 찾고자 했습니다. Redis 캐시를 활용하여 네트워크 부하를 줄이고 빠른 데이터 접근이 가능하게끔 시스템을 개선하였습니다.


Windows에서 Redis 서버 실행

  • Redis 서버: redis-server.exe 실행 시 아래와 같이 Redis 서버가 실행됩니다.
  • Redis CLI: redis-cli.exe를 실행하면 Redis 서버의 캐싱 내용을 조회할 수 있는 CLI가 뜹니다.


Redis 기본 명령어

Redis CLI에서 자주 사용한 명령어는 아래 세 가지입니다:

  1. keys *: Redis 서버에 있는 모든 key 검색
  2. type [키 값]: 해당 key의 데이터 타입 조회
  3. FLUSHALL: 모든 데이터 삭제

프로젝트 구성 및 실행 순서

  1. Redis 서버 실행
  2. ZookeeperKafka 서버 실행
  3. API Gateway, Travel, User 서버 순으로 구동

주의: Redis 서버나 Kafka 서버가 다운될 경우 모듈 서비스가 중단되는 경우가 종종 발생합니다.


Redis 설정 및 코드 적용

1. Redis 의존성 추가

user 모듈의 build.gradle 파일에 Redis 관련 의존성을 추가합니다.

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

2. application.yml 설정

user 모듈의 application.yml 파일에 Redis 서버 설정을 추가합니다.

spring:
  data:
    redis:
      host: localhost
      port: 6379

3. RedisConfig 설정

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;
    }
}

4. UserController와 UserService 구현

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을 사용하여 클래스 정보 출력 문제도 해결했습니다.

참고

GenericJackson2JsonRedisSerializerJackson2JsonRedisSerializer의 차이점은 직렬화 방식의 유연성과 클래스 정보 포함 여부에서 나타납니다.

1. Jackson2JsonRedisSerializer와 클래스 정보(@class) 포함

Jackson2JsonRedisSerializer객체를 JSON 형태로 직렬화할 때 클래스 정보를 함께 포함합니다. 기본적으로 이 직렬화기는 객체를 직렬화할 때 클래스 타입 정보를 JSON에 @class 필드로 추가하여 역직렬화 시 이 정보를 참고할 수 있도록 합니다. 예를 들어, 다음과 같이 UserInfoResponseDTO 객체를 JSON으로 직렬화하면 @class 필드가 자동으로 추가됩니다.

{
  "@class": "com.example.dto.UserInfoResponseDTO",
  "id": 1,
  "username": "exampleUser",
  "nickname": "exUser"
}

장점:

  • 이 방식은 Redis에 저장된 JSON 데이터를 역직렬화할 때 원래의 클래스 타입을 유지할 수 있어 타입 안정성이 높습니다.
  • 다양한 클래스 타입이 있는 경우, JSON 데이터를 읽을 때 어떤 클래스인지 구분할 수 있어 객체 간 변환 오류를 줄여줍니다.

단점:

  • @class 필드가 포함됨에 따라 데이터 크기가 증가할 수 있습니다.
  • 프로젝트의 변경으로 클래스 패키지가 변경되면 역직렬화 시 오류가 발생할 수 있습니다.

Jackson2JsonRedisSerializer는 특정 클래스에 종속적이라 복잡한 데이터 구조에서 @class 정보가 필요하지 않은 경우, 또는 클래스 구조가 자주 변경되는 프로젝트에서는 다소 불편할 수 있습니다.

2. GenericJackson2JsonRedisSerializer와 클래스 정보 제외

GenericJackson2JsonRedisSerializerJackson2JsonRedisSerializer와 달리 클래스 정보를 JSON에 포함하지 않습니다. 기본적으로 일반적인 JSON 포맷으로 직렬화하고 클래스 정보는 제외하므로, 데이터에 @class 필드가 포함되지 않습니다.

예를 들어, UserInfoResponseDTO 객체를 직렬화하면 다음과 같이 순수한 JSON 데이터로 저장됩니다.

{
  "id": 1,
  "username": "exampleUser",
  "nickname": "exUser"
}

장점:

  • JSON 형식이 더 간단해지고 데이터 크기가 작아져 저장 공간과 네트워크 비용이 절약됩니다.
  • 클래스 정보가 포함되지 않아 시스템 전반에서 좀 더 범용적으로 사용 가능합니다.

단점:

  • 역직렬화할 때 클래스 타입을 명시적으로 지정해야 합니다.
  • 다양한 타입의 객체가 저장될 경우, 역직렬화 시 타입을 맞추기 위한 추가적인 코드가 필요할 수 있습니다.

결론적으로, GenericJackson2JsonRedisSerializer는 더 유연하고 간단한 JSON 직렬화를 제공하나, 타입 정보가 누락되므로 일반적인 객체가 아닌 복잡한 객체 간 변환이 필요한 경우 Jackson2JsonRedisSerializer를 사용하는 것이 더 좋을 수 있습니다.


다른 직렬화 방식

GenericJackson2JsonRedisSerializerJackson2JsonRedisSerializer 외에도 다양한 직렬화 방식이 있습니다. 몇 가지 주요 옵션을 소개하겠습니다.

1. StringRedisSerializer

  • 특징: 단순히 문자열(String)로 데이터를 직렬화합니다.
  • 용도: 키 또는 간단한 문자열 값에 유용합니다.
  • 장점: 가장 단순하고 효율적이며, 타입 변환의 이슈가 없습니다.
  • 단점: 복잡한 객체를 저장할 수 없습니다. 모든 데이터가 문자열로 직렬화됩니다.

2. JdkSerializationRedisSerializer

  • 특징: 자바의 기본 직렬화 방식인 Serializable 인터페이스를 이용해 직렬화합니다.
  • 용도: Java 객체를 원형 그대로 Redis에 저장할 때 적합합니다.
  • 장점: 모든 Java 객체를 쉽게 저장하고 불러올 수 있습니다.
  • 단점: 직렬화된 데이터가 JVM에 종속적이며, JSON처럼 언어 간 호환이 어려워 데이터가 길어질 수 있습니다.

3. KryoSerializer

  • 특징: 자바 객체를 바이너리 형태로 직렬화하는 매우 빠른 라이브러리입니다.
  • 용도: 고성능이 필요한 경우, 특히 빠른 직렬화 및 역직렬화가 중요한 대용량 데이터를 다룰 때 유용합니다.
  • 장점: 자바 직렬화보다 성능이 좋고, 직렬화된 데이터가 작아 네트워크 비용이 적습니다.
  • 단점: 설정이 복잡하고, 특정 버전에서 호환성 문제가 발생할 수 있습니다.

4. ProtobufSerializer (Google Protobuf)

  • 특징: Google의 Protocol Buffers를 이용해 직렬화합니다.
  • 용도: JSON보다 성능이 뛰어나고 데이터 크기가 작은 직렬화가 필요할 때 적합합니다.
  • 장점: 크로스 플랫폼 호환이 가능하며 데이터 크기가 작습니다.
  • 단점: 구조화된 데이터에 적합하며 설정 및 관리가 복잡합니다.

요약

직렬화 방식특징 및 장점단점
Jackson2JsonRedisSerializer클래스 정보를 포함하여 타입 안전성 제공데이터 크기 증가, 클래스 변경 시 오류 가능
GenericJackson2JsonRedisSerializer클래스 정보 제외하여 간단한 JSON 제공역직렬화 시 명시적 타입 지정 필요
StringRedisSerializer단순 문자열로 직렬화객체 직렬화 불가
JdkSerializationRedisSerializer자바 기본 직렬화로 객체 그대로 저장JVM 종속적, 데이터 길이가 길어짐
KryoSerializer빠른 바이너리 직렬화로 성능 우수설정 복잡, 특정 버전 호환성 문제
ProtobufSerializer크로스 플랫폼 호환 가능하며 데이터 크기 작음설정 및 관리 복잡

선택 요령:

  • 단순한 키-값 형태의 데이터를 다루거나 JSON 포맷이 중요하지 않을 때는 StringRedisSerializerGenericJackson2JsonRedisSerializer가 적합합니다.
  • 복잡한 객체 간의 직렬화 및 타입 안전성이 중요할 때는 Jackson2JsonRedisSerializer가 유리합니다.
  • 고성능 및 최소화된 데이터 크기가 요구될 때는 KryoSerializerProtobufSerializer와 같은 바이너리 직렬화 방식을 고려해 볼 수 있습니다.

이 차이점을 바탕으로 시스템 요구 사항과 객체의 구조에 따라 적합한 직렬화기를 선택하면, Redis를 효율적으로 사용할 수 있습니다.

profile

0개의 댓글