MSA - 블록 별 좌석 조회 로직 성능 개선

헨도·2025년 5월 27일
0

SpringBoot

목록 보기
32/32
post-thumbnail

야구장 티켓을 예매하기 위해선 예매하고 싶은 블록에 해당하는 좌석을 조회해야합니다.
하지만 블록별 좌석 조회 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;
}

동작 순서

  1. 오늘 날짜 가져오기
  2. userId를 통해 유저 멤버쉽 현황 파악하기
  3. 요청으로 받은 블록 번호로 RList에 저장된 값 조회하기
  4. RList 내 저장된 좌석 List 가져오기

API 테스트 결과

DB 내 샘플 데이터

  • 300 개

API 소요 시간

  • 약 3.1초

수정 후

문제 파악

1. RList

  • 좌석 생성과 통일성을 위해 RList -> RSet 설정
  • 중복 없는 자료구조로 정리 시, List 보다 성능이 빠르다는 장점

2. Redis 호출 수

  • 리스트에 좌석 수만큼 반복문을 돌면서 Redis 통신 발생
    -> 성능 이슈로 이어질 수 있다.

  • 만약 300 좌석일 경우, 300번 Redis 호출

// 문제 코드

RBucket<Map<String, String>> seatBucketKey = redissonClient.getBucket(blockSeat);
Map<String, String> seatBucketValue = seatBucketKey.get();

개선 방향

1. RList -> RSet 변경

  • 처음 설계 시, 좌석 리스트 순서가 보장되어야 한다는 생각에 List 를 사용
    -> 순서 필요없고, 중복이 되지 않게 좌석을 관리하기 위해선 Set을 사용하는게 좋다고 생각

2. 파이프라인 처리

  • 불필요한 네트워크 왕복을 줄이기 위해 알아보다 batch를 사용하여 Redis 명령어를 한번에 보낼 수 있다는 점 확인하여 도입
RBatch batch = redissonClient.createBatch();

// batch 를 통해 bucket 에 값 설정 
batch.getBucket(cacheKey).setAsync(cacheValue);

// batch 실행 명령어
batch.execute();

3. 비동기 요청 보내기

  • 좌석 키마다 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;
}

API 테스트 결과

DB 내 샘플 데이터

  • 300개

API 소요 시간

  • 약 0.67초
profile
Junior Backend Developer

0개의 댓글