티나 개발 노트
.NET Framework 4.8 NuGet Trino.Ado 1.1.0 Trino · Hive 카탈로그

C#에서 Trino(Hive 카탈로그) 조회하기

.NET Framework 콘솔·배치 프로그램에서 NuGet Trino.Ado로 Trino에 붙어 Hive 테이블을 조회하고 적재하는 예제입니다. 코드보다 중요한 건 실제로 막혔던 지점이라, 왜 이렇게 짰는지를 함께 적었습니다.

0이 글을 쓴 이유

Hive 테이블을 HiveServer2(ODBC)가 아니라 Trino로 조회하는 환경에서, 기존 .NET Framework 프로그램이 그대로 붙어야 했습니다. 그런데 막상 해 보면 자료가 거의 없고, 그대로 따라 하면 바로 오류가 나는 지점이 여러 곳 있었습니다.

아래 코드는 이 문제들을 하나씩 피해 가도록 정리한 것입니다. 같은 상황이라면 그대로 가져다 쓰면 됩니다.

🔀

HiveServer2에 ODBC로 직접 붙는 방식은 C#에서 Hive 조회·적재하기 (ODBC)에 따로 정리해 두었습니다. 두 방식의 차이와 고르는 기준은 Trino.Ado 소개와 ODBC 연결과의 차이를 보세요.

1준비

  1. 대상 프레임워크: .NET Framework 4.8 (최소 4.7.2)
    Trino.Ado는 .NET Standard 2.0 패키지이고, 함께 들어오는 System.Text.Json 8.x가 4.6.2 미만을 지원하지 않습니다. 4.6.1은 피하는 것이 안전합니다.
  2. NuGet 설치 (패키지 관리자 콘솔). Trino.Core도 자동으로 같이 설치됩니다.
패키지 관리자 콘솔
Install-Package Trino.Ado -Version 1.1.0
  1. 참조 추가: System.Configuration (App.config 읽기용)
📦

공식 저장소 trino-csharp-client의 Trino.Data.ADO는 NuGet에 없습니다. Trino.Ado는 같은 코드를 포크해서 NuGet에 배포하는 패키지입니다.

2App.config

접속 정보는 코드가 아니라 App.config에 둡니다. 서버마다 파일만 바꾸면 되고 다시 빌드할 필요가 없습니다. 아래 서버 주소(192.0.2.10)는 예시이니 실제 Trino 코디네이터 주소로 바꾸세요.

App.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
  </startup>
  <appSettings>
    <!-- Trino 코디네이터 주소. https 면 SSL, http 면 평문 -->
    <add key="TrinoServer" value="http://192.0.2.10:8080" />
    <add key="TrinoCatalog" value="hive" />
    <add key="TrinoSchema" value="default" />
    <add key="TrinoUser" value="etl_user" />
    <!-- 비밀번호(LDAP/패스워드 인증)가 없으면 비워 두세요. Trino 는 비밀번호 인증을 https 에서만 허용합니다 -->
    <add key="TrinoPassword" value="" />
    <!-- 쿼리 타임아웃(초). 대용량 INSERT 는 넉넉하게 -->
    <add key="TrinoTimeoutSeconds" value="3600" />
    <!-- 사내 자체 서명 인증서를 쓰는 경우만 true -->
    <add key="TrinoAllowSelfSignedCert" value="false" />
  </appSettings>
  <!-- Windows(IE) 프록시를 타면 401 Access Denied 가 납니다. Trino 서버는 프록시를 거치지 않게 -->
  <system.net>
    <defaultProxy>
      <bypasslist>
        <add address="192\.0\.2\.10" />
      </bypasslist>
    </defaultProxy>
  </system.net>
</configuration>
💡

bypasslist의 address는 정규식입니다. 그래서 IP의 점(.)을 \.로 씁니다. 서버 이름을 쓴다면 trino-server01처럼 그대로 적으면 됩니다.

3TrinoJob.cs

연결 만들기, 값 변환, 조회 2가지(한 줄씩 / DataTable), 실행 1가지를 한 클래스에 모았습니다.

