→ 해당 조건들 중 2개 이상 포함되는 성격의 데이터이면 Cache 적용을 적극적으로 고려해 볼 수 있다.
→ 서비스 설계 시, 특히 백엔드의 경우 API 서비스의 기능 설계 단계부터 Cache 정책을 수립해두는게 좋다.
→ 어떤 정보를 Cache로 적용할지?, 해당 정보들을 어떤 시점에 어떤 주기로 갱신, 삭제할 지? ⇒ 캐싱 전략
spring-boot-starter-data-redis
brew install redis
redis-server
redis-cli
keys *
: 전체 키 목록 확인get [key]
: 해당 키 데이터 확인flushall
: 전체 데이터 제거build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
application.yml
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/redistest
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
cache:
type: redis
redis:
host: 127.0.0.1
port: 6379
redis 기본 포트 : 6379
package caching.redis.practice.config;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
@Configuration
@EnableCaching
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public CacheManager cacheManager() {
RedisCacheManager.RedisCacheManagerBuilder builder =
RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경
.disableCachingNullValues()
.entryTtl(Duration.ofMinutes(30L));
builder.cacheDefaults(configuration);
return builder.build();
}
}
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
: JSON 형태로 Serialization 적용.disableCachingNullValues()
: 데이터가 null일 경우 캐싱하지 않음.entryTtl(Duration.ofMinutes(30L))
: 유효기간 설정(JPA와 H2 DB로 최대한 간단하게 구현했다.)
package caching.redis.practice.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "NAME")
private String name;
}
package caching.redis.practice.dao;
import caching.redis.practice.domain.Member;
public interface RedisTestRepository {
Member save(Member member);
Member findOne(Long memberId);
void remove(Member member);
}
package caching.redis.practice.dao;
import caching.redis.practice.domain.Member;
import javax.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class RedisTestRepositoryImpl implements RedisTestRepository {
private final EntityManager em;
@Override
public Member save(Member member) {
if (member.getId() == null) {
em.persist(member);
} else {
Member findMember = em.find(Member.class, member.getId());
findMember.setName(member.getName());
}
return member;
}
@Override
public Member findOne(Long memberId) {
return em.find(Member.class, memberId);
}
@Override
public void remove(Member member) {
em.remove(member);
}
}
package caching.redis.practice.service;
import caching.redis.practice.domain.Member;
public interface RedisTestService {
void joinMember(Member member);
Member updateMember(Member member, Long memberId);
Member getMemberInfo(Long memberId);
void removeMember(Long memberId);
}
package caching.redis.practice.service;
import caching.redis.practice.dao.RedisTestRepository;
import caching.redis.practice.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class RedisTestServiceImpl implements RedisTestService {
private final RedisTestRepository redisTestRepository;
@Override
@Transactional
public void joinMember(Member member) {
redisTestRepository.save(member);
}
@Override
@CachePut(value = "Member", key = "#memberId", cacheManager = "cacheManager")
@Transactional
public Member updateMember(Member member, Long memberId) {
return redisTestRepository.save(member);
}
@Override
@Cacheable(value = "Member", key = "#memberId", cacheManager = "cacheManager", unless = "#result == null")
public Member getMemberInfo(Long memberId) {
return redisTestRepository.findOne(memberId);
}
@Override
@CacheEvict(value = "Member", key = "#memberId", cacheManager = "cacheManager")
@Transactional
public void removeMember(Long memberId) {
Member member = redisTestRepository.findOne(memberId);
redisTestRepository.remove(member);
}
}
package caching.redis.practice.controller;
import caching.redis.practice.domain.Member;
import caching.redis.practice.service.RedisTestService;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/")
@RequiredArgsConstructor
public class RedisTestController {
private final RedisTestService redisTestService;
@GetMapping("/{memberId}")
public ResponseEntity<?> getMemberInfo(@PathVariable("memberId") Long memberId) {
return ResponseEntity.ok(redisTestService.getMemberInfo(memberId));
}
@PostMapping("")
public ResponseEntity<?> joinMember(@RequestBody Map<String, String> memberInfo) {
Member member = new Member();
member.setName(memberInfo.get("name"));
redisTestService.joinMember(member);
return ResponseEntity.ok("가입 완료");
}
@PutMapping("")
public ResponseEntity<?> updateMember(@RequestBody Map<String, String> memberInfo) {
System.out.println(memberInfo);
Member member = new Member();
member.setId(Long.parseLong(memberInfo.get("id")));
member.setName(memberInfo.get("name"));
redisTestService.updateMember(member, member.getId());
return ResponseEntity.ok("수정 완료");
}
@DeleteMapping("/{memberId}")
public ResponseEntity<?> deleteMember(@PathVariable("memberId") Long memberId) {
redisTestService.removeMember(memberId);
return ResponseEntity.ok("삭제 완료");
}
}
redis-server
redis-cli
keys*
(Postman 활용)
데이터 삽입
데이터 조회 : Redis Cache 에 저장된다
→ Redis 확인
데이터 수정 : Redis Cache에 저장된 데이터 수정
→ Redis 확인
→ name 이 updatedname1 로 수정된 것을 확인할 수 있다.
→ 다시 조회 확인
→ 수정됨
→ DB로 SELECT 쿼리가 전송되지 않음도 확인
데이터 삭제 : Redis Cache에 저장된 데이터도 삭제된다.
→ Redis 확인
→ 데이터가 삭제된 것을 확인할 수 있다.
@EnableCaching
@Cacheable
@CachePut
@CacheEvict