1. 기존 구현 방식
기존의 회차별 좌석 조회는 DB + Redis를 조합해서 조회하는 로직이다.
즉, 특정 sessionId 공연 회차의 모든 좌석을 조회하되, 좌석의 상태는 Redis 값을 우선시해서 보여주는 것이다.
//해당 회차별 좌석 목록 조회
@Override
public List<SeatResponseDto> getSeatBySession(UUID sessionId) {
SessionId session = new SessionId(sessionId);
//DB에서 특정 sessionId에 해당하는 모든 좌석 가져오기
//상태는 DB의 p_seats 테이블에서 가져온 기본 상태
List<Seat> seats = seatRepository.findAllBySessionId(session);
//Redis에서 전체 좌석 상태 가져오기(해시 상태로 전부 조회)
String redisKey = "seat:" + sessionId;
Map<Object, Object> redisStatuses = redisTemplate.opsForHash().entries(redisKey);
//Redis 상태를 반영해서 Seat 객체 상태 수정
//각각의 좌석을 순회하면서 Redis에 저장된 최신 상태 확인
return seats.stream()
.map(seat -> {
String seatId = seat.getSeatId().toString(); //UUID
//Redis에 해당 좌석의 상태가 있으면 -> Redis 값을 우선 적용해서 상태 덮어씌움
if (redisStatuses.containsKey(seatId)) {
SeatStatus redisStatus = SeatStatus.valueOf((String) redisStatuses.get(seatId));
return SeatResponseDto.of(seat.withStatus(redisStatus));
}
//Redis에 상태 정보 없으면 DB 값 그대로 사용
return SeatResponseDto.of(seat);
})
.toList();
}
[Seat.java]
/*
* 상태만 바꾼 복제본 만들어줌
* */
public Seat withStatus(SeatStatus newStatus) {
Seat clone = new Seat(this.seatCode, this.sessionId, newStatus, this.price);
clone.seatId = this.seatId;
clone.userId = this.userId;
return clone;
}
Seat의 withStatus 메서드는 원본 Seat 객체는 그대로 유지하면서, 상태만 바꾼 새로운 Seat 객체를 생성하는 것
불변 객체를 흉내 낸 방식으로 Seat 객체에 setter를 쓰는 대신 복사해서 일부 값만 변경하는 방식
즉, Redis 기준 상태를 반영한 Seat 복제본을 만든 다음 그걸 응답 DTO로 변환해서 리턴하는 구조이다.
하지만 해당 회차별 좌석 목록 조회에 있어서 트래픽이 많이 몰릴 것으로 예상하여 조회 결과를 캐싱해두고 더 빠르게 조회하기 위해 리팩토링을 진행하려고 한다.
- 📉 문제점
- 매 요청마다 DB와 Redis에 중복 접근
- 매번 stream().map() 조합 -> GC 및 처리 지연
- 트래픽 증가 시 부하 발생
2. 문제 발생
다른 것과 마찬가지로 캐시를 적용하고 아래와 같이 데이터를 읽으려고 했을 때 ClassCastException 에러가 나는 것이 아닌가?!
List<SeatResponseDto> cached = (List<SeatResponseDto>) redisTemplate.opsForValue().get("seat-cache:sessionId");
ERROR
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to SeatResponseDto
3. 원인 분석 : Redis 직렬화/역직렬화 방식 차이
1. RedisTemplate 기본 설정
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
...
return template;
}
- StringRedisSerializer를 사용하고 있기 때문에 객체를 직렬화하지 않음
- 결과적으로 Objcet -> JSON -> LinkedHashMap으로 역직렬화 됨!
4. 해결 방법: Jackson2JsonRedisSerializer 적용
1. 새로운 RedisTemplate 등록
@Bean
public RedisTemplate<String, Object> objectRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
template.setDefaultSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
return template;
}
- 좌석 목록 캐싱에는 RedisTemplate<String, Objcet> 사용
- JSON 직렬화/역직렬화에 강력한 Jackson2JsonRedisSerializer 적용
2. 서비스 코드에서 적용
List<SeatResponseDto> cached = (List<SeatResponseDto>) objectRedisTemplate.opsForValue().get("seat-cache:" + sessionId);
- 캐시 저장시
objectRedisTemplate.opsForValue().set("seat-cache:" + sessionId, response, Duration.ofMinutes(1));
5. 왜 StringRedisTemplate는 그대로 유지했나?
- 선점 상태(HASH)는 여전히 "seat:<sessionId>" 에 Hash<String, String> 형태로 저장
- 따라서 선점 상태와 캐시 결과 타입이 다르기 때문에 RedisTemplate를 분리하여 사용
6. 캐시 무효화 처리
상태가 변경되면 캐시 키 삭제되고 다시 조회되면 재캐싱을 위해 추가로 작성했다.
이 흐름은 "읽기 최적화"와 "정합성 보장"을 둘 다 만족시키기 위한 Read-Through + Cache Invalidation 패턴으로 아래와 같은 흐름이다.
[동작 흐름]
- 사용자 요청 → getSeatBySession 호출
- Redis 캐시 존재 여부 확인
- 있으면 → 바로 반환
- 없으면 → DB+Redis 읽어서 응답 + Redis 캐싱
- 선점/예약/복구 등 상태 변경 → seat-cache:<sessionId> 삭제 (무효화)
- 다음 조회 요청 → 다시 DB+Redis 읽고 재캐싱
결과적으로 항상 최신 상태 유지하면서 캐싱 효율도 높이는 구조!!
redisTemplate.delete("seat-cache:" + seat.getSessionId().getValue());
- 상태 변경(예약/복구/선점)
- 좌석 생성/삭제
이 모든 지점에서 캐시를 삭제해두면 TTL 이전이라도 정확한 데이터가 다시 캐시된다!
7. 결과
항목 | Before | After |
조회 방식 | DB + Redis 조합 | Redis 단건 조회 |
응답 시간 | 수십 ms 이상 | 수 ms 내외 |
GC/CPU | 중간~높음 | 매우 낮음 |
코드 복잡도 | 반복 로직 많음 | 단순화됨 (read-through 캐시) |
'DBMS > Redis' 카테고리의 다른 글
[Redis] Redis TTL/락 시간 불일치 문제 & TTL 만료 후 userId 조회 실패 & ExpireAt 개선 (0) | 2025.04.28 |
---|---|
[Redis] redis를 Spring Boot에 추가 (0) | 2025.02.14 |
[Redis] Redis 캐싱 전략 (0) | 2025.01.14 |
[Redis] 기본 명령어 (0) | 2025.01.14 |
[Redis] Redis란?, 설치 (0) | 2025.01.07 |