Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20241211 Redis를 활용하기 위한 설계 초안 본문

개발Article

[TIL]20241211 Redis를 활용하기 위한 설계 초안

최밤빵 2024. 12. 11. 04:59

Redis를 프로젝트에 적용하기 위해 공부하면서, 캐싱과, 데이터 저장소로 활용하는 방법을 고민했다. Redis는 메모리 기반의 빠른 데이터 접근이 가능하다는 장점이 있기때문에, 프로젝트의 성능 최적화와 외부 API 호출 비용 절감에 적합하다고 판단했다. 오늘 개발일지에서는 Redis를 캐싱과 데이터 저장소로 활용하기 위한 설계 과정과 고민했던 내용들을 정리했다. 특히, 데이터 특성에 따라 적합한 설계 방식을 선택하는 데 중점을 두었다.


1. 고민

캐싱을 왜 사용해야 할까?

PUBG 외부 API를 호출하는 프로젝트에서 요청 제한과 비용 문제를 해결하기 위해 캐싱이 필요하다. 특히, 반복적으로 조회되는 데이터나 변경되지 않는 데이터는 캐싱을 통해 외부 API 호출을 줄이고, 사용자 경험을 개선할 수 있다.

캐싱이 시스템을 무겁게 만들 수 있다는 우려

하지만 캐싱을 모든 데이터에 적용하면 Redis 메모리 사용량이 증가하고 관리가 복잡해질 수 있다. 특히, 실시간성이 중요한 데이터나 갱신 주기가 짧은 데이터는 캐싱보다 직접 조회가 더 나을 수 있다는 점도 고려해야 했다.

데이터 저장소로서의 활용 고민

Redis를 단순 캐싱 용도로만 사용하는 것이 아니라, 일부 데이터를 데이터 저장소로도 활용해야 한다. 외부 API 호출의 대체 역할을 하거나, 특정 조회 결과를 장기적으로 저장하여 데이터베이스 접근을 줄이는 방안을 생각했다.

갱신 요청 기능의 필요성

프로젝트에서 캐싱된 데이터는 시간이 지남에 따라 오래되거나 최신 상태를 반영하지 못할 수 있다. 이러한 경우, 사용자가갱신버튼을 통해 데이터를 강제로 갱신할 수 있는 기능이 필요하다. 이를 통해 외부 API의 최신 데이터를 강제로 가져오고, 캐싱된 데이터도 업데이트할 수 있도록 설계해야 한다. 특히 매치 결과처럼 종료 후 고정되지만 초기 요청 시 최신 데이터를 요구하는 경우에 유용할 것 같다.


2. 캐싱 데이터와 직접 조회 데이터의 구분

2-1 캐싱에 적합한 데이터
  • 완료된 후 변경되지 않는 데이터
    • ex. PUBG 매치 결과 데이터
    • 완료된 매치의 결과는 고정되기 때문에 캐싱에 적합하다. TTL을 설정하여 일정 시간 동안 데이터를 유지할 수 있다.
  • 변동 주기가 길고 자주 조회되는 데이터
    • ex. 플레이어 기본 정보
    • 플레이어의 이름이나 ID와 같은 기본 정보는 갱신 주기가 길고 반복적으로 조회될 가능성이 높다.
2-2 캐싱에 부적합한 데이터
  • 실시간으로 갱신되는 데이터
    • ex. 진행 중인 매치 상태, 실시간 순위
    • 이러한 데이터는 항상 최신 상태를 제공해야 하므로 캐싱을 사용하면 오히려 신뢰성을 저하시킬 수 있다.
  • 갱신 주기가 짧은 데이터
    • ex. 플레이어의 접속 상태
    • 데이터가 너무 자주 변경되면 캐싱의 효율이 낮아지고 관리 비용이 증가한다.
