이번 글에서는 Redis를 이용하여 게시판 검색 API를 구현하는 방법에 대해 설명합니다. Redis는 고성능의 인메모리 데이터 저장소로, 데이터 조회 속도가 매우 빠르기 때문에 대규모 트래픽을 처리하는 시스템에 적합합니다. Redis를 캐시로 사용하면 데이터베이스 부하를 줄이고 응답 속도를 크게 향상시킬 수 있습니다.
1. Redis와 캐시 개념
1.1 Redis란?
Redis(Remote Dictionary Server)는 인메모리 데이터 저장소로, 주로 캐시(Cache) 또는 빠른 데이터 저장을 위해 사용됩니다. Redis는 NoSQL 데이터베이스의 일종으로, 메모리에서 데이터를 관리하기 때문에 읽기와 쓰기 속도가 매우 빠릅니다.
1.2 Redis의 장점
- 빠른 응답 속도: 모든 데이터가 메모리에 저장되므로, 디스크를 사용하는 일반적인 데이터베이스에 비해 데이터 접근 속도가 빠릅니다.
- 다양한 데이터 구조: 문자열(String), 해시(Hash), 리스트(List), 집합(Set), 정렬된 집합(Sorted Set) 등 다양한 자료구조를 지원합니다.
- 캐시 활용: 캐시로 활용하면, 자주 조회되는 데이터를 Redis에 저장해 데이터베이스의 부하를 줄이고, 조회 성능을 향상시킬 수 있습니다.
2. Redis 캐시 설정
게시판 검색 API에서 Redis를 이용해 검색 결과를 캐시함으로써 데이터베이스 조회 성능을 개선할 수 있습니다.
2.1 Redis Docker Compose 설정
Redis 서버를 구성하기 위해 Docker Compose 파일을 사용합니다.
version: '3.22.1'
services:
board-cache:
image: redis:latest
container_name: cache_container
ports:
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/root/redis
- ./redis.conf:/usr/local/etc/redis/redis.conf
environment:
- REDIS_DATABASES=${REDIS_DATABASES}
restart: always
volumes:
redis_data:
driver: local
2.2 Redis 설정 파일 (redis.conf
)
bind 0.0.0.0
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble no
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes
redis.conf
파일은 Redis 서버의 동작 방식을 결정하는 매우 중요한 설정 파일입니다. Redis는 이 파일을 통해 메모리 관리, 네트워크 설정, 데이터 저장 방식 등을 제어할 수 있습니다. 이 설정 파일을 수정하여 Redis 서버의 성능과 안정성을 향상시킬 수 있습니다.
아래는 redis.conf
파일에 포함된 주요 설정 항목과 그에 대한 설명입니다.
1. Network Configuration (네트워크 설정)
- bind 0.0.0.0
- Redis 서버가 수신할 IP 주소를 지정합니다.
0.0.0.0
은 모든 네트워크 인터페이스에서 Redis가 요청을 수신하도록 허용합니다. - 보안 주의: Redis는 기본적으로 비밀번호 없이 접근할 수 있습니다. 외부 접근이 가능한 서버에서는 주의해야 하며, 보안상의 이유로 내부 IP만 허용하거나 방화벽을 설정하는 것이 좋습니다.
- Redis 서버가 수신할 IP 주소를 지정합니다.
protected-mode yes
- Redis가 외부에서 접근 가능하도록 설정되어 있을 때 추가적인 보호 기능을 활성화하는 옵션입니다.
yes
로 설정되어 있으면, 비밀번호가 설정되지 않았을 때 외부 접근을 차단하여 서버를 보호합니다.
port 6379
- Redis가 수신할 포트를 설정합니다. 기본 포트는
6379
입니다.
- Redis가 수신할 포트를 설정합니다. 기본 포트는
2. Persistence (데이터 지속성)
save 900 1
- Redis 서버가 900초(15분) 동안 1회 이상의 변경이 발생하면 스냅샷을 저장합니다. 이 설정은 Redis의 데이터가 메모리에만 저장되지 않고, 디스크에 저장되어 재부팅 시에도 데이터를 복구할 수 있도록 합니다.
save 300 10
- 300초(5분) 동안 10회 이상의 변경이 발생하면 스냅샷을 저장합니다.
save 60 10000
- 60초(1분) 동안 10000회 이상의 변경이 발생하면 스냅샷을 저장합니다.
이러한 설정은 Redis의 RDB(Redis Database) 스냅샷 기능을 통해 데이터의 지속성을 보장합니다. Redis는 메모리 기반이지만, save
옵션을 통해 메모리의 내용을 주기적으로 디스크에 저장할 수 있습니다.
stop-writes-on-bgsave-error yes
- 스냅샷 저장 중 오류가 발생했을 경우 Redis가 더 이상 데이터를 저장하지 않도록 설정하는 옵션입니다.
- 데이터 손실을 방지하기 위한 안전 장치로,
yes
로 설정하는 것이 일반적입니다.
rdbcompression yes
- RDB 파일을 저장할 때 데이터 압축 여부를 설정합니다.
yes
로 설정하면 저장 공간을 절약할 수 있지만, 압축 및 압축 해제 작업에 CPU 리소스가 소모됩니다.
- RDB 파일을 저장할 때 데이터 압축 여부를 설정합니다.
dbfilename dump.rdb
- RDB 스냅샷 파일의 이름을 지정합니다. 기본적으로
dump.rdb
로 저장되며, Redis 서버가 종료되거나 재시작할 때 이 파일을 사용하여 데이터를 복구합니다.
- RDB 스냅샷 파일의 이름을 지정합니다. 기본적으로
3. AOF (Append-Only File) 설정
Redis는 AOF(Append-Only File)와 RDB(Redis Database)라는 두 가지 방식으로 데이터를 영구 저장할 수 있습니다.
appendonly no
- AOF를 사용하지 않도록 설정합니다. AOF는 모든 쓰기 작업을 파일에 기록하여, 서버 장애 시 데이터를 복구할 수 있게 합니다.
yes
로 설정하면 AOF가 활성화되며, 모든 데이터 변경 사항을 파일에 기록합니다.
appendfilename "appendonly.aof"
- AOF 파일의 이름을 지정합니다. 기본 파일명은
appendonly.aof
입니다.
- AOF 파일의 이름을 지정합니다. 기본 파일명은
appendfsync everysec
- AOF 파일 동기화 빈도를 설정합니다.
always
: 모든 쓰기 작업마다 AOF 파일에 동기화. 데이터의 안전성은 높지만 성능이 저하될 수 있습니다.everysec
: 1초마다 동기화. 데이터 손실 가능성을 최소화하면서 성능을 어느 정도 유지할 수 있는 설정입니다.no
: 동기화를 하지 않음. 성능은 좋지만 데이터 손실 위험이 큽니다.
- AOF 파일 동기화 빈도를 설정합니다.
auto-aof-rewrite-percentage 100
- AOF 파일 크기가 처음 시작했을 때의 크기보다 100% 커지면 자동으로 리라이트(재작성)를 수행합니다. 리라이트는 AOF 파일을 최적화하여 크기를 줄이는 작업입니다.
auto-aof-rewrite-min-size 64mb
- 리라이트가 발생하는 최소 AOF 파일 크기를 설정합니다. 여기서는 AOF 파일이 64MB 이상일 때만 리라이트가 실행됩니다.
4. Memory Management (메모리 관리)
maxmemory-policy noeviction
- Redis가 메모리 부족 상태일 때 데이터를 어떻게 처리할지 결정합니다.
noeviction
: 더 이상 새로운 데이터를 추가하지 않음.allkeys-lru
: 자주 사용되지 않은 키를 제거하여 메모리를 확보(LRU: Least Recently Used).volatile-lru
: 만료 시간(TTL)이 설정된 키들 중 자주 사용되지 않은 것을 제거.allkeys-random
: 임의의 키를 제거.volatile-random
: TTL이 있는 키들 중 임의의 키를 제거.
- Redis가 메모리 부족 상태일 때 데이터를 어떻게 처리할지 결정합니다.
이 설정을 통해 메모리 관리 전략을 결정할 수 있으며, 시스템 메모리 제약에 맞게 조정하는 것이 중요합니다.
lazyfree-lazy-eviction no
- 객체를 삭제할 때 즉시 메모리에서 제거할지 또는 나중에 제거할지 설정합니다.
no
로 설정하면 즉시 삭제하고,yes
로 설정하면 비동기로 삭제하여 성능을 개선할 수 있습니다.
- 객체를 삭제할 때 즉시 메모리에서 제거할지 또는 나중에 제거할지 설정합니다.
5. Replication (복제 설정)
slave-serve-stale-data yes
- 마스터와의 연결이 끊어졌을 때 슬레이브가 데이터를 계속해서 서비스할지 여부를 결정합니다.
yes
로 설정하면 연결이 끊어졌더라도 읽기 요청에 응답할 수 있습니다.
- 마스터와의 연결이 끊어졌을 때 슬레이브가 데이터를 계속해서 서비스할지 여부를 결정합니다.
repl-diskless-sync no
- 디스크 없이 복제를 수행할지 여부를 설정합니다. 기본적으로는
no
로 설정되어 있으며, 디스크를 사용하여 복제를 수행합니다.
- 디스크 없이 복제를 수행할지 여부를 설정합니다. 기본적으로는
slave-read-only yes
- 슬레이브 노드가 읽기 전용으로 동작할지 설정합니다. 슬레이브에 데이터를 쓰는 작업을 방지하려면
yes
로 설정합니다.
- 슬레이브 노드가 읽기 전용으로 동작할지 설정합니다. 슬레이브에 데이터를 쓰는 작업을 방지하려면
6. Log and Debugging (로그 및 디버깅)
loglevel notice
- Redis 서버의 로그 레벨을 설정합니다. 가능한 값은
debug
,verbose
,notice
,warning
등이 있으며,notice
는 권장 설정입니다.
- Redis 서버의 로그 레벨을 설정합니다. 가능한 값은
logfile ""
- Redis 로그 파일의 경로를 설정합니다. 빈 문자열로 설정하면 표준 출력으로 로그를 출력합니다.
slowlog-log-slower-than 10000
- Redis에서 느리게 실행되는 명령어의 실행 시간을 밀리초 단위로 설정합니다. 10000으로 설정하면 10ms 이상 걸리는 명령어가 기록됩니다.
slowlog-max-len 128
- Redis에서 저장할 느린 명령어 로그의 최대 길이를 설정합니다.
7. 기타 성능 관련 설정
tcp-backlog 511
- 커널에서 수신 대기 중인 연결의 최대 대기열 길이를 설정합니다.
tcp-keepalive 300
- Redis 서버가 TCP 연결을 유지하는 시간을 설정합니다. 기본값은 300초입니다.
hz 10
- Redis의 내부 작업을 처리하는 주기를 설정합니다. 기본값은 10Hz로, 초당 10번의 작업을 처리합니다.
결론
redis.conf
파일은 Redis 서버의 전반적인 동작을 제어하는 핵심 설정 파일입니다. 네트워크 설정, 데이터 지속성, 메모리 관리, 복제 설정 등 다양한 항목을 통해 Redis 서버의 성능을 최적화할 수 있습니다. 특히 캐시로 Redis를 사용할 때는 적절한 TTL(Time to Live) 설정, 메모리 관리 전략 등을 통해 효율적인 캐시 관리가 가능하도록 해야 합니다.
2.3 Spring Boot Redis 설정
Spring Boot에서 Redis를 이용하기 위해, application.yml
또는 application.properties
에 Redis 서버 정보를 설정합니다.
spring:
data:
redis:
host: localhost
port: 6379
2.4 RedisConfig 클래스
RedisConfig
클래스는 Spring Boot 애플리케이션에서 Redis를 사용하기 위한 설정을 정의하는 클래스입니다. Redis 서버와의 연결을 설정하고, 캐시를 관리하며, 데이터를 직렬화 및 역직렬화하는 방법을 지정합니다. RedisConfig
를 통해 Redis 서버와 Spring 애플리케이션 간의 통신을 효율적으로 설정할 수 있습니다.
이 클래스에서는 LettuceConnectionFactory를 사용해 Redis에 연결하고, RedisCacheManager를 통해 캐시를 관리합니다. 또한 ObjectMapper를 사용하여 Redis에 저장할 데이터를 JSON 형식으로 직렬화/역직렬화합니다.
package com.example.boardserver.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.factory.annotation.Value;
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;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Value("${spring.data.redis.password}")
private String redisPwd;
@Value("${expire.defaultTime}")
private Long defaultTime;
/**
* ObjectMapper는 Redis에 저장할 객체를 JSON 형식으로 직렬화 및 역직렬화하기 위한 설정을 정의합니다.
* Java 8 날짜/시간 API와 같은 새로운 포맷을 지원하고, 타임스탬프 대신 ISO-8601 형식을 사용합니다.
*/
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule()); // Java 8 날짜/시간 지원
return objectMapper;
}
/**
* RedisConnectionFactory는 Redis 서버와 연결을 설정하는 역할을 합니다.
* 여기서는 LettuceConnectionFactory를 사용하여 Redis와의 연결을 관리합니다.
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost); // Redis 서버 호스트 주소
config.setPort(redisPort); // Redis 서버 포트 번호
config.setPassword(redisPwd); // Redis 비밀번호 설정
return new LettuceConnectionFactory(config); // Lettuce를 사용하여 Redis 연결 생성
}
/**
* RedisCacheManager는 Redis 캐시를 관리하는 역할을 합니다.
* 캐시 키와 값을 각각 String과 JSON 형식으로 직렬화하며, 기본 TTL(Time To Live)을 설정합니다.
*/
@Bean
public RedisCacheManager redisCacheManager(
RedisConnectionFactory redisConnectionFactory,
ObjectMapper objectMapper) {
// 캐시 설정을 정의: null 값을 캐싱하지 않으며, 기본 TTL 설정
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues() // null 값은 캐시하지 않음
.entryTtl(Duration.ofSeconds(defaultTime)) // 캐시의 기본 TTL 설정
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 캐시 키를 String 형식으로 직렬화
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper))); // 캐시 값을 JSON으로 직렬화
// RedisCacheManager를 통해 Redis 캐시를 관리
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
}
RedisConfig 클래스 설명
RedisConfig
클래스는 Spring Boot 애플리케이션에서 Redis와의 연결과 캐시 관리를 설정하는 주요 클래스입니다. 이 클래스는 Redis를 활용한 캐시를 최적화하고, 데이터 직렬화 및 TTL(캐시 만료 시간) 설정을 관리합니다.
1. RedisConnectionFactory 설정
Redis와의 연결을 설정하기 위해 LettuceConnectionFactory를 사용합니다. Lettuce는 비동기 이벤트 기반 Redis 클라이언트로, Redis 서버와의 빠른 통신과 확장성을 지원합니다. 이 구성에서는 Standalone 모드로 Redis에 연결하며, redisHost
, redisPort
, redisPwd
등의 값을 외부 설정 파일에서 불러옵니다.
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost);
config.setPort(redisPort);
config.setPassword(redisPwd);
return new LettuceConnectionFactory(config);
}
2. ObjectMapper 설정
Redis에 저장되는 데이터는 기본적으로 JSON 형식으로 직렬화됩니다. 이를 위해 ObjectMapper를 사용하여 Redis의 객체 데이터를 JSON으로 직렬화/역직렬화할 수 있도록 설정합니다. 특히 Java 8의 날짜/시간 API(JavaTimeModule)를 지원하도록 추가 설정합니다.
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // ISO-8601 날짜 형식 사용
objectMapper.registerModule(new JavaTimeModule()); // Java 8 날짜/시간 지원
return objectMapper;
}
3. RedisCacheManager 설정
RedisCacheManager는 Redis의 캐시 데이터를 관리하는 클래스입니다. 캐시 TTL(Time to Live) 설정과 데이터 직렬화 방식을 정의하며, 키는 String 형식으로, 값은 JSON 형식으로 저장됩니다.
- TTL 설정:
entryTtl(Duration.ofSeconds(defaultTime))
를 통해 캐시 만료 시간을 설정합니다. 이 값은 외부 설정 파일에서 불러오며, 캐시 데이터는 설정된 시간이 지나면 만료됩니다. - 직렬화 설정: 키는
StringRedisSerializer
로 직렬화되고, 값은GenericJackson2JsonRedisSerializer
를 사용하여 JSON 형식으로 직렬화됩니다.
@Bean
public RedisCacheManager redisCacheManager(
RedisConnectionFactory redisConnectionFactory,
ObjectMapper objectMapper) {
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofSeconds(defaultTime)) // TTL 설정
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 키 직렬화
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper))); // 값 직렬화
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
4. 주요 설정 요약
- TTL 설정: 캐시 만료 시간을 통해 Redis 캐시에 저장된 데이터의 유효 기간을 설정합니다. 이 설정은 성능 최적화와 메모리 관리를 위해 매우 중요합니다.
- 데이터 직렬화: 캐시 데이터를 JSON 형식으로 직렬화하여 저장하고, 필요 시 이를 역직렬화합니다. 이를 통해 Redis에서 데이터를 더 직관적이고, 효율적으로 관리할 수 있습니다.
- LettuceConnectionFactory: 비동기 Redis 클라이언트를 사용하여 Redis 서버와 연결을 유지하며, 성능을 최적화합니다.
결론
RedisConfig
클래스는 Redis와의 통신을 위한 기본 설정을 제공합니다. 이 설정을 통해 Lettuce 클라이언트를 사용하여 Redis에 연결하고, RedisCacheManager를 통해 캐시 데이터를 관리하며, ObjectMapper를 활용해 데이터를 JSON 형식으로 직렬화합니다. Redis를 이용해 캐시를 효율적으로 관리함으로써, 애플리케이션 성능을 크게 향상시킬 수 있습니다.
3. 검색 API 구현
3.1 PostSearchController
검색 API는 PostSearchController
를 통해 정의되며, 사용자가 게시글 검색 또는 태그로 검색할 수 있도록 구현됩니다.
@Log4j2
@RestController
@RequestMapping("/search")
@RequiredArgsConstructor
public class PostSearchController {
private final PostSearchService postSearchService;
@PostMapping
public ResponseEntity<CommonResponse<Map<String, Object>>> searchPosts(
@RequestBody PostSearchRequest postSearchRequest,
@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "size", defaultValue = "10") int size
) {
Map<String, Object> result = postSearchService.searchPosts(postSearchRequest, page, size);
return new ResponseEntity<>(new CommonResponse<>(HttpStatus.OK, "SEARCH_SUCCESS", "검색 성공", result), HttpStatus.OK);
}
@GetMapping
public ResponseEntity<CommonResponse<Map<String, Object>>> searchPostsByTag(
@RequestParam @NotBlank String tagName,
@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "size", defaultValue = "10") int size
) {
Map<String, Object> result = postSearchService.searchPostsByTag(tagName, page, size);
return new ResponseEntity<>(new CommonResponse<>(HttpStatus.OK, "SEARCH_BY_TAG_SUCCESS", "태그 검색 성공", result), HttpStatus.OK);
}
}
3.2 PostSearchService
PostSearchService
는 게시판에서 게시글 검색과 태그 기반 검색 기능을 제공하는 서비스 계층입니다. 이 서비스는 MyBatis와 Redis를 사용하여 데이터베이스와 캐시를 연동하고, 검색 결과를 캐시 처리함으로써 성능을 최적화합니다.
@Cacheable 설명
Spring Framework에서 제공하는 @Cacheable
어노테이션은 캐싱을 구현하기 위한 강력한 도구입니다. @Cacheable
을 사용하면 메서드 호출 시 캐시에 데이터를 저장하고, 동일한 입력으로 메서드가 다시 호출되면 저장된 캐시 데이터를 반환함으로써 불필요한 메서드 실행과 데이터베이스 호출을 방지할 수 있습니다.
캐싱을 통한 주요 이점은 성능 향상과 데이터베이스 부하 감소입니다. @Cacheable
은 메모리나 Redis와 같은 외부 캐시 저장소에 데이터를 저장하고, 이후 동일한 요청이 들어오면 캐시에서 데이터를 바로 반환하는 방식으로 동작합니다.
@Service
@RequiredArgsConstructor
public class PostSearchServiceImpl implements PostSearchService {
private final PostSearchMapper postSearchMapper;
@Override
@Cacheable(value = "searchPosts", key = "'searchPosts_' + #postSearchRequest.name + '_' + #page + '_' + #size", unless = "#result.isEmpty()")
public Map<String, Object> searchPosts(PostSearchRequest postSearchRequest, int page, int size) {
List<PostDTO> posts = postSearchMapper.selectPosts(postSearchRequest, (page - 1) * size, size);
int totalCount = postSearchMapper.countTotalPosts(postSearchRequest);
return createResult(posts, page, size, totalCount);
}
@Override
@Cacheable(value = "searchPostsByTag", key = "'searchPostsByTag_' + #tagName + '_' + #page + '_' + #size", unless = "#result.isEmpty()")
public Map<String, Object> searchPostsByTag(String tagName, int page, int size) {
List<PostDTO> posts = postSearchMapper.selectPostsByTag(tagName, (page - 1) * size, size);
int totalCount = postSearchMapper.countTotalPostsByTag(tagName);
return createResult(posts, page, size, totalCount);
}
private Map<String, Object> createResult(List<PostDTO> posts, int page, int size, int totalCount) {
Map<String, Object> result = new HashMap<>();
result.put("posts", posts);
result.put("currentPage", page);
result.put("totalPages", (int) Math.ceil((double) totalCount / size));
result.put("totalCount", totalCount);
return result;
}
}
@Cacheable 설명
Spring Framework에서 제공하는 @Cacheable
어노테이션은 캐싱을 구현하기 위한 강력한 도구입니다. @Cacheable
을 사용하면 메서드 호출 시 캐시에 데이터를 저장하고, 동일한 입력으로 메서드가 다시 호출되면 저장된 캐시 데이터를 반환함으로써 불필요한 메서드 실행과 데이터베이스 호출을 방지할 수 있습니다.
캐싱을 통한 주요 이점은 성능 향상과 데이터베이스 부하 감소입니다. @Cacheable
은 메모리나 Redis와 같은 외부 캐시 저장소에 데이터를 저장하고, 이후 동일한 요청이 들어오면 캐시에서 데이터를 바로 반환하는 방식으로 동작합니다.
@Async
@Override
@Cacheable(value = "getProducts", key = "#root.methodName + '_' + #postSearchRequest.categoryId + '_' + #postSearchRequest.name + '_' + #page + '_' + #size", unless = "#result.isEmpty()")
public List<PostDTO> getPosts(PostSearchRequest postSearchRequest, int page, int size) {
int offset = calculateOffset(page, size);
try {
log.info("Searching posts with request: {}, page: {}, size: {}", postSearchRequest, page, size);
List<PostDTO> postDTOList = postSearchMapper.selectPosts(postSearchRequest, offset, size);
log.info("Found {} posts", postDTOList.size());
return postDTOList;
} catch (Exception e) {
log.error("Failed to select posts", e);
throw new RuntimeException("게시글 검색 중 오류가 발생했습니다.", e);
}
}
@Cacheable 동작 원리
@Cacheable
어노테이션이 적용된 메서드는 다음과 같은 동작 과정을 거칩니다:
- 캐시 조회: 메서드가 호출될 때 먼저 캐시에 동일한 키가 있는지 확인합니다.
- 캐시 히트: 동일한 키가 존재하면, 메서드 실행을 생략하고 캐시에 저장된 값을 반환합니다.
- 캐시 미스: 캐시에 해당 키가 없을 경우, 메서드를 정상적으로 실행하여 결과를 반환한 후, 결과를 캐시에 저장합니다.
- 캐시 저장: 메서드 결과가 캐시에 저장되며, 다음에 동일한 요청이 들어올 때 캐시에서 값을 반환합니다.
@Cacheable 속성 설명
- value: 캐시의 이름을 지정합니다. 여기서는
getProducts
라는 캐시를 사용하여 캐시가 저장될 저장소를 나타냅니다. Redis 또는 메모리와 같은 캐시 스토어에 저장됩니다. @Cacheable(value = "getProducts")
- key: 캐시에서 값을 가져오거나 저장할 때 사용할 고유한 키를 정의합니다. 메서드의 입력값이나 특정 조건을 기반으로 캐시 키를 설정할 수 있습니다. 위 코드에서는 메서드 이름, 카테고리 ID, 검색 이름, 페이지, 사이즈 등을 조합해 캐시 키를 생성합니다.
key = "#root.methodName + '_' + #postSearchRequest.categoryId + '_' + #postSearchRequest.name + '_' + #page + '_' + #size"
- unless: 캐시 조건을 지정하는 속성입니다. 특정 조건을 만족할 때 캐시하지 않도록 설정할 수 있습니다. 여기서는
#result.isEmpty()
조건을 사용하여 결과가 비어 있을 경우 캐싱하지 않도록 설정합니다. unless = "#result.isEmpty()"
@Cacheable 예시 설명
위 코드에서 @Cacheable
은 getPosts
메서드에 적용되어 있으며, 게시글 검색 결과를 캐싱합니다.
- 메서드가 호출될 때, 캐시에서 먼저 검색 결과를 확인합니다.
- 캐시가 없으면 MyBatis를 사용하여 데이터베이스에서 게시글을 조회한 후, 그 결과를 Redis 캐시에 저장합니다.
- 동일한 요청이 다시 발생하면 데이터베이스를 조회하지 않고 캐시된 데이터를 반환하여 성능을 향상시킵니다.
캐시 키 생성
캐시 키는 #root.methodName
(메서드 이름), postSearchRequest.categoryId
, postSearchRequest.name
, page
, size
값을 기반으로 생성됩니다. 이를 통해 각 검색 요청마다 고유한 캐시 키가 생성되어, 동일한 조건으로 검색 시 캐시된 결과를 빠르게 반환할 수 있습니다.
key = "#root.methodName + '_' + #postSearchRequest.categoryId + '_' + #postSearchRequest.name + '_' + #page + '_' + #size"
unless 속성
unless = "#result.isEmpty()"
는 캐시 조건을 지정하는 부분으로, 검색 결과가 비어 있을 때는 캐시에 저장하지 않도록 설정합니다. 즉, 검색 결과가 없는 경우 캐시를 무의미하게 사용하는 것을 방지합니다.
@Cacheable 장점
- 성능 향상: 동일한 검색 요청이 반복될 때, 데이터베이스를 다시 조회하지 않고 캐시에서 결과를 반환함으로써 성능을 크게 향상시킵니다.
- 부하 감소: 캐시를 통해 데이터베이스 접근을 줄여 서버 부하를 줄이고, 응답 속도를 개선합니다.
- 효율적인 리소스 사용: 자주 사용되는 데이터를 캐싱하여 시스템 자원을 효율적으로 사용할 수 있습니다.
@Cacheable
을 통해 Redis 캐시에 검색 결과를 저장함으로써, 검색 성능을 최적화하고 데이터베이스 부하를 줄이는 중요한 역할을 합니다. 특히 대규모 트래픽이 발생하는 게시판에서는 캐싱을 통해 빠른 응답성을 유지하고, 시스템 안정성을 높일 수 있습니다.
4. MyBatis와 Redis 연동
MyBatis를 이용해 검색 요청에 필요한 데이터를 데이터베이스에서 가져오고, Redis를 이용해 검색 결과를 캐시합니다.
<mapper namespace="com.example.boardserver.mapper.PostSearchMapper">
<select id="selectPosts" resultType="com.example.boardserver.dto.PostDTO">
SELECT p.id, p.name, p.contents, p.createTime, p.views, p.categoryId
FROM post p
WHERE p.name LIKE CONCAT('%', #{postSearchRequest.name}, '%')
LIMIT #{offset}, #{size}
</select>
<select id="countTotalPosts" resultType="int">
SELECT COUNT(*)
FROM post p
WHERE p.name LIKE CONCAT('%', #{postSearchRequest.name}, '%')
</select>
</mapper>
5. Redis를 활용한 검색 성능 향상
Redis는 검색 결과를 캐시하여, 반복적으로 요청되는 검색에 대해 빠르게 응답할 수 있도록 도와줍니다. 이를 통해 데이터베이스에 대한 부하를 줄이고, 서버 응답 시간을 단축할 수 있습니다. Redis 캐시를 설정할 때는 다음을 고려해야 합니다.
TTL(Time-to-Live)**: 캐시된 데이터의 유효 기간을 설정하여 오래된 데이터를 자동으로 삭제.
- 캐시 전략: 자주 조회되는 데이터를 캐싱하여 반복적인 조회 요청의 성능을 향상.
- 캐시 무효화: 데이터가 변경되었을 때 캐시를 자동으로 무효화하여, 최신 데이터를 유지.
결론
이번 글에서는 Redis를 활용한 게시판 검색 API를 구현했습니다. Redis 캐시를 사용하면 대규모 트래픽 환경에서 데이터베이스의 부하를 줄이고, 검색 성능을 대폭 향상시킬 수 있습니다. Redis 설정부터 API 구현, 그리고 MyBatis 연동까지의 과정을 통해 Redis 캐시의 장점을 최대한 활용하는 방법을 다루었습니다. 앞으로도 성능 최적화를 위한 다양한 캐싱 기법을 추가할 수 있습니다.
'프레임워크 > 자바 스프링' 카테고리의 다른 글
대규모 트래픽 게시판 구축 시리즈 #12: 성능 테스트 (5) | 2024.09.07 |
---|---|
대규모 트래픽 게시판 구축 시리즈 #11: 로깅, 예외처리 (0) | 2024.09.07 |
대규모 트래픽 게시판 구축 시리즈 #9: 게시판 API (0) | 2024.09.07 |
대규모 트래픽 게시판 구축 시리즈 #8: 카테고리 API (0) | 2024.09.07 |
대규모 트래픽 게시판 구축 시리즈 #7: Spring AOP를 활용한 인증 및 인가 (0) | 2024.09.05 |