Guava를 활용한 데이터 파이프라인 구축

데이터를 수집하고 가공하는 과정에서 반복적인 변환 작업은 코드의 복잡도를 높이는 주요 원인입니다. Guava 라이브러리는 이러한 파이프라인 처리를 위한 다양한 유틸리티를 제공하여, 선언적이고 읽기 쉬운 코드 작성을 지원합니다.

FluentIterable로 구성하는 지연 평가 체인

컬렉션에 대한 연산을 지연 실행으로 엮어나갈 때 FluentIterable은 훌륭한 선택지가 됩니다. 실제 소비가 발생하기 전까지 중간 결과물을 메모리에 올리지 않으므로 대용량 데이터셋에서도 효율적으로 동작합니다.

List<String> rawData = Arrays.asList("kim", "park", null, "lee", "choi", "");

List<String> processed = FluentIterable.from(rawData)
    .skipNulls()
    .filter(s -> s.length() >= 3)
    .transform(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
    .toList();

// 결과: ["Kim", "Park", "Lee", "Choi"]

위 예시에서는 네 가지 연산이 순차적으로 적용됩니다. 각 단계는 이전 단계의 결과를 기반으로 동작하며, 메서드 체이닝을 통해 의도가 명확하게 드러납니다.

Iterables 유틸리티의 실전 활용

Iterables 클래스는 이미 존재하는 Iterable 인스턴스에 기능을 덧입히는 방식으로 동작합니다. 기존 코드베이스와의 통합이 필요할 때 특히 유용합니다.

Iterable<Integer> source = () -> IntStream.rangeClosed(1, 100).iterator();

// 첫 10개 요소만 추출
Iterable<Integer> head = Iterables.limit(source, 10);

// 조건을 만족하는 마지막 요소 탐색
Optional<Integer> lastEven = Iterables.tryFind(
    Iterables.filter(source, n -> n % 2 == 0),
    n -> n > 80
);

Iterables.partition()은 배치 처리가 필요한 상황에서 빛을 발합니다. 대량의 데이터를 일정 크기의 덩어리로 나누어 순회할 수 있습니다.

Iterable<List<Integer>> batches = Iterables.partition(source, 25);
for (List<Integer> batch : batches) {
    processBatch(batch); // 한 번에 25개씩 처리
}

Joiner의 정교한 문자열 조립

단순한 문자열 결합을 넘어, Joiner는 다양한 엣지 케이스를 내장된 정책으로 처리합니다.

Map<String, Integer> scores = ImmutableMap.of(
    "alice", 95,
    "bob", 87,
    "charlie", 92
);

// 키-값 쌍을 특정 형식으로 직렬화
String report = Joiner.on("; ")
    .withKeyValueSeparator("=")
    .useForNull("N/A")
    .join(scores);

// 결과: "alice=95; bob=87; charlie=92"

복잡한 객체의 필드를 추출하여 결합하는 경우, Function을 활용한 변환도 가능합니다.

class Product {
    final String name;
    final BigDecimal price;
    // 생성자 생략
}

List<Product> cart = /* ... */;

String priceList = Joiner.on(" | ")
    .join(Iterables.transform(cart, p -> p.name + ":" + p.price));

Splitter의 역방향 파싱 전략

정규표현식 기반 분할은 예상치 못한 동작을 초래할 수 있습니다. Splitter는 명시적인 빌더 패턴으로 이러한 위험을 줄여줍니다.

String logEntry = "2024-01-15 09:23:17,ERROR,Connection timeout,retry=3";

List<String> fields = Splitter.on(',')
    .trimResults()
    .omitEmptyStrings()
    .limit(3)  // 마지막 필드는 그대로 보존
    .splitToList(logEntry);

// 결과: ["2024-01-15 09:23:17", "ERROR", "Connection timeout,retry=3"]

고정 길이 기반 분할도 지원합니다. 위치 기반 파싱이 필요한 레거시 포맷을 다룰 때 유용합니다.

String fixedWidth = "20240115092317001ABC1234567890";

List<String> tokens = Splitter.fixedLength(4)
    .splitToList(fixedWidth);
// ["2024", "0115", "0923", "1700", "1ABC", "1234", "5678", "90"]

스트림과의 상호운용

Java 8 이후의 Stream API와 Guava 유틸리티를 조합하면 각자의 강점을 극대화할 수 있습니다. Guava는 불변 컬렉션 생성과 특화된 분할/결합에, Stream은 렬 처리와 무한 스트림에 강점을 보입니다.

String raw = "alpha,,beta,,gamma,,delta";

ImmutableList<String> distinctSorted = Splitter.on(',')
    .omitEmptyStrings()
    .splitToStream(raw)  // Guava 28.0+ 
    .map(String::toUpperCase)
    .distinct()
    .sorted()
    .collect(ImmutableList.toImmutableList());

// 결과: ["ALPHA", "BETA", "DELTA", "GAMMA"]

Guava의 불변 컬렉션을 수집 단계에서 활용하면, 이후 연산에서의 방어적 복사 비용을 절감할 수 있습니다.

성능 고려사항

대용량 데이터 처리 시 다음 사항을 염두에 두세요:

  • FluentIterable은 지연 평가되지만, toList()toSet() 호출 시 전체 데이터를 구체화합니다
  • Splitter는 입력 크기에 비례하는 임시 객체를 생성하므로, 매우 큰 입력에는 splitToStream()을 고려하세요
  • Joiner는 내부적으로 StringBuilder를 효율적으로 활용하며, 대량의 문자열 결합에서 수동 루프보다 우수합니다

이러한 도구들을 상황에 맞게 선택하고 조합함으로써, 데이터 변환 로직의 명확성과 실행 효율을 동시에 달성할 수 있습니다.

태그: Guava FluentIterable Java Stream Splitter Joiner

6월 30일 18:46에 게시됨