Arcus 캐시에서 MaxBkeyRange 이용하여 최근 내역 자동 관리 방안

잼투인·2022년 5월 11일
0

SNS 혹은 쇼핑몰에서 사용자들에게 편리한 서비스 제공을 위해 사용자의 최근 내역(활동 내역, 조회한 상품 내역, 장바구니)들을 저장하여 제공하고 있습니다. 저장된 데이터는 영구적으로 저장하지 않고 최근 N일 내의 데이터만을 유지하며 사용자에게 제공하는 것이 일반적입니다.

대부분의 최근 내역은 DB에 저장하여 관리 합니다. 최근 내역의 관리를 위해서 주기적으로 스케줄링 작업을 통해 오래된 내역을 삭제해야 합니다. 뿐만 아니라 최근 내역은 사용자가 빈번하게 요청하는 데이터이기 때문에 반복적으로 DB 조회가 요청될 수 있고, 데이터 제공 시에 상품 등과 join 질의가 수행된다면 DB에 더 큰 부담이 됩니다. 이러한 스케줄링 작업 및 빈번한 조회 요청으로 부터 DB 부하를 줄이기 위해 최근 내역 데이터를 캐시에 저장해 제공할 수 있습니다.

하지만, 캐시에서도 최근 내역 데이터를 저장하여 관리하는 것은 복잡한 작업입니다. 예를 들어 TreeMap형태로 사용자의 내역 데이터를 저장한다고 가정하면, 오래된 내역을 제거하기 위해 아래와 같은 작업을 주기적으로 수행해야 합니다.

  • 현재일자와 비교하여 제거할 내역의 시간 범위 계산
  • 계산된 범위에 속한 오래된 내역 일괄 제거

주기적으로 DB에서 사용자의 전체 목록을 조회하여 캐시에 저장된 사용자들의 오래된 내역 데이터를 삭제하는 것은 DB뿐만 아니라 캐시 성능에도 영향을 미치어 캐시 사용 효율이 떨어지게 됩니다.

만약 이런 최근 내역 데이터를 캐시에서 설정한 일자로부터 경과한 데이터에 대해 스케줄 작업없이 자동으로 삭제해 준다면, 시스템 전체 성능 향상 뿐만아니라 응용 개발자의 입장에서 얼마나 편리할까요?

지금부터 이 모든 기능이 수행 가능한 Arcus의 기능을 소개하도록 하겠습니다.

Arcus에서 지원하는 아이템 유형은 key-value 유형과 list, set, map, b+tree 형태의 collection 유형이 있으며, 본 글에서는 b+tree 유형의 속성 정보 중 하나인 maxbkeyrange에 대해서 설명하고 사용 예시를 통해 최근 내역 데이터를 관리하는 방법을 소개하도록 하겠습니다.

B+tree

b+tree는 Arcus에서 지원하는 Collection 유형 중 하나로, leaf 노드에 <bkey, data>구조의 elements를 정렬하여 저장하는 자료구조를 가지며, 아래 그림과 같습니다.

MaxBkeyRange

maxbkeyrange는 b+tree 유형에만 제공되는 b+tree only 속성 정보이며, Max(최대) Bkey(bkey) Range(범위) 말 그대로 bkey들의 최대 범위를 지정하는 속성 정보입니다.

더욱 자세하게 설명하면, maxbkeyrange는 b+tree내에 저장할 수 있는 제일 작은 bkey(smallest bkey)와 제일 큰 (largest bkey)의 사이의 최대 범위를 나타냅니다. b+tree에 maxbkeyrange를 설정하고 새로운 element를 추가할 때 maxbkeyrange 범위를 벗어나면 b+tree의 overflowaction 정책에 의해 기존 element가 제거되거나(smallest_trim) 새로운 element가 추가되지 않게(largest_trim) 됩니다.

아래 그림은 maxbkeyrange가 10인 b+tree에 bkey가 11인 새로운 element가 삽입된다고 했을 때의 모습입니다. maxbkeyrange 조건을 위배하지 않았으므로, 새로운 element가 정상적으로 삽입됩니다.

다음으로 bkey가 12인 element를 삽입합니다. 맨 앞 element의 bkey값은 1이고 추가되는 element의 bkey값은 12로 둘의 차이는 11이 되며 maxbkeyrange의 범위를 초과하게 됩니다. 이 경우, maxbkeyrange 설정 준수를 위해 현재 b+tree의 overflowaction 정책을 수행하게 됩니다.

현재 b+tree의 overflowaction 정책이 "smallest_trim"(최소 bkey를 가진 element 삭제)이라고 한다면, bkey가 1인 맨 앞의 element는 삭제되고 새로 추가되는 element가 b+tree에 삽입되게 됩니다. overflowaction 정책이 수행되고 새로운 element 삽입에 관한 결과는 아래의 그림과 같습니다.

