Iris (카카오톡 메시지 브릿지)용 Go 클라이언트 라이브러리 SDK입니다.
go get github.com/park285/iris-client-go/v2@latestv1.0.0은 공개 표면 축소를 포함한 첫 stable major 릴리스로, 하위 호환성이 깨지는 변경 사항(Breaking Changes — 무소비 facade re-export 및 no-op webhook 옵션 제거)이 있습니다. 업그레이드 전에 CHANGELOG.md의 v1.0.0 항목을 반드시 확인하시기 바랍니다. v0.11.0 미만에서 올라오는 경우 MIGRATION-v0.11.0.md도 함께 확인하십시오.
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 계약을
벗어난 응답이면 빈 문자열을 반환합니다.
409 중 CLIENT_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는 이 보장이 없으므로 재발급 없이 종결하십시오.
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를 소유합니다.
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.MessageDeduplicator를 webhook.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을
유지하십시오.
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으로 되돌리기 위해서입니다.
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을 명시하여 설정 변경 시의 충돌을 방지할 수 있습니다.
events, err := c.EventStream(ctx, 0)
for ev := range events {
fmt.Printf("이벤트 타입: %s, 데이터: %s\n", ev.Event, ev.Data)
}// 채팅방 요약 정보 조회
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,
})
// 사용자의 최신 이벤트와 다음 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)
}다중 인프라 혹은 동적 환경을 지원하기 위해, 봇 서비스를 위한 최소 인터페이스인 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()ResolveInterval이 0이면 각 비동시 호출에서 즉시 Base URL을 다시 확인하는 기존 동작을 유지합니다. 양수이면 interval 안의 호출이 마지막 URL 또는 resolver 오류 snapshot을 공유하고 만료 후 첫 호출이 refresh를 수행합니다. 같은 시점의 동시 호출은 하나의 refresh 결과를 공유합니다.
refresh는 개별 API 호출이 아니라 RebindingClient가 소유합니다. refresh를 시작한 호출의 context가 취소되어도 해당 호출만 먼저 반환하며 진행 중인 refresh는 다른 동시 호출과 cache snapshot을 위해 완료됩니다. Close()는 대기 중인 호출을 즉시 깨우지만 context를 받지 않는 ResolveBaseURL 실행을 강제로 중단할 수는 없으므로 resolver는 유한 시간 안에 반환해야 합니다.
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"),
)Iris API의 기본 전송 프로토콜은 HTTP/3(QUIC)입니다. IRIS_TRANSPORT 환경 변수가 누락된 경우 기본적으로 h3 전송이 적용되며 이 경우 https:// 스키마가 포함된 Base URL을 설정해야 합니다.
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와 동일하게 인식합니다. 레거시 또는 로컬 테스트 목적으로 http:// 일반 연결을 사용할 경우 h2c 전송을 명시해야 하며 유효하지 않은 프로토콜 형식 지정 시 에러가 반환됩니다.
운영 환경에서 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하지 않도록 WithH3DialGuardForBaseURL과 WithBaseURL에는 반드시 동일한 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)로 분류할 수 있습니다.
보안 강화를 위해 모든 API 엔드포인트에 단일 토큰(WithHMACSecret)을 적용하는 대신, API 역할별로 전용 비밀 토큰을 지정할 수 있습니다.
c, err := iris.NewClient(
iris.WithBaseURL("http://localhost:3000"),
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 전용
)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.NewMessageDeduplicator와valkeydedup.NewNonceStore로 역할을 분리합니다. - 메시지 순서 보장: in-memory 모드에서는 기본적으로 동일한 채팅방 또는 동일 스레드 내의 메시지가 순차 처리됩니다. 자체적인 durable scheduler나 분산 큐가 순서를 소유하는 경우
webhook.WithDurableAdmission을 사용하거나webhook.WithOrderingMode(webhook.OrderingModeNone)로 in-memory ordering을 끌 수 있습니다.
| 환경 변수 | 설명 |
|---|---|
IRIS_BASE_URL |
Iris 백엔드 서버 Base URL |
IRIS_BOT_TOKEN |
봇 호출 API 인증용 Bearer 토큰 |
IRIS_WEBHOOK_TOKEN |
웹훅 유효성 검증용 인바운드 인증 토큰 |
IRIS_TRANSPORT |
메시지 전송용 프로토콜 (h3 [기본값], h2c, http2, http1 지원) |
- 코드 상에서 옵션 함수(
WithBaseURL등)로 주입된 값이 환경 변수로 로드된 값보다 항상 우선하여 적용됩니다.
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 기반 메시지 중복 제거 구현체
Apache License 2.0 — LICENSE