TrinoJob.cs
// TrinoJob.cs  (.NET Framework 4.8, NuGet: Trino.Ado)
using System;
using System.Collections;
using System.Configuration;   // 참조 추가: System.Configuration
using System.Data;
using System.Globalization;
using System.Linq;
using Trino.Ado.Server;       // TrinoConnection, TrinoCommand, TrinoConnectionProperties
using Trino.Core.Auth;        // BasicAuth (Trino.Core 패키지, Trino.Ado 설치 시 같이 설치됨)

// 연결 설정은 App.config 에서 읽습니다.
public static class TrinoJob
{
    static string Setting(string key)
    {
        return ConfigurationManager.AppSettings[key] ?? "";
    }

    public static TrinoConnection CreateConnection()
    {
        var props = new TrinoConnectionProperties
        {
            Server = new Uri(Setting("TrinoServer")),   // Host/Port/EnableSsl 이 여기서 같이 설정됨
            Catalog = Setting("TrinoCatalog"),
            Schema = Setting("TrinoSchema"),
            User = Setting("TrinoUser"),
            Timeout = TimeSpan.FromSeconds(int.Parse(Setting("TrinoTimeoutSeconds"))),
            AllowSelfSignedServerCert = Setting("TrinoAllowSelfSignedCert") == "true",
            CompressionDisabled = true                  // gzip 응답 처리 중 EOF 오류 방지
        };

        string password = Setting("TrinoPassword");
        if (password.Length > 0)
        {
            props.Auth = new BasicAuth { User = props.User, Password = password };
        }

        var conn = new TrinoConnection(props);
        conn.Open();
        return conn;
    }

    // Trino.Ado 가 돌려주는 값 중 일부는 화면/DataTable 에 그대로 쓰기 어렵습니다.
    //   char -> char[], decimal -> TrinoBigDecimal, array -> List<object>, map -> Dictionary,
    //   timestamp with time zone -> DateTimeOffset
    // 여기서 일반 .NET 값(string, decimal, DateTime 등)으로 바꿉니다. null 은 DBNull.
    public static object ToNetValue(object v)
    {
        if (v == null || v is DBNull) return DBNull.Value;
        if (v is string || v is byte[]) return v;
        if (v is char[]) return new string((char[])v);
        if (v is DateTimeOffset) return ((DateTimeOffset)v).DateTime;
        if (v is DateTime || v is decimal || v is TimeSpan || v is Guid || v.GetType().IsPrimitive) return v;

        if (v is IDictionary)
        {
            var d = (IDictionary)v;
            return "{" + string.Join(", ", d.Keys.Cast<object>()
                .Select(k => ToText(k) + "=" + ToText(d[k]))) + "}";
        }
        if (v is IEnumerable)
        {
            return "[" + string.Join(", ", ((IEnumerable)v).Cast<object>().Select(ToText)) + "]";
        }