2-3 데이터 저장소로 적합한 데이터
  • 반복적으로 조회되는 고정 데이터
    • ex. 특정 매치의 통계 데이터
    • 데이터베이스에 저장할 필요는 없지만, 장기적으로 활용 가치가 있는 경우 데이터 저장소로 활용 가능.
  • 캐싱 데이터와 동일하지만 TTL 없이 유지해야 하는 데이터
    • ex. 프로젝트의 메타 데이터나 설정 정보

3. Redis를 가볍게 설계하자

4-1 꼭 필요한 데이터만 캐싱

캐싱이 필요한 데이터와 그렇지 않은 데이터를 명확히 구분하여 불필요한 캐싱을 줄인다. 반복적으로 요청되는 데이터나 변경되지 않는 데이터만 캐싱하기로 결정했다.

4-2 TTL 설정

TTL을 통해 캐싱된 데이터를 자동으로 만료시킨다. 오래된 데이터를 관리하고, 메모리 사용량을 줄일 수 있다.

  • 매치 결과 데이터: TTL 1시간
  • 플레이어 기본 정보: TTL 24시간
4-3 단순한 로직 유지

캐싱과 외부 API 호출 로직을 단순화하여 유지보수의 부담을 줄인다.

  • Redis에서 먼저 데이터를 조회하고, 없으면 외부 API를 호출.
  • API 호출 후에는 결과를 Redis에 저장.
4-4 캐싱 미스 허용

캐시 미스 시에는 단순히 외부 API를 호출하고 결과를 반환한다. 캐시 미스에 대한 복잡한 로직을 제거함으로써 성능을 유지하기 위함이다.

4-5 데이터 저장소로의 활용

캐싱과 별도로, 일정 데이터는 Redis를 데이터 저장소로 활용하여 TTL 없이 장기적으로 유지하기로 했다. 데이터베이스 접근을 줄이고, 자주 사용되는 데이터를 빠르게 제공할 수 있다.

4-6 갱신 요청 기능 추가

사용자가 최신 데이터를 필요로 할 때 Redis에 저장된 데이터를 강제로 갱신할 수 있는 기능을 추가한다. 사용자 요청에 따라 외부 API를 호출하고, 데이터를 최신 상태로 유지할 수 있다.


4. 구체적인 구현 예시 

 

매치 데이터 캐싱 

public MatchData getMatchData(String matchId) {
    String cacheKey = "PUBG:match:" + matchId;

    // 1. Redis에서 데이터를 조회한다. 
    MatchData cachedData = redisTemplate.opsForValue().get(cacheKey);
    if (cachedData != null) {
        return cachedData; // 캐싱된 데이터 반환
    }

    // 2. 데이터가 없으면 외부 API 호출한다. 
    MatchData apiData = fetchFromApi(matchId);

    // 3. Redis에 데이터 저장 (TTL 1시간)
    redisTemplate.opsForValue().set(cacheKey, apiData, Duration.ofHours(1));

    return apiData;
}

외부 API 호출 

public MatchData fetchFromApi(String matchId) {
    String apiUrl = "https://api.pubg.com/matches/" + matchId;
    try {
        ResponseEntity response = restTemplate.getForEntity(apiUrl, MatchData.class);
        return response.getBody();
    } catch (Exception e) {
        throw new RuntimeException("API 호출 실패", e);
    }
}

데이터 저장소 활용 

public void saveMatchStatistics(String matchId, MatchStatistics stats) {
    String storageKey = "PUBG:stats:" + matchId;

    // TTL 없이 데이터 저장
    redisTemplate.opsForValue().set(storageKey, stats);
}

public MatchStatistics getMatchStatistics(String matchId) {
    String storageKey = "PUBG:stats:" + matchId;
    return redisTemplate.opsForValue().get(storageKey);
}

갱신요청 처리 (다른 방법 찾기)

@RestController
@RequestMapping("/update")
public class UpdateController {

    private final MatchService matchService;

    public UpdateController(MatchService matchService) {
        this.matchService = matchService;
    }

