본문 바로가기

Redis

Redis + Spring 설정 및 간단한 실습

Redis 환경 준비하기

환경: Unubunt 20.04 LTS

Unubunt 20.04 LTS EC2 인스턴스에서 다음 명령어를 사용하여 Docker로 Redis 컨테이너를 구동한다.

docker run -d -p 6379:6379 --name=redis redis

 

해당 명령어를 입력하면, 다음과 같이 구동 중인 Redis 컨테이너를 확인할 수 있다. 

 

다음 명령어를 사용하여 redis-cli에 접속한다.

docker exec -it redis redis-cli

 

Spring Boot Project 

환경: Spring Boot 2.7.5

build.gradle에 다음과 같은 dependency를 추가한다.

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

 

Spring Project에서 Redis를 사용하기 위해서 다음과 같이 코드를 작성한다.

// application.yml
spring:
  redis:
    lettuce:
      pool:
        max-idle: 8 // pool의 "idle" 커넥션 최대 수, 음수=무제한
        max-active: 10 // pool에 할당될 수 있는 커넥션의 최대 수
    host: {host}
    port: {port}
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
        return redisTemplate;
    }
}
  • Java에는 Redis Client 라이브러리가 여러 가지 존재하지만, 현재 Spring에서는 Jedis 대신 Lettuce를 Default로 생성한다.
  • yml 파일에 위와 같은 코드를 작성하면, Spring에서 자동으로 LettuceConnectionFactory를 생성하고 이를 RedisConnectionFactory로 사용할 수 있다.
  • RedisTemplate를 설정하여, Spring에서 Redis를 쉽게 사용할 수 있다.
  • ObjectMapper에 JavaTimeModule()을 등록하여, Java 8 LocalDateTime의 직렬화, 역직렬화 오류를 해결할 수 있다.

 

회원 정보에 Caching 적용 예시

@Slf4j
@Repository
@RequiredArgsConstructor
public class MemberCacheRepository {

    private final static Duration MEMBER_CACHE_TTL = Duration.ofDays(1);
    private final ObjectMapper objectMapper;
    private final RedisTemplate<String, Object> redisTemplate;

    public void setMember(Member member) {
        String key = getKey(member.getUsername());
        try {
            log.info("[Cache] Set {}, {}", key, objectMapper.writeValueAsString(member));
        } catch (JsonProcessingException e) {
        }
        redisTemplate.opsForValue().set(key, member, MEMBER_CACHE_TTL);
    }

    public Optional<Member> getMember(String username) {
        String key = getKey(username);
        Member findMember = null;
        try {
            findMember = objectMapper.convertValue(redisTemplate.opsForValue().get(key), Member.class);
            log.info("[Cache] Get {}, {}", key, objectMapper.writeValueAsString(findMember));
        } catch (JsonProcessingException e) {
        }
        return Optional.ofNullable(findMember);
    }

    private String getKey(String username) {
        return "Member:" + username;
    }
}
  • MemberCacheRepository를 생성하여, RedisTemplate을 사용해 Key=Member:{username}, Value=Member로 Redis에 저장하고, Redis에서 가져온다.
  • Redis의 공간 활용과 데이터 최신화를 위해 데이터는 계속해서 가지고 있는 것이 아니라 데이터의 I/O를 최대한 줄여 줄 수 있는 ExpiredTimed인 MEMBER_CACHE_TTL을 설정해서 일정 시간이 지나면 제거되도록 설정하는 것이 좋다.

 

@Service
@RequiredArgsConstructor
public class JwtUserDetailsService implements UserDetailsService {

    private final MemberService memberService;
    private final MemberCacheRepository memberCacheRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member findMember = memberCacheRepository.getMember(username).orElseGet(() -> memberService.findByUsername(username));
        return new User(findMember.getUsername(), findMember.getPassword(), List.of(new SimpleGrantedAuthority(findMember.getRole().toString())));
    }
}
  • 매 API 요청마다, 요청자의 인증 정보를 확인하는 loadUserByUsername 메서드에서 캐시를 적용해, Redis에 해당하는 데이터가 존재하면, 해당 Member 데이터를 가져오고 존재하지 않으면, DB에서 Member 데이터를 가져오도록 코드를 작성하였다.

 

public String login(String username, String password) {
    Member member = memberService.findByUsername(username);

    if (!passwordEncoder.matches(password, member.getPassword())) {
        throw new SnsApplicationException(ResponseCode.VALIDATION_ERROR, "Password is invalid.");
    }

    memberCacheRepository.setMember(member);

    return jwtTokenProvider.generateToken(username);
}
  • Redis에 Member 정보를 저장하는 시기는 회원이 Login하는 시점으로 작성하였다.

 

결과

로그인 시도 시, 다음과 같은 로그를 확인할 수 있다.

[Cache] Set Member:test4@test.com, {"createdDate":"2022-11-20T01:45:49.519546","lastModifiedDate":"2022-11-20T01:45:49.519546","id":4,"username":"test4@test.com","password":"$2a$10$TlEYkasPMSFP7q0DqLK13ummr6mBXKvtRYIIdUVyBA.NgZMCz1Hfm","role":"ROLE_USER","posts":[]}

 

다른 API를 호출 시, 다음과 같은 로그와 Redis 저장 상태를 확인할 수 있다.

[Cache] Get Member:test4@test.com, {"createdDate":"2022-11-20T01:45:49.519546","lastModifiedDate":"2022-11-20T01:45:49.519546","id":4,"username":"test4@test.com","password":"$2a$10$TlEYkasPMSFP7q0DqLK13ummr6mBXKvtRYIIdUVyBA.NgZMCz1Hfm","role":"ROLE_USER","posts":[]}

 

'Redis' 카테고리의 다른 글

[Redis] Reids 내용 정리  (0) 2022.11.08
[DB] Redis란?  (0) 2022.11.07
Cache란?  (0) 2022.11.06