GenericUDAF 작성 튜토리얼

GenericUDAF 작성 튜토리얼

Hive의 사용자 정의 집계 함수(UDAF)는 고급 데이터 처리를 Hive에 통합하는 훌륭한 방법이에요. simple UDAF와 generic UDAF 두 종류가 있으며, 이 튜토리얼은 generic UDAF로 histogram() 함수를 직접 만들어 보면서 개념을 익혀요.

출처: 문서

본문

사용자 정의 집계 함수(UDAF)는 고급 데이터 처리를 Hive에 통합하는 훌륭한 방법입니다. Hive는 simple UDAF와 generic UDAF 두 가지를 허용합니다. 이름에서 알 수 있듯 simple UDAF는 작성하기가 비교적 단순하지만 Java 리플렉션을 사용해 성능 손실이 있고, 가변 길이 인자 목록 같은 기능을 지원하지 않습니다. Generic UDAF는 이런 모든 기능을 허용하지만, Simple UDAF만큼 직관적으로 작성하기 쉽지는 않습니다.

이 튜토리얼은 고정된 사용자 지정 bin 개수로 히스토그램을 계산하며, 상수 메모리와 입력 크기에 선형인 시간을 사용하는 histogram() UDAF의 개발 과정을 안내합니다. 복잡한 반환 타입(구조체의 배열)과 입력에 대한 타입 검사 같은 Generic UDAF의 여러 기능을 보여줍니다. 독자가 궁극적으로 Hive 오픈소스 프로젝트에 제출할 UDAF를 작성하려고 한다고 가정하기 때문에, Hive의 함수 레지스트리 수정과 .q 테스트 작성 같은 단계도 포함합니다. 그냥 UDAF를 작성해서 로컬에서 디버그·배포하고 싶다면 이 페이지를 보세요.

참고: 이 튜토리얼에서는 histogram() 함수를 만드는 과정을 다룹니다. Hive 0.6.0 릴리스부터 이 함수는 내장 함수 histogram_numeric()으로 등장합니다.

사전 준비

Hive 디렉터리에서 svn up을 실행해 최신 Hive trunk를 확보하세요. Hive 다운로드·설정에 대한 자세한 지침은 Getting Started에 있습니다. 로컬 Hive 사본은 Hive 루트 디렉터리에서 build/dist/bin/hive를 실행해 동작해야 하며, 테스트하려는 UDAF를 위해 로컬 인스턴스에 몇몇 데이터 테이블을 로드해 두어야 합니다. 이 예제에서는 normal이라는 테이블이 표준 정규분포에서 추출된 많은 난수를 담은 val이라는 double 컬럼 하나를 가진다고 가정합니다.

우리가 편집하거나 만들 파일은 Hive 루트 기준으로 다음과 같습니다:

파일 용도
ql/src/java/org/apache/hadoop/hive/ql/udf/generic/GenericUDAFHistogram.java 여러분이 만들 메인 소스 파일
ql/src/java/org/apache/hadoop/hive/ql/exec/FunctionRegistry.java histogram() UDAF를 Hive 내장 함수 목록에 등록하도록 수정할 함수 레지스트리 소스
ql/src/test/queries/clientpositive/udaf_histogram.q 샘플 데이터로 histogram()을 테스트할 샘플 쿼리 파일 (작성 필요)
ql/src/test/results/clientpositive/udaf_histogram.q.out 샘플 쿼리의 기대 출력. 이후 단계에서 ant가 생성
ql/src/test/results/clientpositive/show_functions.q.out SHOW FUNCTIONS Hive 쿼리의 기대 출력. 새 histogram() 함수를 추가하므로 이 출력이 바뀝니다. 이후 단계에서 ant가 수정

소스 작성

이 절은 나만의 generic UDAF를 구현하는 방법의 높은 수준의 개요를 제공합니다. 구체적인 예는 ql/src/java/org/apache/hadoop/hive/ql/udf/generic/ 디렉터리의 기존 UDAF 소스들을 보세요.

개요