    @PostMapping("/match")
    public ResponseEntity<?> updateMatch(@RequestParam String matchId) {
        MatchData updatedData = matchService.updateMatchData(matchId);
        return ResponseEntity.ok(updatedData);
    }
}
@Service
public class MatchService {

    private final RedisTemplate<String, MatchData> redisTemplate;
    private final ExternalApiService externalApiService;

    public MatchService(RedisTemplate<String, MatchData> redisTemplate, ExternalApiService externalApiService) {
        this.redisTemplate = redisTemplate;
        this.externalApiService = externalApiService;
    }

    public MatchData updateMatchData(String matchId) {
        try {
            return getMatchData(matchId, true); // 갱신
        } catch (Exception e) {
            throw new RuntimeException("갱신 실패: " + e.getMessage(), e);
        }
    }

    public MatchData getMatchData(String matchId, boolean forceUpdate) {
        String cacheKey = "PUBG:match:" + matchId;

        // 갱신 요청이 아닌 경우 Redis에서 캐싱 데이터 조회
        if (!forceUpdate) {
            MatchData cachedData = redisTemplate.opsForValue().get(cacheKey);
            if (cachedData != null) {
                return cachedData;
            }
        }

        // 갱신이거나 캐싱 데이터가 없으면 API 호출
        MatchData latestData = externalApiService.fetchFromApi(matchId);

        // Redis에 데이터 갱신 
        redisTemplate.opsForValue().set(cacheKey, latestData, Duration.ofHours(1));

        return latestData;
    }
}
@Service
public class ExternalApiService {

    private final RestTemplate restTemplate;

    public ExternalApiService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public MatchData fetchFromApi(String matchId) {
        String apiUrl = "https://api.pubg.com/matches/" + matchId;
        try {
            ResponseEntity<MatchData> response = restTemplate.getForEntity(apiUrl, MatchData.class);
            return response.getBody();
        } catch (Exception e) {
            throw new RuntimeException("API 호출 실패", e);
        }
    }
}

→ 이 방식은 갱신요청기능을 추가하려면 모든 샤드 클래스를 의존성 주입방식으로 관리해야 한다. 가능은하지만 비효율적이고, 유지보수에 좋지않을 것 같아서 다른 방법을 찾아야한다...! 


5. 느낀점과 배운점

5-1 캐싱은 데이터 특성에 따라 달라져야 한다

모든 데이터를 캐싱하는 것은 비효율적이다. 데이터의 변동 주기와 중요성을 고려하여 캐싱 여부를 결정해야 한다는 점을 배웠다.

5-2 단순한 설계가 중요하다

캐싱은 단순하고 직관적으로 설계해야 유지보수가 쉽다. TTL을 활용하고, 캐시 미스 시 복잡한 로직을 추가하지 않는 것이 효과적이었다.

5-3 실시간 데이터와 캐싱 데이터의 조합이 필요하다

실시간성이 중요한 데이터는 캐싱하지 않고 직접 조회하도록 설계했다. 이를 통해 사용자에게 최신 데이터를 제공하면서도 캐싱의 효율성을 유지할 수 있었다.

5-4 데이터 저장소로서 Redis의 가능성 확인

Redis는 캐싱뿐만 아니라 데이터 저장소로도 활용할 수 있음을 배웠다. 특히, 특정 데이터를 TTL 없이 저장함으로써 데이터베이스 접근을 줄이고 효율성을 높일 수 있었다.

5-5 갱신 요청 기능의 필요성 이해

강제 갱신 요청 기능을 통해 캐싱된 데이터가 최신 상태를 유지할 수 있다. 특히, 초기 조회 시 오래된 데이터로 인해 발생할 수 있는 사용자 불만을 효과적으로 해결할 수 있다.


Redis를 캐싱뿐만 아니라 데이터 저장소로도 활용할 예정이고, 이를 기반으로 PUBG API 호출을 최적화하려고 한다. 이번 고민과 설계 과정을 참고해서 실제 구현할 때 Redis의 장점도 극대화 하고, 큰 어려움이 없었으면 좋겠다.