Skip to content

Repository files navigation

iris-client-go

Iris (카카오톡 메시지 브릿지)용 Go 클라이언트 라이브러리 SDK입니다.

설치 (Installation)

go get github.com/park285/iris-client-go/v2@latest

현재 지원 major는 /v2입니다. v1 또는 v0 module path에서 올라오는 경우 먼저 v2 마이그레이션 가이드를 따르고, 최근 변경은 CHANGELOG.md미출시와 가장 최근 release 섹션을 확인하십시오. v0.11 마이그레이션은 v0 사이의 이전만 기록한 역사 문서이며 현재 v2 업그레이드 가이드가 아닙니다.

JSON 계약

SDK의 JSON 실행 경로는 Go 1.27 encoding/json/v2를 사용합니다. 디코더는 중복된 object 이름과 잘못된 UTF-8을 거절하고 struct field 이름을 대소문자까지 정확히 일치시킵니다. webhook request와 HTTP response body는 하나의 완전한 JSON 값이어야 하며, media/reaction 응답의 닫힌 경계는 알 수 없는 field도 거절합니다.

typed JSON 응답의 공통 decode 경로는 압축 해제 후 본문 전체를 16 MiB로 제한합니다. 정확히 상한인 응답은 허용하고 초과 확인에 필요한 1 byte까지만 추가로 읽습니다. raw JSON과 strict media/reaction 경로의 기존 1 MiB 상한 및 trailer의 별도 byte/time 제한은 유지합니다. 상한 초과는 errors.Is(err, iris.ErrResponseTooLarge)로 식별합니다. 공통 decode 경로의 POST 초과 응답은 서버 처리 결과를 확인할 수 없어 iris.ErrTransport도 유지하지만 iris.ErrRetryable로 분류하지 않고, idempotency key가 있어도 SDK가 자동 재시도하지 않습니다. 이는 이전에 허용하던 16 MiB 초과 typed 응답의 지원을 제한하는 변경입니다. 정책 근거는 DEC-20260906-sdk-typed-json-response-budget입니다.

v2 기본값에 따라 nil slice와 map은 각각 []{}로 인코딩됩니다. omitempty field는 JSON 관점에서 빈 값일 때 생략되며, 숫자와 bool의 기존 zero-value 생략 계약은 omitzero로 명시되어 있습니다. 공개 payload의 encoding/json.RawMessage 명명 타입은 호환성을 위해 유지하지만, 해당 값을 처리하는 실행 경로는 v2입니다.

제네릭 메서드

타입 파라미터가 필요한 메서드는 Go 1.27 제네릭 메서드를 그대로 사용합니다. internal/client/transportdoGet[T], doSignedJSON[T], postStrictJSON[T], postJSON[T], retryPostJSON[T]가 그렇습니다. 이들을 감싸는 패키지 수준 제네릭 함수 별칭이나 호환 래퍼는 두지 않습니다.

.golangci.ymlexclusions.rulesexclusions.paths에는 항목을 추가하지 않습니다. generatedwarn-unused를 제외하면 비어 있어야 합니다(DEC-20260824-golangci-suppression-must-be-local). 위반은 먼저 코드로 고치고, 진짜 오탐인 경우에만 해당 줄의 //nolint·#nosec 주석에 사유를 적어 억제합니다.

빠른 시작 (Quick Start)

1. 메시지 발송 (Sending Messages)

import "github.com/park285/iris-client-go/v2/iris"

c, err := iris.NewClient()
if err != nil {
    log.Fatalf("클라이언트 초기화 실패: %v", err)
}

// 텍스트 메시지 발송
err = c.SendMessage(ctx, "room-id", "Hello, World!",
    iris.WithThreadID("12345"),
)

// 이미지 메시지 발송 (Base64 인코딩 데이터)
err = c.SendImage(ctx, "room-id", base64Img)

