궁금한게 많은 코린이의 Developer 노트

[SpringBoot] 캐시 서버와 Redis 본문

카테고리 없음

[SpringBoot] 캐시 서버와 Redis

lemonarr🍋 2025. 2. 10. 00:08

 

사용자 경험을 향상시키기 위해 필수적으로 알아야 하는 6가지 백엔드 성능 최적화 방법 중 캐싱 전략 구현에 대해 알아보자.

백엔드 성능 최적화 방법

  1. 데이터베이스 최적화
    • 인덱스 최적화: 자주 조회되는 컬럼에 대해 적절한 인덱스를 생성하여 조회 성능을 향상시켰습니다. 특히 복합 인덱스를 활용하여 WHERE절과 ORDER BY 절을 커버할 수 있도록 설계했습니다.
    • 쿼리 최적화: EXPLAIN을 통해 실행 계획을 분석하고, 불필요한 조인이나 서브쿼리를 제거하여 쿼리 성능을 개선했습니다.
    • N+1 문제 해결: JPA를 사용할 때 발생하는 N+1 문제를 fetch join이나 @BatchSize를 사용하여 해결했습니다.
  2. 캐싱 전략 구현
    • Spring Cache와 Redis를 활용하여 자주 조회되는 데이터를 캐싱
    • 멀티레벨 캐싱 전략을 도입하여 Local Cache와 Global Cache를 함께 사용
    • 캐시 hit ratio를 모니터링하고 캐시 정책을 지속적으로 개선
  3. 애플리케이션 레벨 최적화
    • Connection Pool 튜닝: HikariCP의 maximum pool size와 idle timeout 등을 워크로드에 맞게 조정
    • Thread Pool 설정: Tomcat thread pool 크기를 서버 자원과 부하를 고려하여 적절히 설정
    • 비동기 처리: @Async를 활용하여 시간이 오래 걸리는 작업을 비동기로 처리
  4. JVM 튜닝
    • GC 모니터링 및 튜닝: G1GC 사용 및 적절한 heap 크기 설정
    • JVM 옵션 최적화: OutOfMemoryError 방지를 위한 메모리 설정 및 모니터링
  5. 아키텍처 개선
    • 메시지 큐(Kafka/RabbitMQ)를 활용한 비동기 처리로 시스템 부하 분산
  6. 모니터링 및 프로파일링
    • Actuator, Prometheus, Grafana를 활용한 실시간 모니터링 구축
    • JProfiler나 VisualVM을 통한 병목 구간 분석 및 개선

 


 

먼저 캐싱의 개념부터 알아보자.

 

Caching

 Caching은 데이터를 일시적인 저장공간(캐시)에 저장하는 것을 의미한다. 캐시는 임시 파일과 임시 데이터들을 저장하는 메모리로 더 빠르게 관련 데이터들에 접근을 할 수 있게해준다.

 

 

캐시 메커니즘의 동작 원리

1. Look-aside Cache (Lazy Loading) 

주로 읽기 작업에 최적화된 캐시 방식입니다. 데이터 요청이 있을 때, 먼저 캐시에서 데이터를 찾고, 캐시에 없으면 원본 데이터 저장소(예: 데이터베이스)에서 데이터를 가져오는 방식입니다.

public Product getProduct(Long id) {
    // 1. 캐시에서 먼저 조회
    String cacheKey = "product:" + id;
    Product cachedProduct = cacheService.get(cacheKey);
    
    if (cachedProduct != null) {
        return cachedProduct; // 캐시 히트
    }
    
    // 2. 캐시 미스 시 DB에서 조회
    Product product = productRepository.findById(id);
    
    // 3. 조회한 데이터를 캐시에 저장
    cacheService.set(cacheKey, product, Duration.ofHours(1));
    
    return product;
}

작동 방식:
클라이언트가 데이터를 요청합니다.
캐시에서 해당 데이터를 찾습니다.
캐시 히트: 데이터가 캐시에 존재하면, 캐시에서 데이터를 반환합니다.
캐시 미스: 데이터가 캐시에 없으면, 원본 데이터 저장소에서 데이터를 가져와 캐시에 저장한 후 클라이언트에게 반환합니다.

 

장점:
읽기 성능이 향상됩니다.
캐시가 데이터 요청에 따라 동적으로 업데이트되므로, 자주 사용되는 데이터가 캐시에 저장됩니다.

 

단점:
데이터 쓰기 작업 시, 캐시와 원본 데이터 간의 정합성 문제가 발생할 수 있습니다. 데이터가 변경되었지만 캐시에 반영되지 않으면, 오래된 데이터가 반환될 수 있습니다.

 

 

2. Write-Through Cache

데이터 쓰기 작업이 발생할 때, 캐시와 원본 데이터 저장소 모두에 동시에 데이터를 기록하는 방식입니다. 이 방식은 데이터의 정합성을 보장하는 데 유리합니다.