높은 수준에서 Generic UDAF 구현은 두 부분으로 나뉩니다. 첫 번째는 resolver 클래스 작성이고, 두 번째는 evaluator 클래스 생성입니다. resolver는 타입 검사와 연산자 오버로딩(원한다면)을 처리하고, Hive가 주어진 인자 타입 집합에 대해 올바른 evaluator 클래스를 찾도록 도와줍니다. evaluator 클래스는 실제로 UDAF 로직을 구현합니다. 일반적으로 최상위 UDAF 클래스는 추상 기본 클래스 org.apache.hadoop.hive.ql.udf.GenericUDAFResolver2 를 확장하고, evaluator 클래스는 정적 내부 클래스로 작성됩니다.

resolver 작성

resolver는 UDAF 쿼리에 대한 타입 검사와 연산자 오버로딩을 처리합니다. 예를 들어 타입 검사는 사용자가 integer 가 기대되는 곳에 double 표현식을 넘기지 않도록 하며, 연산자 오버로딩은 인자 타입에 따라 다른 UDAF 로직을 가질 수 있게 합니다.

resolver 클래스는 org.apache.hadoop.hive.ql.udf.GenericUDAFResolver2 를 확장해야 합니다(호환성 정보는 #Resolver Interface Evolution 참고). UDAF를 Hive의 향후 인터페이스 변경으로부터 격리하려면 AbstractGenericUDAFResolver 기본 클래스를 확장할 것을 권장합니다.

필요한 import 는 기존 UDAF 중 하나를 보세요.

public class GenericUDAFHistogramNumeric extends AbstractGenericUDAFResolver {
  static final Log LOG = LogFactory.getLog(GenericUDAFHistogramNumeric.class.getName());

  @Override
  public GenericUDAFEvaluator getEvaluator(GenericUDAFParameterInfo info) throws SemanticException {
    // Type-checking goes here!

    return new GenericUDAFHistogramNumericEvaluator();
  }

  public static class GenericUDAFHistogramNumericEvaluator extends GenericUDAFEvaluator {
    // UDAF logic goes here!
  }
}

위 코드는 UDAF의 기본 골격을 보여줍니다. 첫 줄은 Hive 로그에 경고와 오류를 쓰는 데 사용할 Log 객체를 설정합니다. GenericUDAFResolver 클래스에는 getEvaluator 라는 단일 오버라이드 메서드가 있으며, 이는 UDAF가 어떻게 호출되는지에 대한 정보를 받습니다. 가장 관심 있는 것은 info.getParameters()로, 호출 매개변수의 SQL 타입에 대응하는 타입 정보 객체 배열을 제공합니다. histogram UDAF의 경우 두 개의 매개변수를 원합니다: 히스토그램을 계산할 숫자 컬럼과 요청된 히스토그램 bin 개수입니다. 가장 먼저 할 일은 정확히 두 개의 매개변수가 있는지 확인하는 것입니다(아래 3-6행). 그런 다음 첫 번째 매개변수가 예를 들어 array나 map이 아니라 기본(primitive) 타입인지 확인합니다(9-13행). 그러나 기본 타입 컬럼이길 원할 뿐 아니라 숫자 타입이길도 원합니다. 즉 STRING 타입이 주어지면 예외를 던져야 합니다(14-28행). BOOLEAN은 "histogram" 추정 문제가 단순 COUNT() 쿼리로 풀릴 수 있기 때문에 제외합니다. 30-41행은 histogram() UDAF의 두 번째 매개변수인 히스토그램 bin 개수에 대한 유사한 타입 검사를 보여줍니다. 이 경우 히스토그램 bin 개수가 정수여야 한다고 요구합니다.

  public GenericUDAFEvaluator getEvaluator(GenericUDAFParameterInfo info) throws SemanticException {
    TypeInfo [] parameters = info.getParameters();
    if (parameters.length != 2) {
      throw new UDFArgumentTypeException(parameters.length - 1,
          "Please specify exactly two arguments.");
    }
    
    // validate the first parameter, which is the expression to compute over
    if (parameters[0].getCategory() != ObjectInspector.Category.PRIMITIVE) {
      throw new UDFArgumentTypeException(0,
          "Only primitive type arguments are accepted but "
          + parameters[0].getTypeName() + " was passed as parameter 1.");
    }
    switch (((PrimitiveTypeInfo) parameters[0]).getPrimitiveCategory()) {
    case BYTE:
    case SHORT:
    case INT:
    case LONG:
    case FLOAT:
    case DOUBLE:
      break;
    case STRING:
    case BOOLEAN:
    default:
      throw new UDFArgumentTypeException(0,
          "Only numeric type arguments are accepted but "
          + parameters[0].getTypeName() + " was passed as parameter 1.");
    }

    // validate the second parameter, which is the number of histogram bins
    if (parameters[1].getCategory() != ObjectInspector.Category.PRIMITIVE) {
      throw new UDFArgumentTypeException(1,
          "Only primitive type arguments are accepted but "
          + parameters[1].getTypeName() + " was passed as parameter 2.");
    }
    if( ((PrimitiveTypeInfo) parameters[1]).getPrimitiveCategory()
        != PrimitiveObjectInspector.PrimitiveCategory.INT) {
      throw new UDFArgumentTypeException(1,
          "Only an integer argument is accepted as parameter 2, but "
          + parameters[1].getTypeName() + " was passed instead.");
    }
    return new GenericUDAFHistogramNumericEvaluator();
  }

여기서 연산자 오버로딩의 한 형태를 구현할 수 있습니다. 정수 전용으로 설계된 알고리즘과 double 데이터 타입용으로 설계된 완전히 다른 두 개의 히스토그램 구성 알고리즘이 있다고 해 봅시다. 그러면 두 개의 별도 Evaluator 내부 클래스를 만들고, 입력 표현식의 타입에 따라 올바른 것을 Resolver 클래스의 반환 값으로 반환하면 됩니다.

주의(CAVEAT): histogram 함수는 SELECT histogram_numeric(age, 30) FROM employees;처럼 사용하도록 되어 있습니다. 즉 직원 나이의 분포를 30개의 히스토그램 bin으로 추정한다는 뜻입니다. 그러나 resolver 클래스 내에서는 두 번째 인자가 상수인지, 값이 여러 개인 정수 컬럼인지 알 방법이 없습니다. 그래서 변태적인 사용자는 SELECT histogram_numeric(age, age) FROM employees;처럼 쓸 수도 있습니다(age가 정수 컬럼이라고 가정). 물론 이는 전혀 말이 안 되지만, 위의 Resolver 타입 검사 코드에서는 올바르게 검증됩니다.

이 문제는 Evaluator에서 처리할 것입니다.

evaluator 작성

모든 evaluator는 추상 기본 클래스 org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator에서 확장해야 합니다. 이 클래스는 확장 클래스가 구현해야 하는 몇 가지 추상 메서드를 제공합니다. 이 메서드들은 UDAF가 따르는 처리 의미론(processing semantics)을 확립합니다. 다음은 Evaluator 클래스의 골격입니다.

  public static class GenericUDAFHistogramNumericEvaluator extends GenericUDAFEvaluator {

    // For PARTIAL1 and COMPLETE: ObjectInspectors for original data
    private PrimitiveObjectInspector inputOI;
    private PrimitiveObjectInspector nbinsOI;

    // For PARTIAL2 and FINAL: ObjectInspectors for partial aggregations (list of doubles)
    private StandardListObjectInspector loi;

    @Override
    public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {
      super.init(m, parameters);
      // return type goes here
    }

    @Override
    public Object terminatePartial(AggregationBuffer agg) throws HiveException {
      // return value goes here
    }

    @Override
    public Object terminate(AggregationBuffer agg) throws HiveException {
      // final return value goes here
    }

    @Override
    public void merge(AggregationBuffer agg, Object partial) throws HiveException {
    }

    @Override
    public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException {
    }

    // Aggregation buffer definition and manipulation methods 
    static class StdAgg implements AggregationBuffer {
    };

    @Override
    public AggregationBuffer getNewAggregationBuffer() throws HiveException {
    }

    @Override
    public void reset(AggregationBuffer agg) throws HiveException {
    }    
  } 

이 모든 함수는 무엇을 할까요? 다음은 각 함수의 간략한 요약으로, (대략) 호출되는 시간순입니다. 집계 계산은 데이터에 대해 임의로 분할 가능해야 한다는 점을 기억하는 것이 매우 중요합니다. 데이터 분할이 완전히 제어 불가능하고 Hive가 처리하는 분할정복 알고리즘을 작성한다고 생각하세요. 더 공식적으로, 입력 행의 어떤 부분 집합이 주어져도 부분 결과(partial result)를 계산할 수 있어야 하고, 부분 결과의 어떤 쌍이든 다른 부분 결과로 병합(merge)할 수 있어야 합니다. 이는 자연스럽게 기존 알고리즘을 많이 이식하기 어렵게 만들지만, 연구자들의 일자리를 상당 기간 보장해 줄 것입니다.

함수 목적
init UDAF evaluator 클래스의 인스턴스를 초기화하기 위해 Hive가 호출
getNewAggregationBuffer 임시 집계 결과를 저장하는 데 사용할 객체 반환
iterate 새 데이터 행을 집계 버퍼로 처리
terminatePartial 현재 집계 내용을 영속화 가능한 방식으로 반환. 여기서 영속화 가능하다는 것은 반환 값이 Java 기본형, 배열, 기본형 래퍼(예: Double), Hadoop Writable, List, Map으로만 구성될 수 있음을 의미. 자체 클래스는 사용하지 말 것(Java.io.Serializable을 구현하더라도). 그렇지 않으면 이상한 오류 또는 (아마 더 나쁜) 잘못된 결과가 나올 수 있음
merge terminatePartial 이 반환한 부분 집계를 현재 집계로 병합
terminate 집계의 최종 결과를 Hive에 반환

histogram() 함수 작성에 대해 다음과 같은 전략이 채택되었습니다.

getNewAggregationBuffer

히스토그램의 집계 버퍼는 히스토그램의 bin 중심과 높이를 나타내는 (x,y) 쌍의 목록입니다. 또한 집계 버퍼는 최대 bin 개수(사용자 지정 매개변수)와 현재 사용 중인 bin 개수라는 두 개의 정수를 저장합니다. 집계 버퍼는 bin 개수가 0인 '준비 안 됨(not ready)' 상태로 초기화됩니다. 이는 Hive가 UDAF에 제공된 상수 매개변수와 테이블의 컬럼을 구분하지 않기 때문입니다. 따라서 첫 번째 iterate() 호출 전까지는 사용자가 히스토그램에 원하는 bin 개수를 알 방법이 없습니다.

iterate

iterate()에서 가장 먼저 하는 일은 집계 버퍼의 히스토그램 객체가 초기화되었는지 확인하는 것입니다. 초기화되지 않았다면 iterate()의 두 번째 인자인 사용자가 요청한 히스토그램 bin 개수를 파싱합니다. 이를 정확히 한 번 수행하고 히스토그램 객체를 초기화합니다. 여기서 오류 검사가 수행됩니다. 사용자가 히스토그램 bin 개수로 음수 또는 0을 제공하면 이 지점에서 HiveException이 던져지고 계산이 종료됩니다.

다음으로 실제 입력 데이터 항목(숫자)을 파싱해 집계 버퍼의 히스토그램 추정에 추가합니다. 히스토그램 구성에 사용된 휴리스틱에 대한 자세한 내용은 GenericUDAFHistogramNumeric.java 파일을 참고하세요.

terminatePartial

현재 히스토그램 근사치는 DoubleWritable 객체 목록으로 직렬화됩니다. 목록의 처음 두 double은 사용자가 지정한 최대 히스토그램 bin 개수와 현재 사용 중인 bin 개수를 나타냅니다. 나머지 항목들은 현재 히스토그램 근사치의 (x,y) 쌍입니다.

merge

이 시점에서 (아마 초기화되지 않은) 히스토그램 추정치가 있고, 이를 행의 다른 부분 집합에서 수행된 다른 추정치와 병합하라는 요청을 받았습니다. 사용자가 지정한 히스토그램 bin 개수가 N 이라면, 현재 휴리스틱은 먼저 두 추정치의 모든 2N bin으로 히스토그램을 만든 다음, N 개의 bin만 남을 때까지 가장 가까운 bin 쌍을 반복적으로 병합합니다.

terminate

histogram() 함수의 최종 반환 타입은 히스토그램 bin 중심과 높이를 나타내는 (x,y) 쌍의 배열입니다. 이를 {{explode()}}해서 별도의 테이블로 만들거나, 스크립트로 파싱해 (예: Gnuplot에) 전달하여 히스토그램을 시각화할 수 있습니다.

함수 레지스트리 수정

UDAF 코드를 작성하고 소스 파일을 ql/src/java/org/apache/hadoop/hive/ql/udf/generic에 배치했다면, 이제 함수 레지스트리를 수정해 새 함수를 Hive의 함수 목록에 통합할 차례입니다. 이는 단순히 ql/src/java/org/apache/hadoop/hive/ql/exec/FunctionRegistry.java를 편집해 UDAF 클래스를 import하고 이름을 등록하는 일입니다.

show functions Hive 호출의 출력을 업데이트하려면 다음 명령을 실행해야 합니다:

ant test -Dtestcase=TestCliDriver -Dqfile=show_functions.q -Doverwrite=true

컴파일 및 실행

ant package
build/dist/bin/hive

테스트 만들기

시스템 수준 테스트는 샘플 데이터에 동작하는 몇몇 샘플 쿼리를 작성하고, 쿼리의 기대 출력을 생성하며, 향후 기대 출력 측면에서 문제가 생기지 않도록 하는 것으로 구성됩니다. 기대 출력이 Hive의 실제 출력과 diff로 비교되므로, 비결정적 알고리즘은 어떤 통계를 계산한 다음 (예를 들어) 가장 중요한 자릿수만 유지해야 합니다.

새 UDAF/UDF의 테스트 케이스를 만드는 간단한 단계:

  1. ql/src/test/queries/clientpositive/udaf_XXXXX.q에 파일을 만듭니다. 여기서 XXXXX는 UDAF 이름입니다.
  2. .q 파일에 기능의 전체 범위와 특수 케이스를 충분히 커버할 만한 쿼리를 몇 개 넣습니다.
  3. 샘플 데이터는 자신의 것을 hive/data/files에 넣고 LOAD DATA LOCAL INPATH...로 로드하거나, 이미 있는 파일 중 하나를 재사용합니다(테이블 이름을 보려면 queries 디렉터리에서 LOAD를 grep).
  4. touch ql/src/test/results/clientpositive/udaf_XXXX.q.out
  5. 다음 명령을 실행해 출력을 .q.out 결과 파일로 생성합니다.
ant test -Dtestcase=TestCliDriver -Dqfile=udaf_XXXXX.q -Doverwrite=true
  1. 테스트가 잘 실행되는지 확인하려면 다음 명령을 실행합니다.
ant test -Dtestcase=TestCliDriver -Dqfile=udaf_XXXXX.q

오픈소스 제출 체크리스트

  • Hive JIRA(https://issues.apache.org/jira/browse/HIVE)에 계정을 만들고, Query Processor 컴포넌트 아래에 새 패치에 대한 이슈를 만듭니다. 토론을 이끌어내고 피드백을 반영합니다.
  • UDAF를 만들고 로컬 Hive 사본에 통합합니다.
  • Hive 루트에서 ant package를 실행해 Hive와 새 UDAF를 컴파일합니다.
  • .q 테스트와 그에 대응하는 .q.out 출력을 만듭니다.
  • 새 함수를 추가한다면 함수 레지스트리를 수정합니다.
  • ant checkstyle을 실행하고 build/checkstyle/checkstyle-errors.html을 살펴봐서 소스 파일이 Sun Java 코딩 규약(100자 줄 길이 예외 포함)을 따르는지 확인합니다.
  • ant test를 실행해 테스트가 통과하는지 확인합니다.
  • svn up을 실행해 메인 저장소와 충돌이 없는지 확인합니다.
  • 만든 새 파일들에 대해 svn add를 실행합니다.
  • .q.q.out 테스트를 추가했는지 확인합니다.
  • 새 기능에 대해 .q 테스트를 실행했는지 확인합니다.
  • 새 UDAF를 추가한다면 show_functions.q.out이 업데이트되었는지 확인합니다. ant test -Dtestcase=TestCliDriver -Dqfile=show_functions.q -Doverwrite=true를 실행해 수행합니다.
  • Hive 루트 디렉터리에서 svn diff > HIVE-NNNN.1.patch를 실행합니다. 여기서 NNNN은 JIRA가 할당한 이슈 번호입니다.
  • 파일을 JIRA 이슈에 첨부하고 댓글 섹션에서 패치를 설명합니다.
  • 댓글에서 코드 리뷰를 요청합니다.
  • 위 단계를 완료한 후 이슈에서 Submit patch 를 클릭합니다.
  • 이슈를 watch 하여 새 댓글을 모니터링하는 것도 좋습니다.

팁, 트릭, 모범 사례

  • Hive는 때때로 예상치 못한 동작을 보일 수 있습니다. 설명되지 않는 예외부터 문자열이 잘못 이중 인용되는 것까지 이상한 게 보인다면 먼저 ant clean을 실행하는 것이 좋습니다.
  • terminatePartial() 호출에서 집계 버퍼를 직렬화할 때, UDAF가 버퍼를 나타내는 데 몇 개의 변수만 사용한다면(average 등) 복잡한 명명된 구조체 대신 double의 목록 등으로 직렬화하는 것을 고려하세요.
  • 가능한 곳이라면 어디든 generics를 강하게 캐스팅하세요.
  • 여러 UDAF의 핵심 기능을 자체 클래스로 추상화하세요. 예로는 histogram_numeric()percentile_approx()가 있으며, 둘 다 동일한 핵심 히스토그램 추정 기능을 사용합니다.
  • terminatePartial/merge 패러다임에 적용할 알고리즘을 찾는 데 막혔다면, 분할정복(divide-and-conquer)과 병렬 알고리즘은 예측 가능하게 좋은 출발점입니다.
  • 테스트가 기대 출력과 실제 출력을 diff하고, 조금이라도 차이가 있으면 실패한다는 것을 기억하세요. 이것이 끔찍하게 실패할 수 있는 예는 ngrams() 같은 UDAF로, 출력이 정렬된 (word,count) 쌍의 목록입니다. 어떤 경우에는 서로 다른 정렬 구현이 같은 count를 가진 단어를 출력의 다른 위치에 놓을 수 있습니다. 출력이 올바르더라도 테스트는 실패합니다. 이런 경우에는 (예를 들어) count만, 또는 sum 같은 count에 대한 적절한 통계를 출력하는 것이 좋습니다.

Resolver 인터페이스 진화

기존 인터페이스 org.apache.hadoop.hive.ql.udf.GenericUDAFResolver는 0.6.0 릴리스에서 deprecated 되었습니다. GenericUDAFResolver와 GenericUDAFResolver2 인터페이스의 핵심 차이는 후자가 evaluator 구현이 DISTINCT 한정자의 존재나 FUNCTION(*) 같은 와일드카드 문법과의 호출 같은 함수 호출에 관한 추가 정보에 접근할 수 있다는 점입니다. deprecated된 GenericUDAFResolver 인터페이스를 구현한 UDAF는 와일드카드 지정에 대한 정보가 없으므로 FUNCTION()FUNCTION(*) 같은 호출을 구분할 수 없습니다. 마찬가지로 이러한 구현은 DISTINCT 한정자의 존재에 대한 정보도 없으므로 FUNCTION(EXPR)FUNCTION(DISTINCT EXPR)을 구분할 수 없습니다.

GenericUDAFResolver2 인터페이스를 구현하는 resolver는 DISTINCT 한정자 존재나 와일드카드 문법 호출에 대한 추가 정보를 제공받지만, 자신에게 중요하지 않다면 이를 완전히 무시할 수 있다는 점에 주의하세요. DISTINCT 값을 계산하기 위한 기본 데이터 필터링은 실제로 evaluator나 resolver가 아니라 Hive의 핵심 쿼리 프로세서가 수행합니다. 이 정보는 검증 목적으로만 resolver에 제공됩니다. AbstractGenericUDAFResolver 기본 클래스는 이전에 작성된 UDAF 구현이 구현을 다시 쓰지 않고 새 resolver 인터페이스로 마이그레이션할 수 있는 쉬운 방법을 제공합니다. GenericUDAFResolver 인터페이스 구현에서 AbstractGenericUDAFResolver 클래스 확장으로의 변경은 비교적 최소이기 때문입니다.(상속 계층의 일부인 구현에는 기본 클래스를 바꾸기가 쉽지 않아 문제가 있을 수 있습니다.)

더 알아보기 (Learn more)

UDAF/UDF 구현과 관련된 Hive 내장 함수의 구현 예시는 ql/src/java/org/apache/hadoop/hive/ql/udf/generic/ 소스를 참고해요. Hive 함수 개발은 이 튜토리얼의 resolver/evaluator 패턴이 골격이 돼요.