MSA - 좌석 생성 로직 성능 개선

헨도·2025년 5월 26일
0

SpringBoot

목록 보기
31/32
post-thumbnail

야구장 티켓을 예매하기 위해선 예매하고 싶은 날짜에 해당하는 좌석들이 생성되어야 합니다.
그래서 날짜 별 좌석 생성 API 를 만들었지만 좌석 생성 테스트를 할 때 시간이 너무 소요된다는 점을 파악하여 성능 개선이 필요하다고 느꼈습니다.

수정 전

비즈니스 로직 내부

@Transactional
public void createSeatBucket(List<Seat> seats, LocalDate date) {
	for (Seat seat : seats) {
    	String cacheKey = seatCommonHelper.makeCacheKey(seat, date);
        Map<String, String> cacheValue = makeCacheValue(seat);
        
        RBucket<Map<String, String>> bucket = redissonClient.getBucket(cacheKey);
        bucket.set(cacheValue);
        
        String blockKey = makeBlockKey(seat, date);
        RList<String> blockSeats = redissonClient.getList(blockKey);
        
        if (!blockSeats.contains(cacheKey)) {
        	blockSeats.add(cacheKey);
        }
    }
}

동작 순서

  1. 좌석 생성 날짜 입력
  2. Seat 테이블 내 모든 좌석 데이터 정보 가져오기
  3. 가져온 모든 좌석 데이터 정보를 반복문을 이용하여 꺼내기
  4. 좌석 별 Cache Key와 Value 생성

    CacheKey - seat:20250526:101:1
    CacheValue - status : "AVAILABLE"

  5. Bucket 값 생성
  6. 블록 별 List Key 생성

    seat:20250526:101

  7. List 에 좌석 별 Cache Key 추가하기

    seat:20250526:101:1
    seat:20250526:101:2
    seat:20250526:101:3
    ...

API 테스트 결과

DB 내 샘플 데이터

  • 300 개

API 소요 시간

  • 약 21초

수정 후

문제 파악

1. RList.contains(cacheKey)

  • RList의 contains 메서드 사용 시, Redis 에서 리스트를 전부 순회하면서 비교한다.
  • 선형 탐색이므로 O(N) 시간이 소요

2. blockSeats.add(cacheKey)

  • 반복문 내 좌석 개수만큼 Redis 호출하여 네트워크 왕복 시간 소요
  • 좌석 1000개면 최소 1000번 호출
  • 즉, 최악의 경우 2000 ~ 3000번 Redis 왕복

개선 방향

RList -> RSet 변경

  • 처음 좌석 목록 조회 설계 시, 순서가 필요할 수 있다는 생각에 RList 사용
    -> 순서가 필요 없으며 중복이 되지 않게 좌석을 관리하기 위해선 Set 사용이 좋다.

파이프라인 처리

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

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

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

Batch Size

  • 25000석 가정 시, 한번에 25000석을 batch 처리는 부담이 있어 1000으로 나누어 설정
  • 대량 데이터도 배치 사이즈에 맞춰 쪼개서 처리
    -> 메모리 & 성능 부담이 확 줄어든다.
final int batchSize = 1000;

수정 코드

@Transactional
public void createSeatBucket(List<Seat> seats, LocalDate date) {
	final int batchSize = 1000;
    int operationCount = 0;
	
    RBatch batch = redissonClient.createBatch();
    Map<String, List<String>> blockSeatKeysMap = new HashMap<>();
	
    for (Seat seat : seats) {
    	String cacheKey = seatCommonHelper.makeCacheKey(seat, date);
        Map<String, String> cacheValue = makeCacheValue(seat);
        
        batch.getBucket(cacheKey).setAsync(cacheValue);
        
        String blockKey = makeBlockKey(seat, date);
        blockSeatKeysMap.computeIfAbsent(blockKey, k -> new ArrayList<>()).add(cacheKey);
        
        operationCount++;
		
    // 배치 처리 후 남은 좌석들을 한번에 처리 
	if (operationCount % batchSize == 0) {
          executeBatch(batch, blockSeatKeysMap);

          batch = redissonClient.createBatch();
          blockSeatKeysMap.clear();
    	}
 	}
            
    if (!blockSeatKeysMap.isEmpty()) {
		executeBatch(batch, blockSeatKeysMap);
    }
}

private void executeBatch(RBatch batch, Map<String, List<String>> blockSeatKeysMap) {
	for (Map.Entry<String, List<String>> entry : blockSeatKeysMap.entrySet()) {
		batch.getSet(entry.getKey()).addAllAsync(entry.getValue());
	}

	batch.execute();
}

API 테스트 결과

DB 내 샘플 데이터

  • 300 개

API 소요 시간

  • 약 0.24초
profile
Junior Backend Developer

0개의 댓글