// 마크다운 메시지 발송 (텍스트 공유 카드 형태)
resp, err := c.SendMarkdown(ctx, "room-id", "**bold** text")
status, err := c.GetReplyStatus(ctx, resp.RequestID)

// 일반 파일 발송 (메모리 데이터 예시)
file := iris.NewReplyFileBytes("report.txt", "text/plain", []byte("report body"))
accepted, err := c.SendFile(ctx, "room-id", file,
    iris.WithClientRequestID("report:room-id:2026-07-22"),
)

파일 전송은 기존 iris.Sender를 확장하지 않는 별도 iris.FileSender capability입니다. SDK는 1 byte 이상 30 MiB 이하의 단일 file part를 multipart/form-data로 스트리밍하며 전체 파일이나 multipart body를 메모리에 복제하지 않습니다. caller-owned io.ReaderAt, path helper의 descriptor 수명, deterministic retry와 clientRequestId 계약은 파일 reply 전송을 참조하십시오.

Iris가 structured HTTP error를 반환하면 기존처럼 errors.As(err, &httpErr)*iris.HTTPError를 얻을 수 있습니다. clientRequestId 상태처럼 machine-readable code가 필요한 호출부는 iris.HTTPErrorCode(err)를 사용하십시오. code가 없거나 공개 token 계약을 벗어난 응답이면 빈 문자열을 반환합니다.

409CLIENT_REQUEST_ID_FAILED는 durable queue handoff 이전 실패 — 즉 해당 id로 KakaoTalk 부수효과가 없었음 — 을 뜻하므로, 새 clientRequestId(예: :r1, :r2 generation suffix)로 같은 payload를 유한 세대 재전송하는 것이 안전하며 권장 처리입니다. 같은 409군의 CLIENT_REQUEST_ID_OUTCOME_UNKNOWN/PAYLOAD_MISMATCH/ALREADY_EXISTS와 code 없는 409는 이 보장이 없으므로 재발급 없이 종결하십시오.

2. 웹훅 수신 (Receiving Webhooks)

handler, err := iris.NewWebhookHandler(myMessageHandler,
    webhook.WithMessageDeduplicator(valkeydedup.NewMessageDeduplicator(valkeyClient)),
    webhook.WithNonceStore(valkeydedup.NewNonceStore(valkeyClient)),
)
if err != nil {
    log.Fatalf("웹훅 핸들러 생성 실패: %v", err)
}
defer handler.Close()

http.Handle("/webhook/iris", handler)

WithQueueSize는 ordering scheduler가 소유하는 전체 pending 상한입니다. 내부 실행 pool은 별도 buffered queue를 만들지 않습니다. 종료 budget이 있는 서비스는 handler.CloseContext(ctx)를 사용하면 grace 만료 후 queued callback을 건너뛰고 in-flight handler context를 취소할 수 있습니다. 기존 Close()는 무제한 context를 사용하는 호환 wrapper입니다.

HTTP 200 OK가 메모리 admission이 아니라 durable commit을 의미해야 하는 소비자는 webhook.MessageAdmitter를 구현하고 WithDurableAdmission을 사용합니다. 이 모드에서는 scheduler와 deduplicator를 건너뛰므로 admitter의 저장소 unique key가 idempotency를 소유합니다.

idempotency와 nonce 역할 분리

HMAC nonce와 message dedup은 별도 public contract입니다. 모든 handler constructor는 webhook.WithNonceStore로 명시한 webhook.SetOnceNonceStore가 없으면 오류를 반환합니다. process-local default, message backend 재사용, no-op nonce store는 없습니다.

non-durable handler에서 message dedup이 필요하면 token-bound webhook.MessageDeduplicatorwebhook.WithMessageDeduplicator로 주입합니다. Reserve 오류나 invalid state는 dispatch 전에 503으로 종결하며, 반환된 owner token이 있으면 같은 token으로만 bounded ReleaseReservation을 시도합니다. durable handler는 inbox unique key가 idempotency를 소유하므로 message deduplicator를 주입하지 않습니다.