public void updateProduct(Product product) {
    // 1. DB 업데이트
    productRepository.save(product);
    
    // 2. 캐시 동시 업데이트
    String cacheKey = "product:" + product.getId();
    cacheService.set(cacheKey, product);
}

작동 방식:
클라이언트가 데이터를 쓰기 요청합니다.
캐시에 데이터를 먼저 기록한 후, 원본 데이터 저장소에도 동일한 데이터를 기록합니다.

 

장점:
데이터의 정합성이 보장됩니다. 캐시와 원본 데이터가 항상 일치하므로, 데이터 일관성 문제가 발생하지 않습니다.
읽기 작업 시, 캐시에서 데이터를 빠르게 반환할 수 있습니다.

 

단점:
쓰기 작업이 두 번 발생하므로, 성능이 저하될 수 있습니다. 특히, 데이터 쓰기 작업이 빈번한 경우 성능에 부정적인 영향을 미칠 수 있습니다.
캐시의 크기와 성능에 따라 전체 시스템 성능이 영향을 받을 수 있습니다.

 

Summary

Look-aside Cache는 주로 읽기 작업에 최적화되어 있으며, 캐시와 원본 데이터 간의 정합성 문제가 발생할 수 있습니다.
Write-Through Cache는 데이터의 정합성을 보장하지만, 쓰기 성능이 저하될 수 있습니다.

 

**데이터 정합성 문제 : 데이터 저장 시스템에서 데이터가 정확하고 일관된 상태를 유지하는 것.

 

 

캐시를 사용하는 주요 이유(장점)

  1. 응답 시간 개선
    • DB 조회보다 메모리 접근이 훨씬 빠름
    • 실제 프로젝트에서 평균 응답 시간을 200ms → 20ms로 개선한 경험
  2. 데이터베이스 부하 감소
    • 반복적인 DB 조회를 줄여 서버 리소스 절약
    • 특히 고부하 상황에서 DB 부하를 분산시킴
  3. 배용 효율성
    • 데이터베이스 커넥션 수를 줄일 수 있음
    • 서버 자원을 효율적으로 사용 가능

캐시를 사용할 때의 단점 

사용자가 반복해서 같은 정보가 아닌 다른 정보를 입력할  때 기존에 저장해두었던 데이터가 계속 변경되어야 하기 때문에 다른 정보를 입력할 경우에는 효율성이 떨어진다는 단점이 있습니다.

 

Redis의 주요 사용 사례

 

1. 캐싱 시스템

- API 응답 데이터 캐싱이나, 세션 데이터 저장, 캐시 hit ratio 모니터링 및 캐시 정책 관리 등이 있습니다.

2. 실시간 순위 시스템

Sorted Set을 활용한 실시간 랭킹 구현이 가능하며, 게임 순위표, 실시간 차트 등 구현이 가능합니다.

3. 실시간 메시징

Pub/Sub 기능을 활용한 실시간 메시지(채팅 시스템, 실시간 알림 서비스) 전달이 가능합니다.

4. 분산 락(lock) 구현

동시성 제어를 위한 분산 락 처리 및 동시 접근 제어가 필요한 리소스 관리가 가능합니다.

 

 

캐시 전략

1. 로컬 캐시 (예: Caffeine)

로컬 캐시는 특정 애플리케이션이나 프로세스 내에서만 사용되는 캐시입니다. 일반적으로 메모리 내에 저장되며, 해당 애플리케이션이 직접 관리합니다. 로컬 캐시는 데이터 접근 속도를 높이고, 네트워크 지연을 줄이는 데 유리합니다.

 

단점: 데이터 일관성 문제 발생 가능 여러 인스턴스 간의 데이터 공유가 어려움

@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new CaffeineCacheManager()
            .builder()
            .maximumSize(10000)
            .expireAfterWrite(Duration.ofMinutes(5))
            .build();
    }
}

2. 분산 캐시 (예: Redis)

분산 캐시는 여러 서버나 노드에 걸쳐 데이터를 저장하는 캐시입니다. 이 방식은 대규모 시스템에서 데이터의 일관성을 유지하면서도 성능을 향상 시키기 위해 사용됩니다. 분산 캐시는 여러 클라이언트가 동시에 접근할 수 있도록 설계되어 있습니다.

 

단점: 복잡한 구현 및 관리 네트워크 지연이 발생할 수 있음

@EnableCaching
@Configuration
public class RedisCacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
    }
}

캐시 운영 시 고려사항

1. 캐시 정합성 관리

 

캐시와 원본 데이터 간의 일관성을 유지하는 것이 중요합니다. 데이터가 변경될 때 캐시도 적절히 업데이트되어야 하며, 이를 위해 다양한 전략이 필요합니다. 

  • TTL(Time To Live) 설정
  • 이벤트 기반 캐시 갱신
  • 캐시 무효화 전략

