프로젝트/Wedle

[자주 조회되는 데이터 Redis 캐싱] – 학사 캘린더 캐싱

예롱메롱 2025. 4. 9. 11:36
728x90
반응형

들어가며

우리는 매년 1년치 학교 일정을 Neis API를 통해 미리 받아와 DB에 저장해두고 있는데 실제로 사용자들이 가장 자주 조회하는 건 ‘오늘’ 혹은 ‘이번 달’ 일정이라서 자주 쓰는 정보를 매번 DB를 거치는 게 과연 효율적일까? 고민이 돼서 캐싱을 하기로 했습니다!

💡 캐싱 고민의 시작

초기에는 다음과 같은 방식으로 설계했습니다:

  • 매년 1월 1일, 1년치 데이터를 DB에 저장 
  • 사용자가 특정 날짜를 조회하면 DB에서 일정 조회
  • DB에 없으면 외부 NEIS API를 호출해 저장 후 제공

그런데,

🤔 “같은 데이터를 왜 매번 DB에서 불러올까?”, “자주 조회되는 '이번 달' 일정만 Redis에 올려두면 더 빠르지 않을까?”

라는 생각이 들어 Redis 캐싱을 도입하게 되었습니다.


🚨 Redis 저장 중 발생한 ClassCastException

Redis에 데이터를 저장하려다 아래와 같은 에러를 마주했습니다.

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class java.lang.String
	at org.springframework.data.redis.serializer.StringRedisSerializer.serialize

📌 원인

  • StringRedisSerializer는 오직 String만 직렬화할 수 있음
  • 우리는 Redis에 Object, 정확히는 LinkedHashMap 형태의 데이터를 저장하고 있었음
  • 타입 불일치로 직렬화 실패 → 캐스팅 에러 발생

✅ 해결: RedisTemplate에 JSON 직렬화기 적용

Redis에 객체를 저장하고 싶다면, Jackson2JsonRedisSerializer 같은 JSON 직렬화기를 적용해야 합니다.

🔧 적용 방법

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); // 바뀐 부분
    return template;
}

⚙️ Redis 캐싱 적용한 핵심 로직

public TotalSchoolCalendarResponse getScheduleByDate(SchoolCalendarByDateRequest request) {
    String socialId = getCurrentUserId();
    Member member = memberRepository.findBySocialId(socialId)
            .orElseThrow(MemberNotFoundException::new);
    School school = member.getSchool();
    LocalDate date = request.getDate();

    String redisKey = "calendar:" + school.getId() + ":" + date;

    // 1. Redis에서 먼저 조회
    Object cached = redisTemplate.opsForValue().get(redisKey);
    if (cached != null) {
        return objectMapper.convertValue(cached, TotalSchoolCalendarResponse.class);
    }

    // 2. DB 조회
    List<SchoolCalendar> schoolCalendarsDB = schoolCalendarRepository.findBySchoolAndDate(school, date);
    if (!schoolCalendarsDB.isEmpty()) {
        TotalSchoolCalendarResponse response = convertToSchoolCalendarResponse(schoolCalendarsDB, school.getName());
        redisTemplate.opsForValue().set(redisKey, response); // Redis 캐싱
        return response;
    }

    // 3. 외부 API 호출
    List<NeisSchoolCalendarResponse> scheduleByDate = neisSchoolCalendarApiClient.getScheduleByDate(
            school.getSchoolCode(), date, school.getAtptCode());

    TotalSchoolCalendarResponse response = convertToSchoolCalendarResponseByNeis(scheduleByDate, school.getName());
    redisTemplate.opsForValue().set(redisKey, response); // Redis 캐싱
    return response;
}

📈 기대 효과

  • 자주 조회되는 월/일 단위 데이터는 Redis를 통해 빠르게 응답
  • DB 부하 감소, 특히 같은 날짜를 여러 사용자가 반복 조회할 때 유효
  • NEIS API 호출 감소 (비동기 호출로도 여전히 대비됨)

🧠 언제 캐싱을 고려할까?

조건  캐싱 여부
데이터 변경이 적고 조회가 잦음 ✅ 캐싱 적극 추천
매번 값이 달라지는 정보 ❌ 캐싱 부적절
특정 기간만 자주 조회됨 (ex. 이번 달) ✅ 부분 캐싱

✅ 정리하며

  • 학교 일정은 특정 날짜/월별로 자주 조회됨
  • 연 단위 전체 데이터를 매번 RDB에서 불러오는 건 비효율적
  • Redis를 활용해 자주 조회되는 날짜 범위만 캐싱
  • 조회 속도 개선 + RDB 부하 감소 효과
  • Redis TTL을 이용해 단기적으로만 캐싱하고, 장기 데이터는 여전히 RDB에 의존
  • 예상 가능한 범위(예: 오늘 ~ 다음 달)만 미리 캐싱하는 전략도 유효
  • 데이터 변경이 적고 조회가 많은 구조에 Redis 캐싱은 유리
728x90
반응형