Valkey consumer는 같은 client를 사용하더라도 역할별 constructor를 호출합니다.

messageDeduplicator := valkeydedup.NewMessageDeduplicator(valkeyClient)
nonceStore := valkeydedup.NewNonceStore(valkeyClient)

예약 TTL은 WithDedupPendingTTL(기본 5s), 확정 TTL은 WithDedupTTL(기본 16m)입니다. EnqueueTimeout + 2 × DedupTimeout < DedupPendingTTL과 sender retry horizon보다 긴 확정 TTL을 유지하십시오.

HMAC v3-only 계약

receiver는 authority-bound signature v3만 검증하며 v2는 unknown version으로 거절합니다. webhooksign.SignRequest는 v3만 생성합니다. 진단값은 V3Validated, UnknownRejected, MalformedRejected의 고정 cardinality 필드만 노출합니다.

handler, err := iris.NewWebhookHandler(inboxRuntime,
    webhook.WithDurableAdmission(inboxRuntime),
    webhook.WithNonceStore(nonceStore),
    webhook.WithAdmitTimeout(200 * time.Millisecond),
    webhook.WithWebhookToken("webhook-secret"),
)

웹훅 송신 테스트나 smoke 도구에서는 X-Iris-Message-Id를 먼저 설정한 뒤 SignRequest로 signature v3 header를 생성합니다. req.Host가 설정된 경우 URL authority와 canonical parity가 없으면 서명하지 않습니다.

req, err := http.NewRequest(http.MethodPost, targetURL, bytes.NewReader(body))
if err != nil {
    return err
}
req.Header.Set(webhook.HeaderIrisMessageID, messageID)
if err := webhooksign.SignRequest(req, secret, body); err != nil {
    return err
}

WithAdmitTimeout은 durable commit의 deadline입니다. 기본값은 30s이며 0 이하를 넘겨도 "무제한"이 아니라 이 기본값으로 정규화됩니다. deadline이 끝나면 다른 admission 오류와 동일하게 HTTP 503 Service Unavailable을 반환하므로 발신자가 재시도할 수 있습니다. 기본값을 발신자의 attempt timeout(125s)보다 훨씬 짧게 잡은 이유는, 저장소가 정체됐을 때 admission goroutine이 요청 context가 끊길 때까지 살아남아 종료(Close)까지 지연시키는 대신 빠르게 503으로 되돌리기 위해서입니다.

3. 관리 API (Admin APIs)

cfg, err := c.GetConfig(ctx)
health, err := c.GetBridgeHealth(ctx)
rooms, err := c.GetRooms(ctx)
members, err := c.GetMembers(ctx, chatID)

// 설정 업데이트 예시
forwardUnmatched := true
_, err = c.UpdateConfig(ctx, "routes", iris.ConfigUpdateRequest{
    CommandRoutePrefixes: map[string][]string{"chatbot": []string{"!", "/"}},
    EventTypeRoutes:      map[string][]string{"events": []string{"member_nickname_updated"}},
    ForwardUnmatchedMessagesToDefault: &forwardUnmatched,
})

// HTTP/3 TLS 인증서 핫 리로드
_, err = c.ReloadH3Certificate(ctx) // POST /admin/cert-reload
  • CAS(Compare-And-Swap) 제어가 필요한 경우 ConfigUpdateRequest.ExpectedRevision을 명시하여 설정 변경 시의 충돌을 방지할 수 있습니다.

4. SSE 이벤트 스트림 (Server-Sent Events)

events, err := c.EventStream(ctx, 0)
for ev := range events {
    fmt.Printf("이벤트 타입: %s, 데이터: %s\n", ev.Event, ev.Data)
}

5. 조회 API (Query APIs)

// 채팅방 요약 정보 조회
summary, err := c.QueryRoomSummary(ctx, chatID)

