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
반응형
'프로젝트 > Wedle' 카테고리의 다른 글
[좋아요 수 기준 HOT 게시판 이동 시 동시성 이슈 해결] 비관적 락을 이용한 게시글 좋아요 수 Race Condition 해결 (0) | 2025.04.09 |
---|---|
[JPA 성능 최적화] @ManyToOne(fetch = FetchType.LAZY) 와 Fetch Join 활용하기 (0) | 2025.04.08 |
[JPA 성능 최적화] 반복되는 save() 대신 saveAll()로 벌크 저장 (0) | 2025.04.08 |
[Spring Boot/Kafka/Stomp] 실시간 채팅 구현 – 7. 채팅 전송 및 조회 (0) | 2025.03.19 |
[Spring Boot/Kafka/Stomp] 실시간 채팅 구현 - 6. 채팅방 생성 (0) | 2025.03.19 |