들어가며

ACOR v1.3.0에서 HTTP와 gRPC 서버 어댑터가 추가되었다. 이제 ACOR을 마이크로서비스 아키텍처에 쉽게 통합하거나, 별도의 서버로 배포할 수 있다. 이 포스트에서는 두 어댑터의 사용법과 배포 방법을 살펴본다.

아키텍처 개요

v1.3.0에서 pkg/server 패키지가 추가되어 기존 pkg/acor API를 HTTP JSON과 gRPC로 노출한다:

ClientHTpTSkPegR/r/egvadReciProsCr

HTTP 서버

기본 사용법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
    "log"
    "net/http"
    
    "github.com/skyoo2003/acor/pkg/acor"
    "github.com/skyoo2003/acor/pkg/server"
)

func main() {
    ac, err := acor.Create(&acor.AhoCorasickArgs{
        Addr: "localhost:6379",
        Name: "sample",
    })
    if err != nil {
        log.Fatal(err)
    }
    defer ac.Close()

    httpHandler := server.NewHTTPHandler(ac)
    http.Handle("/", httpHandler)
    
    log.Println("HTTP server listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

API 엔드포인트

MethodPath설명
POST/add키워드 추가
POST/remove키워드 삭제
POST/find텍스트 검색
POST/find-index위치 정보와 함께 검색
POST/suggest자동완성 제안
POST/suggest-index위치 정보와 함께 자동완성
GET/info컬렉션 정보
POST/flush컬렉션 삭제

요청/응답 예제

키워드 추가

1
2
3
curl -X POST http://localhost:8080/add \
  -H "Content-Type: application/json" \
  -d '{"keyword": "he"}'
1
2
3
{
  "added": true
}

텍스트 검색

1
2
3
curl -X POST http://localhost:8080/find \
  -H "Content-Type: application/json" \
  -d '{"text": "he is him"}'
1
2
3
{
  "matched": ["he", "him"]
}

위치 정보와 함께 검색

1
2
3
curl -X POST http://localhost:8080/find-index \
  -H "Content-Type: application/json" \
  -d '{"text": "he is him"}'
1
2
3
4
5
6
{
  "matched": {
    "he": [0],
    "him": [6]
  }
}

gRPC 서버

프로토버퍼 정의

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
syntax = "proto3";

package acor;

option go_package = "github.com/skyoo2003/acor/pkg/server/proto";

service AhoCorasick {
    rpc Add(AddRequest) returns (AddResponse);
    rpc Remove(RemoveRequest) returns (RemoveResponse);
    rpc Find(FindRequest) returns (FindResponse);
    rpc FindIndex(FindRequest) returns (FindIndexResponse);
    rpc Suggest(SuggestRequest) returns (SuggestResponse);
    rpc SuggestIndex(SuggestRequest) returns (SuggestIndexResponse);
    rpc Info(InfoRequest) returns (InfoResponse);
    rpc Flush(FlushRequest) returns (FlushResponse);
}

message AddRequest {
    string keyword = 1;
}

message AddResponse {
    bool added = 1;
}

message FindRequest {
    string text = 1;
}

message FindResponse {
    repeated string matched = 1;
}

message FindIndexResponse {
    map<string, repeated int32> matched = 1;
}

서버 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
    "log"
    "net"
    
    "github.com/skyoo2003/acor/pkg/acor"
    "github.com/skyoo2003/acor/pkg/server"
    "google.golang.org/grpc"
)

func main() {
    ac, err := acor.Create(&acor.AhoCorasickArgs{
        Addr: "localhost:6379",
        Name: "sample",
    })
    if err != nil {
        log.Fatal(err)
    }
    defer ac.Close()

    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatal(err)
    }

    grpcServer := grpc.NewServer()
    server.RegisterGRPCServer(grpcServer, ac)

    log.Println("gRPC server listening on :50051")
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatal(err)
    }
}

클라이언트 예제

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
    "context"
    "fmt"
    "log"
    
    pb "github.com/skyoo2003/acor/pkg/server/proto"
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    client := pb.NewAhoCorasickClient(conn)

    // 키워드 추가
    _, err = client.Add(context.Background(), &pb.AddRequest{Keyword: "he"})
    if err != nil {
        log.Fatal(err)
    }

    // 검색
    resp, err := client.Find(context.Background(), &pb.FindRequest{Text: "he is him"})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(resp.Matched) // [he, him]
}

배포

Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FROM golang:1.24-alpine AS builder

WORKDIR /app
COPY . .
RUN go build -o server ./cmd/server

FROM alpine:latest

WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080 50051

CMD ["./server"]
1
2
docker build -t acor-server .
docker run -p 8080:8080 -p 50051:50051 acor-server

Kubernetes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: Deployment
metadata:
  name: acor-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: acor-server
  template:
    metadata:
      labels:
        app: acor-server
    spec:
      containers:
      - name: acor-server
        image: acor-server:latest
        ports:
        - containerPort: 8080
          name: http
        - containerPort: 50051
          name: grpc
        env:
        - name: REDIS_ADDR
          value: "redis-service:6379"
---
apiVersion: v1
kind: Service
metadata:
  name: acor-server
spec:
  selector:
    app: acor-server
  ports:
  - port: 8080
    targetPort: 8080
    name: http
  - port: 50051
    targetPort: 50051
    name: grpc

HTTP vs gRPC 선택 가이드

기준HTTPgRPC
프로토콜HTTP/1.1 + JSONHTTP/2 + Protobuf
성능보통높음
디버깅curl 등으로 쉬움도구 필요
클라이언트모든 언어Go, Java, Python 등
스트리밍미지원지원

마치며

v1.3.0의 서버 어댑터로 ACOR을 마이크로서비스로 쉽게 배포할 수 있게 되었다. HTTP는 디버깅과 빠른 프로토타이핑에, gRPC는 고성능이 필요한 프로덕션 환경에 적합하다.

더 자세한 내용은 ACOR GitHub 저장소공식 문서를 참고하자.