코드로 살펴보기

다음은 위의 예시를 arcus-java-client 이용하여 응용 코드로 살펴 보겠습니다. maxbkeyrange를 10으로 overflowaction은 smallest_trim으로 설정한 뒤 15개의 elements를 삽입 했을 때 수행 결과를 살펴 볼 수 있습니다.

public void testMaxBkeyRange() throws InterruptedException, ExecutionException {
    String key = "btree";
    long maxBkeyRange = 10L;

    // b+tree 생성
    CollectionOperationStatus createStatus = createBtree(key);
    System.out.println("b+tree 생성결과 :"+ createStatus.getResponse());
    if (!createStatus.isSuccess()) {
      System.out.printf("b+tree 생성 실패");
      return;
    }

    // b+tree의 attribute 설정
    if (!setAttribute(key, maxBkeyRange)) {
      System.out.printf("setAttribute 실패");
      return;
    }

    for (int i = 1; i <= 15; i++) {
      long bkey = i;
      String value = "TestValue" + i;

      // element 삽입 수행
      CollectionOperationStatus insertStatus = insertElement(key, bkey, value);
      if (!insertStatus.isSuccess()) {
        System.out.println(i + "번째 element 삽입 실패");
        return;
      }
      System.out.println(i +"번째 element 삽입 결과 :" + insertStatus.getResponse());

      // element 조회
      List<Element<Object>> elementList = findAllElements(key);
      long first = elementList.get(0).getLongBkey();
      long last = elementList.get(elementList.size() - 1).getLongBkey();
      System.out.println("맨 앞 element bkey : " + first + ", 맨 뒤 element bkey : " + last);
    }

    // attribute 정보를 조회합니다.
    System.out.println("");
    System.out.println("attribute 정보");
    CollectionFuture<CollectionAttributes> attributeFuture = client.asyncGetAttr(key);
    CollectionAttributes collectionAttributes = attributeFuture.get();
    System.out.println("type=" + collectionAttributes.getType());
    System.out.println("expiretime=" + collectionAttributes.getExpireTime());
    System.out.println("count=" + collectionAttributes.getCount());
    System.out.println("overflowaction=" + collectionAttributes.getOverflowAction());
    System.out.println("maxbkeyrange=" + collectionAttributes.getMaxBkeyRange());
    System.out.println("minbkey=" + collectionAttributes.getMinBkey());
    System.out.println("maxbkey=" + collectionAttributes.getMaxBkey());
}

private boolean setAttribute(String key, long maxBkeyRange) 
  throws ExecutionException, InterruptedException {
    CollectionAttributes collectionAttributes = new CollectionAttributes();
    // overflowAction 정책 설정
    collectionAttributes.setOverflowAction(CollectionOverflowAction.smallest_trim);
    // maxbkeyrange 설정
    collectionAttributes.setMaxBkeyRange(maxBkeyRange);
    return client.asyncSetAttr(key, collectionAttributes).get();
}

private CollectionOperationStatus createBtree(String key) 
  throws ExecutionException, InterruptedException {
    CollectionFuture<Boolean> future 
        = client.asyncBopCreate(key, ElementValueType.STRING, new CollectionAttributes());
    future.get();
    return future.getOperationStatus();
}

private CollectionOperationStatus insertElement(String key, Long bkey, String value) 
  throws ExecutionException, InterruptedException {
    CollectionFuture<Boolean> insertFuture
            = client.asyncBopInsert(key, bkey, null, value, null);
    insertFuture.get();
    return insertFuture.getOperationStatus();
}

private List<Element<Object>> findAllElements(String key) 
  throws InterruptedException, ExecutionException {
    Map<Long, Element<Object>> resultMap
            = client.asyncBopGet(key, 0, Long.MAX_VALUE,
                                 ElementFlagFilter.DO_NOT_FILTER, 
                                 0, 0, false, false).get();
    return resultMap.entrySet()
                    .stream()
                    .map(Map.Entry::getValue)
                    .collect(toList());
} 

실행 결과, 맨 앞 element의 bkey와 맨 뒤 element의 bkey의 차이가 10이하(bkey 1 ~ bkey 11) 인 경우에는 element의 삭제가 일어나지 않고, 모든 element의 삽입이 이루어졌으며, 10을 초과시(bkey 1 ~ bkey 12) overflowaction 정책에 따라 맨 앞 element가 삭제되고 새로운 element가 삽입됨을 볼 수 있습니다.

