야구장 티켓을 예매하기 위해선 예매하고 싶은 블록에 해당하는 좌석을 조회해야합니다.
하지만 블록별 좌석 조회 API 작성 후 테스트 시 시간이 너무 소요되어 성능 개선이 필요하다고 생각하여 개선하게 되었습니다.
@Transactional(readOnly=true)
public CacheBlockServiceResponseDto getBlockSeats(Long userId, CacheBlockServiceRequestDto request) {
LocalDate today = LocalDate.now();
membershipHelper.checkMembership(today, request.date(), userId);
// 멤버쉽 여부 확인 후 예약 가능 날짜와 시간인지 확인
RList<String> blockSeats = getCacheBlockSeatsService.getBlock(request);
// List로 저장된 블록 목록 중 요청이 들어온 블록 확인 후 가져오기
List<CacheSeatServiceResponseDto> seats = getCacheBlockSeatsService.getBlockSeats(blockSeats);
// 확인 후 가져온 블록 내 존재하는 좌석들 List 로 가져오기
return seatApplicationMapper.toCacheBlockServiceResponseDto(request.blockId(), seats);
}
public RList<String> getBlocks(CacheBlockServiceRequestDto request) {
String cacheBlockKey = seatCommonHelper.createCacheBlockKey(request.blockId(), request.date());
// 블록을 가져오기 위해 키 조합
RList<String> blockSeats = redissonClient.getList(cacheBlockKey);
// 조합된 키로 해당하는 블록 확인 후 가져오기
if (!blockSeats.isExists()) {
throw new GlobalException(SeatErrorCode.NOT_FOUND_BLOCK);
}
// 존재하지 않는 블록일 경우, 오류
return blockSeats;
}
public List<CacheSeatServiceResponseDto> getBlockSeats(RList<String> blockSeats) {
List<CacheSeatServiceResponseDto> seats = new ArrayList<>();
for (String blockSeat : blockSeats) {
RBucket<Map<String, String>> seatBucketKey = redissonClient.getBucket(blockSeat);
// 블록 별 좌석 가져오기
Map<String, String> seatBucketValue = seatBucketKey.get();
// Redis 에 저장된 블록 별 좌석 정보 가져오기
if (seatBucketValue == null) {
throw new GlobalException(SeatErrorCode.NOT_FOUND_SEAT);
}
// 만약 좌석 정보가 존재하지 않는다면 오류
String status = seatBucketValue.get("status");
// status 값 확인
CacheSeatServiceResponseDto seat = seatApplicationMapper.toCacheSeatServiceResponseDto(
blockSeat,
status
);
seats.add(seat);
// seats ArrayList에 추가
}
return seats;
}
RList
-> RSet
설정리스트에 좌석 수만큼 반복문을 돌면서 Redis 통신 발생
-> 성능 이슈로 이어질 수 있다.
만약 300 좌석일 경우, 300번 Redis 호출
// 문제 코드
RBucket<Map<String, String>> seatBucketKey = redissonClient.getBucket(blockSeat);
Map<String, String> seatBucketValue = seatBucketKey.get();
RList
-> RSet
변경List
를 사용Set
을 사용하는게 좋다고 생각batch
를 사용하여 Redis 명령어를 한번에 보낼 수 있다는 점 확인하여 도입RBatch batch = redissonClient.createBatch();
// batch 를 통해 bucket 에 값 설정
batch.getBucket(cacheKey).setAsync(cacheValue);
// batch 실행 명령어
batch.execute();
setAsync()
메서드를 사용하여 비동기 요청futureMap
에 넣어 결과를 사용할 때 좌석 키와 매칭 가능하게 만들기RFuture<Map<String, String>> future = batch.getBucket(blockSeat).getAsync();
// RFuture - Redisson 에서 제공하는 비동기 결과 객체
futureMap.put(blockSeat, future);
// batch.execute() 후 각 좌석 키와 비동기 요청 결과를 좌석 키별로 매칭하여 순서대로 꺼내쓰기 위해 사용
public RSet<String> getBlocks(CacheBlockServiceRequestDto request) {
String cacheBlockKey = seatCommonHelper.createCacheBlockKey(request.blockId(), request.date());
RSet<String> blockSeats = redissonClient.getSet(cacheBlockKey);
if (!blockSeats.isExists()) {
throw new GlobalException(SeatErrorCode.NOT_FOUND_BLOCK);
}
return blockSeats;
}
public List<CacheSeatServiceResponseDto> getBlockSeats(RSet<String> blockSeats) {
List<CacheSeatServiceResponseDto> seats = new ArrayList<>();
RBatch batch = redissonClient.createBatch();
Map<String, RFuture<Map<String, String>>> futureMap = new HashMap<>();
for (String blockSeat : blockSeats) {
RFuture<Map<String, String>> future = batch.<Map<String, String>>getBucket(blockSeat).getAsync();
futureMap.put(blockSeat, future);
}
batch.execute();
for (Map.Entry<String, RFuture<Map<String, String>>> entry : futureMap.entrySet()) {
String blockSeat = entry.getKey();
Map<String, String> seatBucketValue;
try {
seatBucketValue = entry.getValue().get();
} catch (Exception e) {
throw new GlobalException(SeatErrorCode.NOT_FOUND_BLOCK);
}
if (seatBucketValue == null) {
throw new GlobalException(SeatErrorCode.NOT_FOUND_SEAT);
}
String status = seatBucketValue.get("status");
CacheSeatServiceResponseDto seat = seatApplicationMapper.toCacheSeatServiceResponseDto(blockSeat, status);
seats.add(seat);
}
return seats;
}