// 멤버 통계 조회
stats, err := c.QueryMemberStats(ctx, iris.QueryMemberStatsRequest{
    ChatID: chatID,
    Limit:  20,
})

// 최근 스레드 목록 조회
threads, err := c.QueryRecentThreads(ctx, chatID)

// 최근 메시지 내역 조회
msgs, err := c.QueryRecentMessages(ctx, iris.QueryRecentMessagesRequest{
    ChatID:     chatID,
    Limit:      50,
    ChatLogIDs: []string{"500", "300"}, // 선택적 exact filter, 최대 1,000개
})

// 사용자의 최신 이벤트와 다음 older page 조회
events, err := c.GetRoomUserEventsBefore(ctx, chatID, userID, 500, 0)
if len(events) > 0 {
    older, err := c.GetRoomUserEventsBefore(ctx, chatID, userID, 500, events[len(events)-1].ID)
}

for _, msg := range msgs.Messages {
    fmt.Printf("[%d] %s: %s\n", msg.SequenceID, msg.SenderName, msg.Message)
}

6. BotClient 및 RebindingClient

다중 인프라 혹은 동적 환경을 지원하기 위해, 봇 서비스를 위한 최소 인터페이스인 iris.BotClient (Sender + Ping + GetConfig) 및 동적으로 Base URL을 핫스왑할 수 있는 iris.RebindingClient를 제공합니다.

rc := iris.NewRebindingClient(iris.RebindingClientConfig{
    ResolveBaseURL:  func() (string, error) { return readBaseURL() },
    BotToken:        token,
    ResolveInterval: time.Second,      // URL 또는 resolver 오류 snapshot의 최대 유지 시간
    StaleCloseGrace: 30 * time.Second, // 동적 교체된 이전 클라이언트 연결 정리 유예 시간
})
defer rc.Close()

ResolveInterval0이면 각 비동시 호출에서 즉시 Base URL을 다시 확인하는 기존 동작을 유지합니다. 양수이면 interval 안의 호출이 마지막 URL 또는 resolver 오류 snapshot을 공유하고 만료 후 첫 호출이 refresh를 수행합니다. 같은 시점의 동시 호출은 하나의 refresh 결과를 공유합니다.

refresh는 개별 API 호출이 아니라 RebindingClient가 소유합니다. refresh를 시작한 호출의 context가 취소되어도 해당 호출만 먼저 반환하며 진행 중인 refresh는 다른 동시 호출과 cache snapshot을 위해 완료됩니다. Close()는 대기 중인 호출을 즉시 깨우지만 context를 받지 않는 ResolveBaseURL 실행을 강제로 중단할 수는 없으므로 resolver는 유한 시간 안에 반환해야 합니다.


클라이언트 설정 옵션 (Configuration)

c, err := iris.NewClient(
    iris.WithBaseURL("https://iris-host:31001"), // 또는 IRIS_BASE_URL 환경변수 사용
    iris.WithBotToken("my-token"),              // 또는 IRIS_BOT_TOKEN 환경변수 사용
    iris.WithTimeout(5 * time.Second),
    iris.WithHMACSecret("shared-secret"),
    iris.WithLogger(slog.Default()),
    iris.WithReplyRetry(3),                     // 최초 요청을 포함한 최대 시도 횟수
    iris.WithTransport("h3"),                   // 또는 IRIS_TRANSPORT 환경변수 사용
    iris.WithH3CACertFile("/run/iris/h3-ca.crt"),
)

1. HTTP/3 전송 설정

Iris API의 기본 전송 프로토콜은 HTTP/3(QUIC)입니다. IRIS_TRANSPORT 환경 변수가 누락된 경우 기본적으로 h3 전송이 적용되며 이 경우 https:// 스키마가 포함된 Base URL을 설정해야 합니다.