b+tree 생성결과 :CREATED
1번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 1
2번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 2
3번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 3
4번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 4
5번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 5
6번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 6
7번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 7
8번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 8
9번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 9
10번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 10
11번째 element 삽입 결과 :STORED
맨 앞 element bkey : 1, 맨 뒤 element bkey : 11
12번째 element 삽입 결과 :STORED
맨 앞 element bkey : 2, 맨 뒤 element bkey : 12
13번째 element 삽입 결과 :STORED
맨 앞 element bkey : 3, 맨 뒤 element bkey : 13
14번째 element 삽입 결과 :STORED
맨 앞 element bkey : 4, 맨 뒤 element bkey : 14
15번째 element 삽입 결과 :STORED
맨 앞 element bkey : 5, 맨 뒤 element bkey : 15

attribute 정보
type=btree
expiretime=10
count=11
overflowaction=smallest_trim
maxbkeyrange=10
minbkey=5
maxbkey=15

지금까지 가장 작은 bkey와 가장 큰 bkey의 범위를 설정하고 그 범위가 초과하면, b+tree의 smallest_trim overflowaction 정책에 따라 동작이 수행되는 maxbkeyrange 속성에 대해 알아보고 해당 특성을 코드로 살펴보았습니다.

MaxBkeyRange 활용

다음으로는 maxbkeyrange를 활용하여 응용에서 적용해 볼 수 있는 예제에 대해서 알아보도록 하겠습니다.

이번 예제에서는 어떤 응용이 b+tree에 데이터를 캐싱하고 최근 5일 치의 데이터만을 유지한다고 가정합니다. 초 단위의 시간 값을 bkey로 사용한다면 maxbkeyrange는 5일 치에 해당하는 값인 432000(5 24 60 * 60)으로 지정합니다. 그리고 최근 값만을 유지하기 위해 overflowaction 정책을 "smallest_trim"으로 설정합니다. 따라서, 새로운 아이템이 추가될 시에 가장 오래된 아이템과 새로 추가된 아이템이 5일 차이가 난다면 가장 오래된 아이템을 캐시에서 자동으로 삭제하게 됩니다.

실제 응용 코드 구현

맨 처음 말씀드렸던 SNS 혹은 쇼핑몰에서 나의 최근 내역(활동 내역, 조회한 상품 내역, 장바구니)를 시간순으로 캐싱하는 요구사항이 있습니다. 이 데이터는 영구적으로 캐싱 되지 않고 최근 데이터를 기준으로 5일만 캐싱 된다고 합니다. 해당 요구 사항을 위 예제 설명을 이용해 코드로 구현해 보겠습니다.

코드 구현은 java를 이용하여 구현하도록 하겠습니다. 먼저 해당 요구사항에 맞게 코드를 구현할 MyService 클래스를 생성해 줍니다.

public class MyService {

  private final ArcusClient repository;
  private static final String KEY_PREFIX = "MY_SERVICE";
  // 실제로는 5일이지만, 빠른 테스트를 결과를 위해 1일 = 1초라고 가정하겠습니다.
  private static final long MAX_BKEY_RANGE = 5;
  
  // 생성자를 통해 설정된 arcusClient 객체를 주입 받습니다.
  // Spring에서 해당 객체를 Bean으로 등록하였다면 @Autowired를 사용해도 무방하지만, 생성자 주입을 권장합니다.
  public MyService(ArcusClient repository) {
    this.repository = repository;
  }
  
  // 유저의 최근 데이터를 기록하기 위해 b+tree 생성 및 사용목적에 맞게 설정하는 메소드
  public boolean initUserAction(long id) throws InterruptedException, ExecutionException {
    CollectionAttributes attributes = new CollectionAttributes();
    
    // b+tree를 생성해 줍니다. 유저의 id를 활용하여 key를 설정해줍니다.
    repository.asyncBopCreate(KEY_PREFIX + id, ElementValueType.STRING, attributes).get();
    
    // maxbkeyrange를 설정해 줍니다. 5일(5초) 이상 차이가 난다면 b+tree에서 해당 데이터를 제거할 것 입니다.
    attributes.setMaxBkeyRange(MAX_BKEY_RANGE);
    // 가장 오래된 데이터를 먼저 제거하기 위해 smallest_trim 정책을 설정합니다. 
    attributes.setOverflowAction(CollectionOverflowAction.smallest_trim);
    
    return repository.asyncSetAttr(KEY_PREFIX + id, attributes).get();
  }
  
  // 최근 내역부터 조회를 위해 역순으로 조회하고, 키를 제외한 데이터만 추출하여 반환합니다.
  public List<String> getLatestData(long id) throws InterruptedException, ExecutionException {
    Map<Long, Element<Object>> resultMap
            = repository.asyncBopGet(KEY_PREFIX + id, Long.MAX_VALUE, 0,
                                      ElementFlagFilter.DO_NOT_FILTER,
                                      0, 0,
                                      false, false).get();
    
    if (resultMap == null) {
      return null;
    }

    return resultMap.entrySet().stream()
                               .map(entry -> entry.getValue().getValue().toString())
                               .collect(toList());
  }