        // TrinoBigDecimal 등: 문자열로 바꾼 뒤 decimal 로, 범위를 넘으면 문자열 그대로
        string s = v.ToString();
        decimal num;
        if (decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out num)) return num;
        return s;
    }

    public static string ToText(object v)
    {
        object x = ToNetValue(v);
        return x is DBNull ? "NULL" : Convert.ToString(x, CultureInfo.InvariantCulture);
    }

    // 1) SELECT -> DataReader 로 한 줄씩 읽기
    public static void PrintRows(string sql)
    {
        using (var conn = CreateConnection())
        using (var cmd = new TrinoCommand(conn, sql))
        using (IDataReader reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                var values = new object[reader.FieldCount];
                reader.GetValues(values);
                Console.WriteLine(string.Join("\t", values.Select(ToText)));
            }
        }
    }

    // 2) SELECT -> DataTable (그리드 바인딩 등)
    //    DataTable.Load(reader) 는 Trino.Ado 와 타입이 맞지 않아 오류가 나므로 직접 채웁니다.
    //    컬럼 타입은 object 라서 숫자/날짜는 원래 타입 그대로 들어갑니다.
    public static DataTable Query(string sql)
    {
        using (var conn = CreateConnection())
        using (var cmd = new TrinoCommand(conn, sql))
        using (IDataReader reader = cmd.ExecuteReader())
        {
            var table = new DataTable();
            bool first = true;
            while (reader.Read())
            {
                if (first)
                {
                    for (int i = 0; i < reader.FieldCount; i++)
                    {
                        string name = reader.GetName(i);
                        if (name.Length == 0 || table.Columns.Contains(name)) name = name + "_" + i;  // 같은 이름 컬럼(조인 등) 대비
                        table.Columns.Add(name, typeof(object));
                    }
                    first = false;
                }
                var row = table.NewRow();
                for (int i = 0; i < reader.FieldCount; i++)
                {
                    row[i] = ToNetValue(reader.GetValue(i));
                }
                table.Rows.Add(row);
            }
            return table;   // 결과가 0행이면 컬럼 없는 DataTable
        }
    }

    // 3) INSERT / CREATE TABLE AS 등 실행
    //    반환값은 Trino 가 처리한 행 수(ProcessedRows)라 실제 삽입 행 수와 다를 수 있습니다.
    public static int Execute(string sql)
    {
        using (var conn = CreateConnection())
        using (var cmd = new TrinoCommand(conn, sql))
        {
            return cmd.ExecuteNonQuery();
        }
    }
}

메서드 설명

CreateConnection

App.config 값으로 TrinoConnectionProperties를 채워 연결을 엽니다. Server에 URL 하나만 주면 호스트·포트·SSL 여부가 같이 정해집니다.

ToNetValue / ToText

Trino 전용 타입을 string·decimal·DateTime 같은 일반 .NET 값으로 바꿉니다. array는 [a, b], map은 {k=v} 문자열이 되고, null은 DBNull(출력할 때는 NULL)이 됩니다.

PrintRows

결과를 한 줄씩 읽어 탭으로 구분해 출력합니다. 컬럼 구성을 몰라도 동작해서 연결 확인용으로 좋습니다.

Query

결과를 DataTable로 받아 그리드 바인딩 등에 씁니다. 첫 행을 읽을 때 컬럼을 만들고, 이름이 겹치면 _번호를 붙입니다.

Execute

INSERT, CREATE TABLE AS처럼 결과 행이 없는 문장을 실행합니다.

4Program.cs

세 가지 사용법을 순서대로 실행하는 콘솔 예제입니다. 테이블 이름(sample_table, target_table)은 실제 이름으로 바꾸세요.

Program.cs
// Program.cs  (.NET Framework 4.8 콘솔, NuGet: Trino.Ado)
using System;
using System.Data;

class Program
{
    static void Main()
    {
        // 1) 한 줄씩 출력 — 연결 확인용
        TrinoJob.PrintRows("SELECT * FROM sample_table LIMIT 10");

        // 2) DataTable 로 받기
        DataTable dt = TrinoJob.Query(
            "SELECT dt, COUNT(*) AS cnt FROM sample_table GROUP BY dt ORDER BY dt");
        foreach (DataRow r in dt.Rows)
            Console.WriteLine(r["dt"] + "\t" + r["cnt"]);

        // 3) 적재
        //    Trino 에는 INSERT OVERWRITE 문법이 없습니다. 파티션 덮어쓰기가 필요하면
        //    TrinoJob.CreateConnection() 의 props 에 아래 줄을 추가하세요.
        //    props.SessionProperties = new Dictionary<string, string>
        //        { { "hive.insert_existing_partitions_behavior", "OVERWRITE" } };
        int n = TrinoJob.Execute(
            "INSERT INTO target_table SELECT * FROM sample_table WHERE dt = '2026-10-27'");
        Console.WriteLine("처리 행 수=" + n);

        Console.WriteLine("완료. 아무 키나 누르세요.");
        Console.ReadKey();
    }
}
⚠️