Base endpoint는 iris.ParseBaseEndpoint와 모든 client 생성 경로에서 같은 문법으로 검증합니다. 절대 http/https URL과 host가 필요하며 opaque URL, userinfo, query, fragment는 허용하지 않습니다. 끝의 /만 제거하고 /tenant/iris 같은 deployment prefix는 보존합니다. 실제 요청 URL에는 고정 API route를 prefix 뒤에 한 번 붙이지만 HMAC canonical target은 계속 /reply 같은 API route만 사용합니다.

c, err := iris.NewClient(
    iris.WithBaseURL("https://iris-host:31001"),
    iris.WithBotToken("my-token"),
    iris.WithTransport("h3"),
    iris.WithH3CACertFile("/run/iris/h3-ca.crt"),
    iris.WithH3ServerName("iris-host"),
)
defer c.Close()

IRIS_TRANSPORT=h3 옵션은 https:// 보안 연결에서만 활성화됩니다. http3, http/3, quic 문자열 역시 h3와 동일하게 인식합니다. 현재 Iris runtime에서 http1은 loopback의 GET /health, GET /ready probe와 transport 단위 테스트에만 사용합니다. config, reply, query, diagnostics와 SSE를 포함한 보호 메서드는 h3https:// Base URL이 필요합니다. 그 밖의 전송 값은 지원하지 않습니다.

운영 환경에서 H3 egress 대상을 Base URL host로 제한하려면 DNS allowset을 TTL마다 갱신하는 WithH3DialGuardForBaseURL을 사용할 수 있습니다. 만료 시 stale allowset이 허용하는 dial은 즉시 통과하고 refresh는 뒤에서 끝납니다. stale allowset이 거부하는 dial만 그 refresh 결과를 기다렸다 한 번 더 판정하므로, host의 IP가 바뀌어도 TTL 경계의 요청이 ErrH3EgressDenied로 희생되지 않습니다. 어느 경우든 동시 dial은 하나의 refresh를 공유하며, allowset이 아직 유효한 동안의 거부는 DNS를 조회하지 않고 즉시 반환합니다. dial의 context가 먼저 취소되면 기다리지 않고 거부합니다. 초기 DNS 해석 실패는 기본적으로 오류를 반환하며 WithH3DialGuardLenientInit을 지정하면 deny-all 상태로 기동한 뒤 TTL이 만료된 첫 dial이 refresh를 수행해 자가회복합니다. 엉뚱한 host를 allowlist하지 않도록 WithH3DialGuardForBaseURLWithBaseURL에는 반드시 동일한 Base URL을 전달해야 합니다.

baseURL := "https://iris-host:31001"
dialGuard, err := iris.WithH3DialGuardForBaseURL(
    ctx,
    baseURL,
    iris.WithH3DialGuardTTL(time.Minute),
    iris.WithH3DialGuardResolveTimeout(5*time.Second),
    iris.WithH3DialGuardLogger(logger),
)
if err != nil {
    return err
}
c, err := iris.NewClient(
    iris.WithBaseURL(baseURL),
    iris.WithTransport("h3"),
    dialGuard,
)

직접 정책을 구현해야 하는 경우 기존 WithH3DialGuard 또는 context 값을 받는 WithH3DialGuardContext를 사용할 수 있습니다. guard가 에러를 반환하면 연결은 시도되지 않고 iris.IsH3EgressDenied(err)로 분류할 수 있습니다.

2. 엔드포인트별 비밀키(Token) 분리 권장

보안 강화를 위해 모든 API 엔드포인트에 단일 토큰(WithHMACSecret)을 적용하는 대신, API 역할별로 전용 비밀 토큰을 지정할 수 있습니다.

c, err := iris.NewClient(
    iris.WithBaseURL("https://iris-host:31001"),
    iris.WithTransport("h3"),
    iris.WithH3CACertFile("/run/iris/h3-ca.crt"),
    iris.WithBotToken("shared-token"),               // 공유 폴백 키 (하위 호환 유지)
    iris.WithInboundSecret("config-signing-secret"),  // /config 전용
    iris.WithBotControlToken("bot-control-token"),    // /reply, /rooms 등 제어 API 전용
    iris.WithCertReloadToken("cert-reload-token"),    // /admin/cert-reload 전용
)

