[Kafka] 서킷 브레이커와 Fallback 장애 대응

궁금하면 500원·2025년 1월 15일
0

미생의 개발 이야기

목록 보기
26/58

Resilience4j분산 시스템에서 성능을 높이는 비법, 로컬 캐시를 알고 계신가요?🤔

오늘은 데이터베이스 부하를 줄이고 빠른 응답 속도를 제공하는 로컬 캐시의 필요성에 대해 소개합니다!

⚡️ 분산 시스템에서 로컬 캐시의 필요성

분산 시스템에서는 데이터베이스 호출을 줄이기 위해 로컬 캐시를 활용하는 것이 중요합니다.
이를 통해 데이터베이스의 부하를 줄이고, 빠른 데이터 접근을 가능하게 하여 서비스 성능을 크게 향상시킬 수 있습니다.

🔄 로컬 캐시와 글로벌 캐시의 차이점

로컬 캐시는 네트워크 트래픽을 줄여 빠른 응답 속도를 제공하며, 글로벌 캐시와는 다른 특성을 갖고 있습니다.
각 캐시의 특성을 이해하고, 환경에 맞는 적절한 캐시 전략을 선택하는 것이 중요합니다.

⚙️ 유연한 캐싱 시스템 구축

로컬 캐시와 Redis 캐시를 목적에 맞게 구분하여 사용하고, CacheManager를 통해 두 캐시 시스템을 통합하여 효율적으로 관리할 수 있습니다.
이렇게 하면 데이터 조회 속도를 개선하고, 서버 성능을 최적화할 수 있습니다.

고급 캐싱 전략의 실제 시나리오 확장

1. 캐시 일관성 문제를 해결하기 위한 사례

  • 문제: 분산 환경에서 데이터베이스와 캐시 간 데이터 일관성 문제가 발생.
    예: 상품의 재고 정보가 갱신되었으나, 캐시에 반영되지 않아 주문 과정에서 오류 발생.

  • 해결 전략

    • 이벤트 기반 캐시 무효화 및 동기화를 도입.
    • Redis의 Pub/Sub, Kafka 이벤트 스트리밍 활용.

코드 예시 - 이벤트 기반 캐시 무효화

@Component
class CacheInvalidationListener {
    @Autowired
    private lateinit var multiLevelCacheManager: MultiLevelCacheManager

    @KafkaListener(topics = ["product-update-topic"])
    fun onProductUpdate(event: ProductUpdateEvent) {
        // 변경된 데이터를 캐시 무효화
        multiLevelCacheManager.invalidate(event.productId)
    }
}

data class ProductUpdateEvent(
    val productId: String,
    val updatedAt: LocalDateTime
)

2. 멀티 캐시 환경에서 장애 대응 시나리오

  • 문제: Redis 노드가 과부하 상태로 장애 발생.

    • Redis 클러스터가 과부하 상태에서 로컬 캐시에 과도한 의존도가 생김.
  • 해결 전략

    • Resilience4j 기반의 서킷 브레이커 패턴 적용.
    • Redis 장애 시 Fallback 전략을 활용하여 데이터베이스 조회로 전환.

코드 예시 - 서킷 브레이커 적용

@Component
class ResilientCacheService {
    private val circuitBreaker = CircuitBreaker.ofDefaults("redis")

    @Autowired
    private lateinit var redisClient: RedissonClient

    @CircuitBreaker(name = "redis", fallbackMethod = "fallbackToDatabase")
    fun getFromCache(key: String): String? {
        val bucket = redisClient.getBucket<String>(key)
        return bucket.get()
    }

    // Redis 장애 시 데이터베이스 조회로 전환
    fun fallbackToDatabase(key: String, e: Exception): String? {
        println("Redis 장애 발생: $e")
        // 데이터베이스 조회 로직
        return "데이터베이스에서 조회된 값"
    }
}

3. 캐시 성능 최적화를 위한 메트릭 기반 개선

  • 문제: 캐시 히트율이 50% 이하로 낮아짐 → 캐시 활용이 비효율적.

  • 해결 전략

    • Micrometer로 캐시 성능 메트릭 수집.
    • 히트율 분석 후, 동적 TTL 조정 및 자주 조회되는 데이터만 캐싱.

코드 예시 - Micrometer로 캐시 모니터링

@Component
class CacheMetricsCollector {
    private val hitCounter = Counter.builder("cache_hit_count").register(Metrics.globalRegistry)
    private val missCounter = Counter.builder("cache_miss_count").register(Metrics.globalRegistry)

    fun recordHit() {
        hitCounter.increment()
    }

    fun recordMiss() {
        missCounter.increment()
    }

    fun getHitRatio(): Double {
        val total = hitCounter.count() + missCounter.count()
        return if (total > 0) hitCounter.count() / total else 0.0
    }
}

4.확장성 문제 해결: 캐시 클러스터링과 샤딩 도입

  • 문제: 트래픽 증가로 Redis의 단일 노드가 병목 현상을 초래.
  • 해결 전략
    • Redis 클러스터 모드에서 샤딩(sharding) 적용.
    • 데이터를 Key 범위별로 분리하여 여러 Redis 노드에 분산

코드 예시 - Redis 클러스터 샤딩 설정

spring:
  redis:
    cluster:
      nodes:
        - redis-node1:6379
        - redis-node2:6379
        - redis-node3:6379
      max-redirects: 3

5.데이터 보안 문제 해결: 민감 데이터 캐싱 보안

  • 문제: 캐시에 민감한 사용자 데이터(예: 결제 정보)가 저장될 경우 데이터 유출 위험.

  • 해결 전략
    - AES256 암호화 후 캐시에 저장.
    - 데이터 복호화는 사용자 요청 시점에 수행.

코드 예시 - 데이터 암호화와 복호화

class SecureCacheService {
    private val encryptionKey = "1234567890123456" // AES256 Key

    fun encrypt(data: String): String {
        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
        cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(encryptionKey.toByteArray(), "AES"))
        return Base64.getEncoder().encodeToString(cipher.doFinal(data.toByteArray()))
    }

    fun decrypt(data: String): String {
        val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
        cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKey.toByteArray(), "AES"))
        return String(cipher.doFinal(Base64.getDecoder().decode(data)))
    }
}
profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글