세션 속성을 추가할 때는 TrinoJob.cs 맨 위에 using System.Collections.Generic;도 함께 넣어야 합니다. 마지막 Console.ReadKey()는 창이 바로 닫히지 않게 하는 용도이니, 작업 스케줄러에서 돌릴 때는 지우세요.

5주의사항 · 자주 나는 문제

401 Access Denied

Windows 프록시 문제입니다. .NET은 기본으로 Windows(IE) 프록시 설정을 따라가는데, 사내 프록시가 Trino 요청을 막으면서 401이 납니다. App.config의 <system.net> → bypasslist에 Trino 서버 주소가 들어 있는지 확인하세요.

DataTable.Load(reader)에서 null 오류

Trino.Ado의 DataReader와 DataTable.Load가 기대하는 타입 정보가 맞지 않아 납니다. TrinoJob.Query()처럼 직접 컬럼을 만들고 행을 채우세요.

char · decimal · array · map 값이 이상하게 나옴

char는 char[], decimal은 TrinoBigDecimal, array는 List<object>, map은 Dictionary, 시간대 있는 timestamp는 DateTimeOffset으로 넘어옵니다. TrinoJob.ToNetValue()로 일반 .NET 값으로 바꿔서 쓰세요.

응답을 읽다가 EOF 오류

gzip 압축 응답을 처리하다 끊기는 경우입니다. CompressionDisabled = true로 압축을 끄면 해결됩니다(예제 코드에 이미 들어 있음).

INSERT OVERWRITE 문법 오류

Trino에는 INSERT OVERWRITE가 없습니다. 같은 파티션을 다시 적재하려면 세션 속성 hive.insert_existing_partitions_behavior = 'OVERWRITE'를 설정한 뒤 일반 INSERT INTO를 실행하세요(Program.cs 주석 참고).

ExecuteNonQuery 반환값이 실제 건수와 다름

반환값은 Trino가 처리한 행 수(ProcessedRows)라서 실제 삽입 행 수와 다를 수 있습니다. 적재 건수를 검증하려면 적재 후 SELECT COUNT(*)로 따로 확인하세요.

비밀번호를 넣었더니 연결이 안 됨

Trino는 비밀번호(LDAP·패스워드) 인증을 https에서만 허용합니다. TrinoServer가 http://라면 비밀번호를 비워 두거나, https 주소를 쓰세요.

트랜잭션 · DataAdapter를 쓰고 싶음

Trino.Ado는 트랜잭션과 DataAdapter를 지원하지 않습니다. 조회는 Query(), 적재는 Execute()로 처리하세요.

6이렇게 짠 이유

선택이유
Trino.Ado 패키지공식 Trino.Data.ADO는 NuGet에 없어서, 같은 코드를 NuGet으로 배포하는 포크를 씀
.NET Framework 4.8의존 패키지 System.Text.Json 8.x가 4.6.2 미만을 지원하지 않음 (최소 4.7.2)
설정을 App.config로서버마다 주소·계정이 달라도 다시 빌드하지 않고 파일만 수정
프록시 예외 (bypasslist)Windows 프록시를 타면 401 Access Denied가 남
CompressionDisabled = truegzip 응답 처리 중 EOF 오류 방지
비밀번호 있을 때만 BasicAuthTrino는 비밀번호 인증을 https에서만 허용, http 환경에서는 사용자 이름만 보냄
DataTable을 직접 채움DataTable.Load(reader)가 타입 불일치로 null 오류
컬럼 타입 object숫자·날짜를 문자열로 바꾸지 않고 원래 타입 그대로 담기 위해
ToNetValue()char·decimal·array·map·timestamptz가 Trino 전용 타입으로 넘어오기 때문
INSERT OVERWRITE 대신 세션 속성Trino에 INSERT OVERWRITE 문법이 없음
호출마다 연결 열고 닫기트랜잭션이 없어 연결을 오래 들고 있을 이유가 없고, 배치에서 단순·안전
🐝 HiveServer2에 직접 붙는다면C#에서 Hive 조회·적재하기 (ODBC) →