3. 웹훅 핸들러 설정 (Webhook Handler Configuration)

import (
    "github.com/park285/iris-client-go/v2/iris"
    "github.com/park285/iris-client-go/v2/valkeydedup"
    "github.com/park285/iris-client-go/v2/webhook"
)

handler, err := iris.NewWebhookHandler(msgHandler,
    webhook.WithWebhookToken("webhook-secret"),  // 또는 IRIS_WEBHOOK_TOKEN 환경변수 사용
    webhook.WithMessageDeduplicator(valkeydedup.NewMessageDeduplicator(valkeyClient)),
    webhook.WithNonceStore(valkeydedup.NewNonceStore(valkeyClient)),
    webhook.WithDedupTTL(16 * time.Minute),
    webhook.WithWorkerCount(32),                 // Key-ordering 동시성 워커 개수
    webhook.WithQueueSize(2000),
    webhook.WithHandlerTimeout(30 * time.Second),
    webhook.WithMaxBodyBytes(1 << 20),           // 최대 요청 크기 (1MB)
    webhook.WithMetrics(myPrometheusAdapter),
    webhook.WithWebhookLogger(slog.Default()),
)
  • 웹훅 메시지 스키마(webhook.Message/webhook.MessageJSON)와 핸들러 옵션(webhook.WithXxx)은 webhook 패키지에서 직접 import합니다. SDK 진입점인 iris.NewWebhookHandler(환경변수 해석·검증 포함)는 iris 패키지에 유지되며 Valkey 구현은 valkeydedup.NewMessageDeduplicatorvalkeydedup.NewNonceStore로 역할을 분리합니다.
  • optional sourceCreatedAtMsWebhookRequest.SourceCreatedAtMS로 decode되고 durable handler용 MessageJSON.SourceCreatedAtMS까지 그대로 전달됩니다. 값은 원본 Kakao row의 초 단위 시각을 millisecond로 표현한 계측 입력이며 message identity나 ordering key가 아닙니다.
  • 메시지 순서 보장: in-memory 모드에서는 기본적으로 동일한 채팅방 또는 동일 스레드 내의 메시지가 순차 처리됩니다. 자체적인 durable scheduler나 분산 큐가 순서를 소유하는 경우 webhook.WithDurableAdmission을 사용하거나 webhook.WithOrderingMode(webhook.OrderingModeNone)로 in-memory ordering을 끌 수 있습니다.

환경 변수 (Environment Variables)

환경 변수 설명
IRIS_BASE_URL Iris 백엔드 서버 Base URL
IRIS_BOT_TOKEN 봇 호출 API 인증용 Bearer 토큰
IRIS_WEBHOOK_TOKEN 웹훅 유효성 검증용 인바운드 인증 토큰
IRIS_TRANSPORT 메시지 전송용 프로토콜 (h3 [기본값], http1 지원)
  • 코드 상에서 옵션 함수(WithBaseURL 등)로 주입된 값이 환경 변수로 로드된 값보다 항상 우선하여 적용됩니다.

라이브러리 구조 (Directory Layout)

iris/              # SDK Facade - 외부 노출용 엔트리 포인트 (NewClient, NewWebhookHandler 등)
webhook/           # WebhookHandler, 메시지 스키마 정의 및 순차 스케줄러 큐
webhooksign/       # Webhook signature v3 요청 header 생성 helper
valkeydedup/       # Valkey 기반 메시지 중복 제거 public wrapper
internal/client/   # transport/signing/SSE/multipart/rebind/query/common 내부 구현
internal/dedup/    # Valkey 기반 메시지 중복 제거 구현체

라이선스 (License)

Apache License 2.0 — LICENSE

About

Unified Go client library for Iris (KakaoTalk message bridge)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages