본문으로 바로가기
반응형

개요

캐시는 상당히 많은 부분에서 사용되는데, 그 중 하나를 뽑아서 이야기해보자면 웹 사이트의 메인 페이지가 좋은 예가 아닐까싶다. 요구사항마다 다르겠지만 일반적으로 메인 페이지에는 상당히 많은 데이터를 뽑아서 보여주는 경우가 많다. 그렇다면 매 요청마다 DB를 조회하여 데이터를 가져와야 하는데 이는 성능적인 면에서 좋지 않은 결과를 나타낼 수 있다. 따라서 실시간성이 요구되는 데이터가 아닌 경우 특정 시간동안 캐싱 처리하여 DB를 조회하는것이 아닌 Redis등에 저장된 데이터를 가져와서 보여준다.

스프링에서는 @Cacheable, @CachePut, @CacheEvict 등의 어노테이션을 통해 이를 손쉽게 처리할 수 있다. 본 예제에서는 설정부터 실제 적용하는 방법에 대해 자세히 알아본다.

설정

@EnableJpaAuditing
@EnableCaching
@SpringBootApplication
public class SlidoCloneApplication {

	public static void main(String[] args) {
		SpringApplication.run(SlidoCloneApplication.class, args);
	}

}

메인쪽에 @EnableCaching 어노테이션을 붙여줌으로써 캐시 사용 설정을 한다.

spring:
  redis:
    host: localhost
    port: 6379

application.yml에 사용할 redis의 host, port를 적어준다.

@Configuration
@EnableCaching
public class RedisConfig {
    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build();
    }
}

RedisConfig.class 파일을 하나 생성하고 cacheManager를 만들어준다. 우리는 JPA Entity를 Redis에 serialize해서 넣어두고 나중에 불러온다음 deserialize하여 사용할텐데 이 부분에 대한 설정을 해줘야한다. 위 부분 설정이 없다면

Failed to serialize object using DefaultSerializer~DefaultSerializer requires a Serializable payload but received an~

라는 오류가 발생한다. 이 부분에 대해 조금 더 찾아보니, @EnableCaching 어노테이션이 등록되어있는 경우 자동으로 설정을 할 시점에 RedisConnectionFactory를 생성한다고 한다. 따라서 우리가 위에서 생성한 cacheManagerRedisConnectionFactory를 주입받아서 키는 StringRedisSerializer를, 값은 GenericJackson2JsonRedisSerializer를 통해 serialize, deserialize를 진행하게 된다. 

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public class BaseTimestampEntity {
    @CreatedDate
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime createdAt;

    @LastModifiedBy
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime updatedAt;
}

추가적으로 만약 캐시로 사용할 객체에 LocalDateTime 타입의 값이 존재한다면 위처럼 @JsonSerialize, @JsonDeserialize 어노테이션을 기입해줘야 한다. 그렇지 않으면

Could not read JSON: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): cannot deserialize from Object value~

오류가 발생한다. (참고로 나는 대다수의 Entity에 공통적으로 created_at, updated_at을 넣어주기 위해 위처럼 MappedSuperclass를 사용하고 있다)

@Cacheable

@Cacheable이 붙어있는 메소드는 먼저 캐시를 조회하고 없다면 저장한다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class RoomQueryService {
    private final RoomRepository roomRepository;

    @Cacheable(value = "search", key = "#code", cacheManager = "cacheManager")
    public Room searchRoomByCode(String code) {
        return roomRepository.findByCode(code).orElseThrow(RoomNotFoundException::new);
    }
}

value에는 키값의 prefix를, key에는 구분값을 넣어준다. key는 SPEL문법을 따른다. 위처럼 코드를 작성하고 실행시킨이후 레디스를 조회해보면

127.0.0.1:6379> keys *
1) "search::code"

형태로 값이 저장되게 된다. 값을 확인해보면

"{\"@class\":\"slido.slidoclone.room.domain.Room\",\"createdAt\":[2021,10,29,4,58,9],\"updatedAt\":[2021,10,29,4,58,9],\"id\":1,\"password\":\"hide\",\"code\":\"code\"}"

이런 형태로 직렬화하여 삽입된다.

@CacheEvict

캐시를 삭제하고 싶다면 @CacheEvict를 사용한다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class RoomQueryService {
    private final RoomRepository roomRepository;

    @CacheEvict(value = "search", key = "#code", cacheManager = "cacheManager")
    public void deleteRoomByCode(String code) {
        roomRepository.deleteByCode(code);
    }
}

캐시 삭제의 경우 allEntries라는 인자가 추가적으로 존재하는데, 만약 search::1, search::2..와 같이 search라는 value를 가진 값이 여러개 존재할 경우 allEntries = true값을 주면 한번에 해당 캐시의 모든 값을 삭제시킬 수 있다.

@CachePut

@Cacheable은 캐시가 있다면 사용하고 없다면 저장한다고 했다. 반면에 @CachePut은 단순히 캐시를 저장하는 기능만 한다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class RoomQueryService {
    private final RoomRepository roomRepository;

    @CachePut(value = "search", key = "#code", cacheManager = "cacheManager")
    public void createRoomByCode(String code) {
    }
}

Redis를 확인해보면 @Cacheable과 동일하게 정상적으로 저장된 모습을 확인할 수 있다.

127.0.0.1:6379> keys *
1) "search::code"
127.0.0.1:6379> get search::code
"\xac\xed\x00\x05sr\x00+org.springframework.cache.support.NullValue\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00xp"
반응형