[항해 플러스 Lite] 6주차 회고 - Redis Distributed Lock, Cache

2025. 6. 29. 22:43·IT/WIL

이번 주 학습 내용

  • Redis를 이용한 분산락
  • Redis를 이용한 캐싱

Spring에서 사용하는 Redis 명령어

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
public class RedisService {

    private final StringRedisTemplate stringRedisTemplate;

    // 생성자 주입
    public RedisService(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // ==================== 저장 메서드 ====================

    // 1. String (문자열) 데이터 저장
    public void saveString(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    // 1-1. 만료 시간과 함께 String 데이터 저장
    public void saveStringWithExpiry(String key, String value, long timeoutInSeconds) {
        stringRedisTemplate.opsForValue().set(key, value, timeoutInSeconds, TimeUnit.SECONDS);
    }

    // 2. List 데이터 저장
    public void saveToList(String key, String value) {
        stringRedisTemplate.opsForList().rightPush(key, value);
    }

    public void saveAllToList(String key, List<String> values) {
        stringRedisTemplate.opsForList().rightPushAll(key, values);
    }

    // 3. Hash 데이터 저장
    public void saveToHash(String key, String field, String value) {
        stringRedisTemplate.opsForHash().put(key, field, value);
    }

    public void saveAllToHash(String key, Map<String, String> data) {
        stringRedisTemplate.opsForHash().putAll(key, data);
    }

    // 4. Set 데이터 저장
    public void saveToSet(String key, String... values) {
        stringRedisTemplate.opsForSet().add(key, values);
    }

    // 5. Sorted Set 데이터 저장
    public void saveToSortedSet(String key, String value, double score) {
        stringRedisTemplate.opsForZSet().add(key, value, score);
    }


    // ==================== 조회 메서드 ====================

    // 1-2. String (문자열) 데이터 조회
    public String getString(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    // 2-1. List 데이터 조회 (전체)
    public List<String> getListRange(String key) {
        // 0부터 -1까지 조회하면 리스트의 모든 원소를 가져옵니다.
        return stringRedisTemplate.opsForList().range(key, 0, -1);
    }

    // 3-1. Hash의 특정 필드 조회
    public String getFromHash(String key, String field) {
        // get() 메서드는 Object를 반환하므로 적절한 타입으로 캐스팅해야 합니다.
        return (String) stringRedisTemplate.opsForHash().get(key, field);
    }

    // 3-2. Hash의 모든 필드와 값 조회
    public Map<Object, Object> getAllFromHash(String key) {
        return stringRedisTemplate.opsForHash().entries(key);
    }

    // 4-1. Set 데이터 조회 (모든 멤버)
    public Set<String> getAllFromSet(String key) {
        return stringRedisTemplate.opsForSet().members(key);
    }

    // 5-1. Sorted Set 데이터 조회 (랭킹 순)
    public Set<String> getSortedSetRange(String key, long start, long end) {
        // start ~ end 범위의 멤버들을 스코어 순(오름차순)으로 조회합니다.
        return stringRedisTemplate.opsForZSet().range(key, start, end);
    }

    // 5-2. Sorted Set 데이터 조회 (역순, 스코어 높은 순)
    public Set<String> getSortedSetReverseRange(String key, long start, long end) {
        // start ~ end 범위의 멤버들을 스코어 역순(내림차순)으로 조회합니다. (랭킹보드 TOP 10 등에 사용)
        return stringRedisTemplate.opsForZSet().reverseRange(key, start, end);
    }
}

분산락

@Service
public class StockService {

    private final RedissonClient redissonClient;
    private int stock = 100; // 예제를 위한 가상 재고

    public StockService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    public void decreaseStock(String stockKey) throws InterruptedException {
        // 1. 락 객체 가져오기
        RLock lock = redissonClient.getLock(stockKey);

        try {
            // 2. 락 획득 시도 (최대 10초 대기, 락 획득 후 1초 뒤 자동 해제)
            // waitTime: 락을 얻기 위해 기다리는 시간
            // leaseTime: 락을 점유하는 시간. 이 시간이 지나면 자동으로 해제됨.
            boolean isLocked = lock.tryLock(10, 1, TimeUnit.SECONDS);

            if (!isLocked) {
                System.out.println("락 획득 실패!");
                return;
            }

            // 3. 락 획득 성공 시 비즈니스 로직 수행
            if (stock > 0) {
                stock--;
                System.out.println(Thread.currentThread().getName() + " -> 재고 감소 성공! 현재 재고: " + stock);
            } else {
                System.out.println(Thread.currentThread().getName() + " -> 재고 없음.");
            }

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw e;
        } finally {
            // 4. 락 해제 (반드시 finally 절에서 수행)
            if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
  1. redissonClient.getLock(key): 동일한 key에 대한 락 객체를 가져옵니다.
  2. lock.tryLock(): 락 획득을 시도합니다. 성공하면 true, 실패하면 false를 반환합니다.
  3. try-finally 구문: 비즈니스 로직에서 예외가 발생하더라도 finally 블록에서 락이 반드시 해제되도록 보장하는 것이 매우 중요합니다.
  4. lock.unlock(): 로직이 끝나면 락을 해제하여 다른 스레드/서버가 락을 획득할 수 있도록 합니다.

예시는 재고 감소로 했지만, 저는 유저의 주문 중복에 분산락을 걸어두었습니다. 하지만 생각해보니 주문도 분산락을 걸어야 할 것 같은데, 어떻게 설계해야 락을 최소화하며 좋은 서비스가 될지 고민해봐야겠습니다.

캐싱

@Service
public class ProductService {

    /**
     * @Cacheable: 캐시가 있으면 DB를 조회하지 않고 캐시에서 바로 반환. 없으면 메서드를 실행하고 결과를 캐시에 저장.
     * - value(cacheNames): 캐시의 이름을 지정. Redis에서는 key의 prefix가 됨. (e.g., products::1)
     * - key: 캐시의 key를 지정. SpEL(스프링 표현 언어)을 사용해 동적으로 생성. '#id'는 파라미터 id를 의미.
     */
    @Cacheable(value = "products", key = "#id")
    public ProductDto findProductById(String id) {
        // DB 조회에 2초가 걸린다고 가정
        try {
            System.out.println("DB에서 상품 정보를 조회합니다: " + id);
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return new ProductDto(id, "상품 " + id, 10000);
    }

    /**
     * @CachePut: 메서드를 항상 실행하고, 그 결과를 캐시에 업데이트(덮어쓰기).
     * 주로 데이터 '수정' 시 사용.
     */
    @CachePut(value = "products", key = "#id")
    public ProductDto updateProduct(String id, String name, long price) {
        System.out.println("DB에서 상품 정보를 수정합니다: " + id);
        // ... DB 업데이트 로직 수행 ...
        return new ProductDto(id, name, price);
    }


    /**
     * @CacheEvict: 지정된 키의 캐시를 삭제.
     * 주로 데이터 '삭제' 시 사용.
     */
    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(String id) {
        System.out.println("DB와 캐시에서 상품 정보를 삭제합니다: " + id);
        // ... DB 삭제 로직 수행 ...
    }
}

또는

@Slf4j
@Component
@RequiredArgsConstructor
public class OrderEventListener {

    private final CacheManager cacheManager;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleCacheEviction(OrderCompletedEvent event) {
        log.info("주문 완료, 캐시 제거를 시작합니다.");

        // 캐시
        Cache productCache = cacheManager.getCache("products");
        if (productCache == null) {
            log.warn("products 캐시를 찾을 수 없습니다. 캐시 제거를 건너뜁니다.");
            return;
        }

        event.productIds().forEach(productId -> {
            try {
                productCache.evict(productId);
                log.info("products::{} 캐시 제거 완료", productId);
            } catch (Exception e) {
                log.warn("products::{} 캐시 제거 실패", productId);
            }
        });

    }
}

회고

이번 주는 Redis를 도커로 띄웠지만 NoAuth 에러가 자꾸 발생하여 시간을 많이 빼았기는데다가, 인텔리제이에서 프로젝트를 빌드 못하는 이슈가 있어서 시간을 너무 많이 빼았겼습니다. NoAuth의 경우 local docker를 다른 포트에 띄우니 잘 되었고, 인텔리제이 프로젝트 빌드 못하는 것은 경로에 한글이 포함되어도 잘 되다가 갑자기 안되어 프로젝트를 다시 pull하여 build하니 잘 되었습니다.

그래도 과제를 하루 늦게 제출하여, 분산락과 캐싱에대해 학습을 할 수 있어 다행이었고, 항해 10 Pass를 못한 것은 아쉽지만, 꾸준하게 제가 챙겨갈 수 있는 몫을 챙겨가도록 하려합니다.

'IT > WIL' 카테고리의 다른 글

항해99 플러스 Lite 백엔드 과정 후기  (3) 2025.08.10
[항해 플러스 Lite] 7주차 회고 - Redis를 사용한 대기열 및 랭킹 조회  (4) 2025.07.06
[항해 플러스 Lite] 5주차 회고 - 서버 구축 DB Lock  (0) 2025.06.22
[항해 플러스 Lite] 4주차 회고 - 서버 구축 DB 설계  (1) 2025.06.15
[항해 플러스 Lite] 3주차 회고 - 클린 아키텍처  (1) 2025.06.08
'IT/WIL' 카테고리의 다른 글
  • 항해99 플러스 Lite 백엔드 과정 후기
  • [항해 플러스 Lite] 7주차 회고 - Redis를 사용한 대기열 및 랭킹 조회
  • [항해 플러스 Lite] 5주차 회고 - 서버 구축 DB Lock
  • [항해 플러스 Lite] 4주차 회고 - 서버 구축 DB 설계
exp999
exp999
stack-up 님의 블로그 입니다.
  • exp999
    Stack 모아 Overflow
    exp999
  • 전체
    오늘
    어제
    • 분류 전체보기 (10)
      • IT (10)
        • TIL (2)
        • WIL (8)
  • 블로그 메뉴

    • IT
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    TDD
    항해99lite 1기
    항해99플러스Lite 후기
    항해99+lite
    항해99플러스
    항해99+ lite
    항해99+
    항해플러스후기
    항해99plus
    항해99
    서버 설계
    항해플러스백엔드
    동시성
    항해99lite
    서버 구축
    til
    항해솔직후기
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
exp999
[항해 플러스 Lite] 6주차 회고 - Redis Distributed Lock, Cache
상단으로

티스토리툴바