레디스(Redis)를 활용하는 방법 중 하나는 캐시 서버(Cache Server)로 활용하는 것이다.
레디스와 캐시에 대한 기본적인 내용, 스프링에서 레디스를 사용하는 기본적인 방법은 아래의 게시글을 참조하자.
이번 게시글은 스프링에서 레디스를 캐시 서버로 활용하는 방법에 대해 집중하여 작성할 것이다.
Company 도메인
이번 게시글에서 레디스 캐시를 사용하기 위해 Company 도메인 클래스들을 생성하였다.
Company, CompnayDto
Company는 JPA를 통해 H2 DB에 저장할 엔티티 클래스이다.
CompanyDto는 CompanyService 메소드들의 반환 타입으로 사용할 클래스이다. 그리고 캐시 서버에 저장할 타입으로 사용할 것이다.
- 테스트 코드 실행 후 RedisRepository를 통해 캐시 데이터를 삭제해줄 것이다. 이것을 위해 @RedisHash, @Id를 적용해주었다.
@NoArgsConstructor
@Getter
@Entity
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Long workerNumber;
@Builder
public Company(String name, Long workerNumber) {
this.name = name;
this.workerNumber = workerNumber;
}
public void changeWorkerNumber(Long number) {
this.workerNumber = number;
}
}
@RedisHash("companyDto")
@NoArgsConstructor
@Getter
public class CompanyDto {
@Id
private Long id;
private String name;
private Long workerNumber;
@Builder
public CompanyDto(Long id, String name, Long workerNumber) {
this.id = id;
this.name = name;
this.workerNumber = workerNumber;
}
public static CompanyDto from(Company company) {
return CompanyDto.builder()
.id(company.getId())
.name(company.getName())
.workerNumber(company.getWorkerNumber())
.build();
}
}
CompanyRepository, CompanyDtoRepository
두 개의 Repository를 사용할 것이다.
CompanyRepository는 Company 객체를 H2 DB에 저장, 수정, 삭제하기 위한 Repository이다.
CompanyDtoRepository는 CompanyDto 객체를 Redis에 저장, 수정, 삭제하기 위한 Repository이다.
public interface CompanyRepository extends JpaRepository<Company, Long> {
}
public interface CompanyDtoRedisRepository extends CrudRepository<CompanyDto, Long> {
}
CompanyService
CompanyService에는 세 가지 메소드가 정의되어있다.
- getCompany()는 주어진 companyId에 일치하는 데이터를 조회하여 CompanyDto 객체로 변환하여 반환한다.
- modCompany()는 주어진 companyId에 일치하는 데이터의 workerNumber를 변경하고, CompanyDto 객체로 변환하여 반환한다.
- deleteCompany()는 주어진 companyId에 일치하는 데이터를 삭제하는 메소드이다. 하지만 테스트 코드에서 캐시의 작동을 이해하기 위해 실제로 데이터를 삭제하지는 않고, 삭제되었다고 가정할 것이다.
@Slf4j
@RequiredArgsConstructor
@Service
public class CompanyService {
private final CompanyRepository companyRepository;
public CompanyDto getCompany(Long companyId) {
log.info("getCompany 실행");
Company company = companyRepository.findById(companyId)
.orElseThrow(NoSuchElementException::new);
return CompanyDto.from(company);
}
public CompanyDto modCompany(Long companyId, Long workerNumber) {
log.info("modCompany 실행");
Company company = companyRepository.findById(companyId)
.orElseThrow(NoSuchElementException::new);
company.changeWorkerNumber(workerNumber);
return CompanyDto.from(company);
}
public void deleteCompany(Long companyId) {
log.info("deleteCompany 실행: Company 데이터가 삭제되었습니다.");
}
}
Redis Cache 적용
RedisConfig
레디스 캐시를 사용하기 위해서는 캐시 기능을 활성화한다는 의미를 지닌 @EnableCaching을 적용해줘야한다.
설정 클래스에 적용해줘도 되며, 스프링 어플리케이션 실행 클래스에 적용해줘도 무방하다.
RedisConfig의 redisCacheManger()는 RedisCacheManager를 스프링 빈으로 등록한다.
redisCacheManager() 내부를 살펴보자.
- RedisCacheConfiguration
- RedisCacheConfiguration은 레디스 캐시를 사용하기 위한 기본 설정 객체이다. 이번 예제에서는 기본적인 사용 방법만 알아볼 것이므로 RedisCacheConfiguration.defaultCacheConfig()를 통해 객체를 생성했다.
- null 값 허용 여부, 캐시의 기본 유효 시간, key와 value의 직렬화 방법 등을 설정할 수 있다.
- RedisCacheManager
- RedisCacheManager는 실질적으로 레디스 캐시 기능을 수행한다.
- RedisConnectionFactory와 RedisCacheConfiguration을 의존하여 RedisCacheManager 객체를 생성할 수 있다.
@EnableCaching // 캐시 기능 활성화
@EnableRedisRepositories // RedisRepository 활성화
@PropertySource("classpath:/env.properties")
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
/**
* RedisConnectionFactory는 스프링 어플리케이션과 레디스를 연결하기 위해 사용된다.
* 커넥션의 종류로는 Jedis와 Lettuce가 있는데, Lettuce의 성능이 더 좋은 것으로 알려져있다.
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// redis 연결 정보(host, port, password)를 지정한다.
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(
host, port);
redisStandaloneConfiguration.setPassword(password);
// redis 연결 정보를 토대로 LettuceConnectionFactory 객체를 생성하여 빈으로 등록한다.
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
/**
* RedisTemplate는 RedisConnection에서 넘겨준 byte 값을 직렬화 하기 위해 사용된다.
*/
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
// 데이터를 받아 올 RedisConnection 설정
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
@Bean
public RedisCacheManager redisCacheManager() {
// RedisCache를 사용하기 위한 설정 객체
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith(CacheKeyPrefix.simple()) // 레디스 캐시의 키를 value::key 형태로 만든다.
.disableCachingNullValues() // null value에 대해서는 캐시하지 않음
.entryTtl(Duration.ofMinutes(2)) // 캐시의 기본 유효 기간
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 레디스 캐시의 키 직렬화 방법
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // 레디스 캐시의 값 직렬화 방법
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory())
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
CompanyService
레디스 캐시를 적용하기 위해 CompanyService의 코드를 수정했다.
getCompany(), modCompany(), deleteCompany()에는 각각 @Cacheable, @CachePut, @CacheEvict가 적용되어 있다.
- @Cacheable
- 애노테이션이 적용된 메소드의 반환 값이 레디스에 저장되어있지 않을 경우, 메소드를 실행하고 반환 값을 레디스에 저장하고 반환한다. 만약 메소드의 반환 값이 레디스에 저장되어 있다면, 메소드를 실행하지 않고 레디스에 저장된 값을 반환한다.
- value는 캐시이름의 역할을 하며, key는 고유한 레디스 키를 식별하기 위한 역할을 한다. 레디스 키는 value:key의 형태로 저장된다.
- @CachePut
- 애노테이션이 적용된 메소드를 무조건 실행하며 반환 값을 레디스에 저장한다. 즉 해당 메소드가 호출될 때 마다 레디스에 저장된 값이 갱신되는 것이다.
- value와 key는 @Cacheable에서와 동일하게 사용된다.
- @CacheEvict
- 애노테이션이 적용된 메소드가 호출되면 value:key에 해당하는 캐시를 찾아 삭제한다.
@Slf4j
@RequiredArgsConstructor
@Service
public class CompanyService {
private final CompanyRepository companyRepository;
@Cacheable(value = "companyDto", key = "#companyId")
public CompanyDto getCompany(Long companyId) {
log.info("getCompany 실행");
Company company = companyRepository.findById(companyId)
.orElseThrow(NoSuchElementException::new);
return CompanyDto.from(company);
}
@CachePut(value = "companyDto", key = "#companyId")
public CompanyDto modCompany(Long companyId, Long workerNumber) {
log.info("modCompany 실행");
Company company = companyRepository.findById(companyId)
.orElseThrow(NoSuchElementException::new);
company.changeWorkerNumber(workerNumber);
return CompanyDto.from(company);
}
@CacheEvict(value = "companyDto", key = "#companyId")
public void deleteCompany(Long companyId) {
log.info("deleteCompany 실행: Company 데이터가 삭제되었습니다.");
}
}
CompanyServiceTest
CompnayServiceTest에는 레디스 캐시의 동작을 보여주는 테스트 코드들이 작성되어있다.
총 3개의 테스트 코드가 작성되어있는데, 하나씩 살펴보자.
- cacheable()
- 첫 번째 getCompany()가 호출될 때는 해당 메소드의 반환 값이 캐시로 저장되어있지 않은 상황이다. 따라서 메소드가 실행되고 반환 값이 캐시로 저장된다.
- 두 번째 getCompany()가 호출될 때는 해당 메소드의 반환 값이 캐시로 저장된 상황이다. 따라서 메소드가 실행되지 않고 캐시로 저장된 값을 반환한다.
- cachePut()
- modCompany()는 해당 메소드의 반환 값이 캐시로 저장되어있던, 저장되어있지 않던 무조건 메소드를 실행하고 반환 값을 캐시로 저장한다.
- cacheEvict()
- 첫 번째 getCompany()가 호출 될 때는 해당 메소드의 반환 값이 캐시로 저장되어있지 않은 상황이다. 따라서 메소드가 실행되고 반환 값이 캐시로 저장된다.
- 두 번째 getCompany()가 호출 될 때는 해당 메소드의 반환 값이 캐시로 저장되어있는 상황이다. 따라서 메소드가 실행되지 않고 캐시로 저장된 값을 반환한다.
- deleteCompany()가 호출된다. 메소드가 실행되고 캐시는 삭제된다.
- deleteCompany()로 인해 캐시가 삭제된 상황에서 세 번째 getCompany()가 호출된다. 캐시가 삭제된 상황이므로, 메소드가 실행되고 반환 값이 캐시로 저장된다.
@Transactional
@SpringBootTest
class CompanyServiceTest {
@Autowired
CompanyService companyService;
@Autowired
CompanyRepository companyRepository;
@Autowired
CompanyDtoRedisRepository companyDtoRedisRepository;
@AfterEach
void afterEach() {
companyDtoRedisRepository.deleteAll();
}
@Test
void cacheable() {
// company 데이터 저장
Company company = Company.builder()
.name("AAA")
.workerNumber(5L)
.build();
companyRepository.save(company);
// 첫 번째 조회
CompanyDto first = companyService.getCompany(company.getId());
// companyRepository.findById 실행
// 두 번째 조회
CompanyDto second = companyService.getCompany(company.getId());
// 첫 번째 조회에서 캐시에 데이터가 저장되었음 -> companyService.getCompany()는 실행되지 않고, 캐시에 저장된 데이터를 반환
}
@Test
void cachePut() {
// company 데이터 저장
Company company = Company.builder()
.name("AAA")
.workerNumber(5L)
.build();
companyRepository.save(company);
CompanyDto first = companyService.modCompany(company.getId(), 8L);
// modCompany 실행
CompanyDto second = companyService.modCompany(company.getId(), 10L);
// modCompany 실행
}
@Test
void cacheEvict() {
// company 데이터 저장
Company company = Company.builder()
.name("AAA")
.workerNumber(5L)
.build();
companyRepository.save(company);
// 첫 번째 조회: companyService.getCompany() 실행
CompanyDto first = companyService.getCompany(company.getId());
// getCompany 실행
// 두 번째 조회: 첫 번째 조회에서 캐시에 데이터가 저장되었음 -> companyService.getCompany()는 실행되지 않고, 캐시에 저장된 데이터를 반환
CompanyDto second = companyService.getCompany(company.getId());
// 캐시 삭제
companyService.deleteCompany(company.getId());
// deleteCompany 실행: Company 데이터가 삭제되었습니다.
// 세 번째 조회: deleteCompany()를 호출하면서 캐시 데이터가 삭제되었음 -> companyService.getCompany() 다시 실행
CompanyDto third = companyService.getCompany(company.getId());
// getCompany 실행
}
}
Reference
[Spring] Redis Cache 사용하기 — 데이지의 IT이야기 (tistory.com)