  // 데이터 저장 메소드
  public boolean save(long id, String data) throws InterruptedException, ExecutionException {
    long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
    CollectionFuture<Boolean> future 
            = repository.asyncBopInsert(KEY_PREFIX + id, now, null, 
                                        data, null);
    future.get();
    return future.getOperationStatus().isSuccess();
  }
}

MyService를 수행할 Test class를 생성하거나, Spring의 경우에는 Controller Layer를 통해 각 기능(생성 / 삽입 / 조회)별로 API 요청을 받아 해당 예제를 수행할 수 있습니다.

public static void main(String[] args){  
  long myId = 1;
  String value = "testValue";
  
  // client 객체를 주입해 줍니다.
  MyService myService = new MyService(client);
  
  try {
     // user의 최근 데이터 설정을 위해 user id를 이용하여 b+tree를 생성 및 설정합니다.
    if (!myService.initUserAction(myId)) {
      System.out.println("초기화 실패");
      return;
    }

    for (int i = 1; i <= 10; i++) {
      System.out.println(i + "일차 데이터");
      
      // 1일에 한번 데이터를 저장합니다.
      boolean saved = myService.save(myId, value + i);
      if (!saved) {
        System.out.println("저장 실패");
        return;
      }
      
      // 최근 내역 데이터를 조회합니다.
      List<String> latestData = myService.getLatestData(myId);
      if (latestData == null) {
        System.out.println("조회 실패");
        return;
      }

      // 1일(1초) 경과를 위해 사용합니다.
      Thread.sleep(1000);
    }
  } catch (ExecutionException | InterruptedException | IllegalStateException e) {
      System.out.println("test 실패");
  } 
}

코드를 실행한 결과는 아래와 같습니다. 최근 내역부터 데이터가 출력됩니다.

1일차 데이터
testValue1
2일차 데이터
testValue2
testValue1
3일차 데이터
testValue3
testValue2
testValue1
4일차 데이터
testValue4
testValue3
testValue2
testValue1
5일차 데이터
testValue5
testValue4
testValue3
testValue2
testValue1
6일차 데이터
testValue6
testValue5
testValue4
testValue3
testValue2
testValue1
7일차 데이터
testValue7
testValue6
testValue5
testValue4
testValue3
testValue2
8일차 데이터
testValue8
testValue7
testValue6
testValue5
testValue4
testValue3
9일차 데이터
testValue9
testValue8
testValue7
testValue6
testValue5
testValue4
10일차 데이터
testValue10
testValue9
testValue8
testValue7
testValue6
testValue5

결과를 살펴보면, 가장 최근 데이터를 기준으로 5일(예제 코드에서는 5초) 내 데이터를 조회할 수 있습니다.

사용자가 직접 스케줄링 작업을 통해 유저들의 최근 데이터를 삭제를 수행하지 않고, maxbkeyrange 설정을 통해 캐시에서 자동으로 편리하게 최근 데이터를 유지할 수 있습니다.

마무리

지금까지 Arcus에서 제공하는 b+tree 유형에 대한 maxbkeyrange 속성을 알아보고 응용 사례까지 코드로 구현해 보았습니다. DB의 부담으로 최근 내역 데이터를 캐시에 저장하여 사용할 때, 최근 내역 데이터를 주기적으로 관리하기 위해서는 관리 대상인 아이템의 key 목록이 필요합니다. 하지만 maxbkeyrange를 설정하여 b+tree에 최근 내역 데이터를 저장하여 관리한다면, 관리 대상 아이템의 key 목록 없이 자동으로 최근 내역을 유지할 수 있습니다. 이는 key 목록 관리 및 스케줄링 구현이 필요한 복잡한 응용 개발의 부담을 줄일 수 있습니다. 그러므로 현재 최근 내역을 사용하거나, 사용 예정이거나 혹은 시스템 부담으로 인해 서비스를 제공하기 부담스러웠던 개발자분들에게 maxbkeyrange를 이용한 최근 내역 기능을 구현해 보실 것을 추천드립니다.

오늘 설명한 maxbkeyrange는 Arcus에서 제공하는 b+tree의 일부 기능입니다. b+tree 외에도 list, set, map과 같은 collection 유형이 존재하며 이를 다양하게 이용할 수 있습니다. 간단하게 key-value 형태의 저장 / 조회 기능만 이용하여 복잡한 응용 코드를 생산하기보다 Arcus에서 제공하는 고급진 기능들을 통해 간결하게 작성할 수 있습니다.

앞으로도 블로그를 통해 Arcus에서 제공하는 편리한 기능들을 소개하는 시간을 갖도록 하겠습니다. arcus-java-client의 추가적인 기능에 대해 궁금하신 분은 arcus-java-client-doc에서 더 많은 기능들을 살펴보실 수 있습니다. 감사합니다.

0개의 댓글