2. 캐시 크기 관리

  • 메모리 사용량 모니터링
  • 적절한 캐시 용량 설정
  • 캐시 교체 정책 (LRU, LFU 등)

3. 캐시 히트율 최적화

  • 캐시 적중률 모니터링
  • 캐시 워밍업 전략
  • 적절한 캐시 키 설계

 

Redis의 데이터가 영구적으로 저장되지 않는 주요 이유들

  1. In-Memory 데이터베이스 특성
    • Redis는 모든 데이터를 메모리에 저장하는 In-Memory 데이터 스토어이다
    • RAM은 휘발성 메모리로, 전원이 차단되면 데이터가 손실됨
    • 디스크 기반 DB보다 빠른 읽기/쓰기 성능을 제공하는 대신 영구성을 희생
  2. 메모리 관리 정책
    • Maxmemory 정책에 따른 데이터 Eviction
    • LRU(Least Recently Used), LFU(Least Frequently Used) 등의 알고리즘 기반 삭제
    • 메모리 부족 시 새로운 데이터를 위한 공간 확보 과정에서 기존 데이터 삭제

데이터 Eviction : 공간을 확보하기 위해 데이터를 삭제하는 방식

 

 

데이터 영구 저장을 위한 해결책

1. Redis Persistence 설정

# redis.conf 설정
# RDB 스냅샷 설정
save 900 1      # 900초 동안 1번 이상 변경 시 저장
save 300 10     # 300초 동안 10번 이상 변경 시 저장
save 60 10000   # 60초 동안 10000번 이상 변경 시 저장

# AOF 설정
appendonly yes
appendfsync everysec  # 매 초마다 디스크에 저장
  • RDB (Redis Database) 스냅샷
    • 작동 방식
      • Point-in-time 스냅샷으로 메모리의 데이터를 디스크에 저장
      • fork()를 통해 자식 프로세스를 생성하여 스냅샷 생성
      • 비동기적으로 진행되어 성능 영향 최소화
    • 장점
      • 작은 파일 크기로 빠른 백업과 복구
      • 재시작 시 빠른 로딩
      • 디스크 I/O 부하 감소
    • 단점
      • 스냅샷 사이의 데이터 유실 가능성
      • fork() 프로세스로 인한 메모리 사용량 증가
  • AOF (Append Only File)
    • 작동 방식
      • 모든 쓰기 명령을 로그 파일에 순차적으로 기록
      • 재시작 시 명령을 재실행하여 데이터 복구
      • fsync 정책에 따라 디스크 동기화 수행
    • fsync 정책
      • always: 매 명령마다 동기화 (가장 안전, 가장 느림)
      • everysec: 매 초마다 동기화 (절충안)
      • no: OS에 위임 (가장 빠름, 가장 위험)
    • 장점
      • 데이터 손실 최소화
      • 실시간 데이터 보호
      • 파일 손상 시 복구 용이
    • 단점
      • 파일 크기가 RDB보다 큼
      • 재시작 시 로딩 시간이 더 김
      • 디스크 I/O 부하 증가

2. 백업 전략 구현

@Service
@Slf4j
public class RedisBackupService {
    private final RedisTemplate<String, Object> redisTemplate;
    
    // 정기적인 백업 실행
    @Scheduled(cron = "0 0 * * * *") // 매시간 실행
    public void scheduleBackup() {
        try {
            String backupCommand = "SAVE";
            redisTemplate.execute((RedisCallback<String>) connection -> {
                connection.execute(backupCommand);
                return null;
            });
            log.info("Redis backup completed successfully");
        } catch (Exception e) {
            log.error("Redis backup failed", e);
        }
    }
}

 

 

 

 

 

https://velog.io/@gyubster_shim/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%A1%9C%EB%93%9C%EB%A7%B5-%EC%BA%90%EC%8B%B1

 

[백엔드 로드맵] 캐싱

캐싱에 대해서 알아보자. 주로 참고한 사이트는 여기이다. : Caching은 데이터를 일시적인 저장공간(캐시)에 저장하는 것을 의미한다. 캐시는 임시 파일과 임시 데이터들을 저장하는 메모리로 더

velog.io

https://velog.io/@anlee/%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EB%8C%80%EB%B9%84-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B0%8F-%EC%BA%90%EC%8B%B1

 

면접 질문 대비 - 성능 최적화 및 캐싱

질문 의도 의도 : 성능 최적화 카테고리는 데이터베이스 및 서버 성능을 최적화하는 능력을 평가합니다. 특히 캐싱, 쿼리 최적화, CDN 등은 실무에서 서버 부하를 줄이고 사용자 경험을 향상시키

velog.io