Guava Table을 활용한 다차원 데이터 모델링

Table의 차원 확장 전략

Guava의 Table은 행과 열의 교차점에 값을 저장하는 2차원 자료구조다. 실무에서는 단순한 2차원 구조로는 부족한 경우가 많다. 예를 들어 시간, 지역, 제품 카테고리, 매출 지표 등 여러 기준으로 데이터를 분석해야 할 때가 있다.

이런 상황에서 Table을 중첩하거나 Map과 결합하여 마치 다차원 배열처럼 활용할 수 있다. 물론 실제로는 2차원 구조를 여러 겹으로 포개어 사용하는 것이다.

중첩 Table로 3차원 데이터 구현

가장 직관적인 방법은 Table의 셀 값으로 또 다른 Table을 담는 것이다. 시계열 데이터를 다루는 예를 살펴보자. 날짜, 지역, 지표명을 각각의 차원으로 삼아 데이터를 조직화할 수 있다.

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class DimensionalTableTest {

    @Test
    void threeDimensionalLookup() {
        // 날짜 × 지역 → (지표명 × 수치)
        Table<String, String, Table<String, Integer>> timeSeries = HashBasedTable.create();

        Table<String, Integer> northMetrics = HashBasedTable.create();
        northMetrics.put("Revenue", 850_000);
        northMetrics.put("Cost", 620_000);

        Table<String, Integer> southMetrics = HashBasedTable.create();
        southMetrics.put("Revenue", 720_000);
        southMetrics.put("Cost", 580_000);

        timeSeries.put("2024-Q1", "North", northMetrics);
        timeSeries.put("2024-Q1", "South", southMetrics);

        // 3차원 점 조회: 2024-Q1, North 지역의 Revenue
        Table<String, Integer> regionResult = timeSeries.get("2024-Q1", "North");
        assertThat(regionResult.get("Revenue")).isEqualTo(850_000);
    }
}

외부 Table의 행키는 날짜, 열키는 지역을 담당한다. 셀에 들어간 내부 Table이 지표명과 수치를 관리한다. 이렇게 하면 세 개의 키로 원하는 값을 정확히 찾아낼 수 있다.

다차원 순회 패턴

중첩 구조의 단점은 탐색 코드가 복잡해진다는 것이다. 깊이 우선 방식으로 각 층을 파고들며 순회해야 한다.

@Test
void iterateWithDepthFirst() {
    Table<String, String, Table<String, Integer>> inventory = buildSampleInventory();

    for (String period : inventory.rowKeySet()) {
        for (String zone : inventory.columnKeySet()) {
            Table<String, Integer> metrics = inventory.get(period, zone);
            if (metrics == null) continue;

            metrics.forEach((metric, amount) -> 
                System.out.printf("%s | %s | %s = %d%n", period, zone, metric, amount)
            );
        }
    }
}

순회 시 주의할 점은 특정 (행, 열) 조합에 값이 없으면 null이 반환될 수 있다는 것이다. 따라서 방어적으로 처리하거나, 데이터를 넣을 때 모든 조합을 채워야 한다.

Map과 결합한 4차원 이상 확장

3차원으로도 부족하다면 Map을 한 겉 더 씌우면 된다. 예를 들어 연도별로 Table을 분리하여 관리하는 방식이다.

@Test
void fourDimensionalWithMapWrapper() {
    // 연도 → (분기 × 지역 → (지표 × 수치))
    Map<Integer, Table<String, String, Table<String, Integer>>> annualRecords = new HashMap<>();

    Table<String, Integer> q1North = HashBasedTable.create();
    q1North.put("NetProfit", 230_000);

    Table<String, String, Table<String, Integer>> q1Table = HashBasedTable.create();
    q1Table.put("Q1", "North", q1North);

    annualRecords.put(2024, q1Table);

    // 4차원 접근: 2024년 Q1 북부 NetProfit
    Integer profit = annualRecords.get(2024)
                                 .get("Q1", "North")
                                 .get("NetProfit");

    assertThat(profit).isEqualTo(230_000);
}

차원이 늘어날수록 타입 선언이 복잡해지는 것이 단점이다. 실무에서는 별도의 래퍼 클래스를 만들어 타입 안전성을 확보하고, 잡한 중첩 타입을 숨기는 것이 좋다.

구조 설계 시 고려사항

무작정 중첩만 하면 오히려 코드 가독성이 떨어진다. 다음 원칙을 지키면 유지보수성을 높일 수 있다.

  • 의미 있는 타입 별칭 사용: Map<String, Table<...>> 대신 전용 클래스나 레코드로 포장
  • Null 안전성 확보: get() 호출 시 Optional이나 기본값 패턴 적용
  • 불변성 고려: 데이터 생성 후 변경이 없다면 ImmutableTable 활용
  • 캐싱 최적화: 빈번한 조회 패턴을 분석해 적절한 인덱스 구조 설계

Guava Table은 본질적으로 2차원이지만, 적절한 조합과 추상화를 통해 복잡한 다차원 데이터 모델을 우아하게 표현할 수 있다.

태그: Guava java Table collections Multidimensional Data

8월 5일 09:55에 게시됨