소규모의 경우에는 이렇게 하지 않아도 될 수 있으나, 데이터가 많고 읽는 경우가 많을때 이 방식을 많이 쓴다.
메인DB로 Postgresql 을 사용하는 이유는 성능도 좋지만 무료이고, 검증되었기 때문이라고 본다.
SpringBoot 설정에서 캐시 설정을 Redis로 해 놓으면 캐시에서 조회하는것도, 캐시를 갱신하는것도 다 자동으로 해 준다.
이론적인 부분은 여기를 참조 (https://dean83.tistory.com/399). 이 중 Cache-aside 방식으로 동작한다.
이 글에서는 외부 글로벌 캐싱 기준으로 설명한다. 만일 인메모리 캐싱이 필요하다면, caffein을 추가로 알아보는것이 좋다. (ConcurrentMap 이용으로 빠름)
1. Gradle 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.postgresql:postgresql'
2. yaml 설정
- db 접속 정보는 대부분 env 파일로 따로 빼 낸 다음 불러오거나 AWS를 이용하므로 참고. (https://dean83.tistory.com/322 여기 하단부 참고)
spring:
application:
name: my-service
# =========================
# PostgreSQL (영속 DB)
# =========================
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://db.mycompany.com:5432/mydb
username: myuser
password: mypassword
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
# =========================
# Redis (Cache 전용)
# =========================
data:
redis:
host: redis.mycompany.com # 외부 Redis
port: 6379
password: redispassword # 없으면 제거
timeout: 2s
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
max-wait: 1s
# =========================
# Spring Cache
# =========================
cache:
type: redis # ★ 중요: 캐시 구현체를 Redis로 명시
3. Redis Cache Manager 커스텀 설정
@Configuration
@EnableCaching // 캐시 기능 활성화
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
ObjectMapper mapper = objectMapper.copy();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer(mapper))
).prefixCacheNameWith("이름")
.disableCachingNullValues();
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("users", config.entryTtl(Duration.ofMinutes(3)));
configs.put("channels", config);
configs.put("notifications", config);
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configs)
.build();
}
}
- 미리 cache 이름을 지정해 줄 수 있고, 이때 각 캐시별로 유효시간 등을 지정할 수 있다.
- 여기서 주의할점, 위에서 별도의 ObjectMapper 설정을 해주었는데, 이는 List 같은 제너릭 타입이 아닌, 클래스 타입은 가능하다.
- 그러나 List 타입을 사용하게 되면 Deserialize 할때 터진다. 이 경우, ObjectMapper 관련 설정은 지우고, 아래 코드도 고친다.
- 그후, 별도의 캐싱 전용 서비스 클래스를 생성하고, 직접 ObjectMapper 를 통해 List 를 Serialize 하여 String 형태로 변환하고 리턴한다.
- 기존에 사용하던 비즈니스 로직 서비스는 (캐싱을 하지 않는) 리턴받은 json 문자열을 수동으로 Deserialize 하여 컨트롤러에 리턴한다.
...
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer(mapper))
위의 코드를 아래처럼 고친다.
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer())
4. 기본 활용 예
- 기본적으로 @Cacheable, @CacheEvict, @CachePut 3개가 있다.
//엔티티
@Entity
@Table(name = "users")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
....
//리파지토리
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
...
//서비스
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Cacheable(value = "userCache", key = "#id")
public User getUser(Long id) {
// Redis에 캐시가 없을 때만 DB 조회
System.out.println("DB 조회 발생 for id=" + id);
return userRepository.findById(id).orElseThrow();
}
@CacheEvict(value = "userCache", key = "#user.id")
public User updateUser(User user) {
// DB 갱신 + 캐시 무효화
return userRepository.save(user);
}
@Cacheable(
value = "userCache",
key = "#id",
condition = "#id < 1000", // 실행 전 조건
unless = "#result.status == 'DELETED'" // 실행 후 조건
)
public User getUserWithCondition(Long id) {
System.out.println("DB 조회 발생 for id=" + id);
return userRepository.findById(id)
.orElseThrow();
}
}
...
//컨트롤러
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUser(id);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return userService.updateUser(user);
}
}
- @Cacheable의 value 값 : 캐시의 이름으로, Redis 에서 논리적으로 공간을 구분하기 위해 사용 (DB 테이블과 유사)
- SpEL 중, condition, unless
- condition : 해당 조건일때 캐시에서 찾아오는것을 시도.
- unless : 해당 조건일 때에는 캐시하지 않음.
- key는 복합키로도 사용이 가능하다
- 예 : key : "#category" + "_" + "#page" : 카테고리와 페이지가 모두 하나의 키로 의미있는 경우.
- 예 : key : "#category" + "_" + "#page" : 카테고리와 페이지가 모두 하나의 키로 의미있는 경우.
- 개발자가 따로 뭘 하지 않아도 서비스의 메서드가 호출될 때 @Cachable 로 인해 자동으로 캐시에서 내용을 찾고, 없을경우만 쿼리 조회 한다.
- 캐시 갱신 등 관리도 자동으로 한다.
- 쓰기 작업을 할때 @CacheEvict (해당 캐시 키 삭제) 나 CachePut을 반드시 해야한다. 내용이 변경되었기 때문에 무효화를 해주어야 한다.
- @CachePut (value = "userCache", key = "#user.id") 를 통해 갱신.
- @CacheEvict 사용시, allEntries = true 속성을 주면, 해당 value의 모든 캐싱을 지운다.
5. 복합 캐싱 예
- 하나의 메서드에서 여러 캐싱을 동시에 활용할때 사용한다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Caching(
cacheable = {
@Cacheable(value = "userById", key = "#id"),
@Cacheable(value = "userByEmail", key = "#result.email")
}
)
public User getUser(Long id) {
System.out.println("DB 조회 발생 id=" + id);
return userRepository.findById(id)
.orElseThrow();
}
@Caching(
evict = {
@CacheEvict(value = "userById", key = "#user.id"),
@CacheEvict(value = "userByEmail", key = "#user.email")
}
)
public User updateUser(User user) {
return userRepository.save(user);
}
@Caching(
put = {
@CachePut(value = "userById", key = "#result.id"),
@CachePut(value = "userByEmail", key = "#result.email")
}
)
public User updateAndRefresh(User user) {
return userRepository.save(user);
}
}
캐싱의 다양한 문제점을 방지하고 해결하기 위해 아래를 참고 하여 설정할 필요가 있다. (condition, unless 사용 등)
https://dean83.tistory.com/399
https://dean83.tistory.com/404
Cache
캐싱은 DB 정보를 매번 조회하지 않아도 되므로, 응답속도에도 잇점이 있고, DB 부하를 줄일 수 있는 잇점도 있다. 보통은 MemoryDB 인 Redis를 많이 사용하고, 외부 Redis를 통해 다수의 서버가 캐싱을
dean83.tistory.com
'Backend > SpringBoot' 카테고리의 다른 글
| @Controller exception 처리 (@ControllerAdvice, @RestControllerAdvice) (0) | 2025.09.15 |
|---|---|
| Controller 에서 각종 파라메터 받기 + Cookie (0) | 2025.09.12 |
| yaml 이나 env 에서 설정값 가져오기 (0) | 2025.09.03 |
| 설정(Configuration) 오버라이딩 하기 (0) | 2025.09.02 |
| 커스텀 Event 발생 -> 수신 (0) | 2025.09.02 |