데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      Python에서 Golang으로: 에이닷 에이전트 Workflow Engine 전환기

      isaac.park 25.10.22
      1,913 13 3
      DEVOTEE 요약
      Python으로 개발된 에이전트 워크플로우 엔진을 Golang으로 전환한 결과, 응답 시간(TTFB 48%, TTLB 26%)과 인프라 효율성(운영 서버 85% 감소)에서 큰 성능 개선을 이루었습니다. Golang의 고루틴과 채널은 LLM 기반 워크로드의 핵심인 스트리밍, 병렬 처리, 순서 보장 구현을 단순하고 효과적으로 지원하며, 높은 프로덕션 안정성을 제공합니다. Python은 ML 개발에 강점이 있지만, Golang은 뛰어난 병렬성과 리소스 효율성을 통해 에이전트 오케스트레이션 워크로드에 더 적합한 선택이었습니다.

      우리팀은 최근 Python으로 구축된 에이닷 에이전트 워크플로우 엔진을 Golang으로 재구현했습니다.

      GIL(Global Interpreter Lock) 제약과 동기식 웹 프레임워크의 한계를 벗어나 비동기 구조로 전환한 결과 처리량과 응답 시간에서 의미 있는 개선을 얻을 수 있었습니다.

      이 글에서는 그 과정과 선택의 기술적 이유를 소개하려고 합니다.

      TL;DR: Golang 전환 전후 성능 비교

      실제 프로덕션 환경에서 Golang으로 재구축한 에이전트 워크플로우 엔진의 성능 측정 결과입니다.

      응답 시간 개선

      latency 개선 지표

      Agent

      메트릭

      Python (as-was)

      Golang (as-is)

      개선도

      전화 에이전트

      TTFB avg

      1.38

      0.72

      48% ↓

      TTLB avg

      1.76

      1.37

      22% ↓

      길찾기 에이전트

      TTFB avg

      2.90

      1.44

      50% ↓

      TTLB avg

      4.01

      2.56

      36% ↓

      라디오 에이전트

      TTFB avg

      1.92

      1.07

      44% ↓

      TTLB avg

      2.18

      1.76

      19% ↓

      지하철 혼잡도 에이전트

      TTFB avg

      2.41

      1.11

      54% ↓

      TTLB avg

      3.82

      2.72

      29% ↓

      날씨 에이전트

      TTFB avg

      2.38

      1.25

      47% ↓

      TTLB avg

      3.05

      2.31

      24% ↓

      TTLB(Time To Last Byte)는 평균 26%, TTFB(Time To First Byte)는 평균 48% 단축되었습니다. 사용자가 실제로 체감하는 반응성이 개선되었습니다.

      인프라 효율성

      구분

      개선도

      운영 서버 대수

      85% ↓

      서버당 평균 CPU 사용률

      45% ↓

      Golang의 효율적인 리소스 사용 덕분에 운영 서버 대수를 85% 감축할 수 있었습니다.

      그럼에도 서버당 CPU 사용률은 45% 감소했습니다.

      왜 Golang인가: 에이전트 워크플로우의 특성

      LLM 기반 에이전트 워크플로우는 전통적인 웹 애플리케이션과 특성이 다릅니다.

      • 단일 요청의 처리 시간이 수초에서 수십초

      • 중간 결과를 스트리밍으로 클라이언트에 전달

      • 여러 에이전트의 호출이 직/병렬로 연결

      Python은 풍부한 생태계와 빠른 프로토타이핑으로 데이터 과학과 ML 개발에 탁월합니다.

      하지만 우리 시스템은 모델 개발이 아닌 에이전트 오케스트레이션 서비스입니다.

      Golang은 이런 워크로드에 근본적 장점을 가지고 있습니다.

      1. 가벼운 동시성 모델 - 고루틴은 런타임 관리 경량 스레드로 수천 개를 생성해도 오버헤드가 미미하고, 채널은 고루틴 간 안전하게 동기화 된 통신을 제공합니다.

      2. 명확한 제어 흐름 - 콜백이나 복잡한 비동기 추상화 없이 순차적 사고로 동시성 코드를 작성할 수 있습니다.

      3. 효율적인 실행과 메모리 - 컴파일된 바이너리와 효율적인 GC로 동일 리소스에서 더 많은 작업을 처리합니다.

      고루틴 + 채널: 스트리밍 에이전트의 자연스러운 구현

      LLM 기반 에이전트의 주요 특성은 긴 지연시간과 스트리밍 응답입니다.

      사용자는 수초에서 수십초를 기다려야 하는데, 이때 중간 결과를 실시간으로 보여주는 것이 체감 속도를 크게 개선합니다.

      Golang의 고루틴과 채널은 이런 요구사항을 자연스럽게 표현할 수 있는데요, 예외 처리 등은 생략하고, 핵심 흐름만 남긴 간단한 예제로 설명해보겠습니다.


      먼저 에이전트 인터페이스를 정의합니다.

      // Request 요청 구조체
      type Request struct {
          Message string `json:"message"`
      }
      
      // ResponseChunk 에이전트 응답 스트리밍 단위
      type ResponseChunk struct {
          Name    string `json:"name"`
          Content string `json:"content"`
      }
      
      // Agent 에이전트 인터페이스
      type Agent interface {
          Run(input Request) <-chan ResponseChunk
      }

      다음은 이 인터페이스를 구현한 모의 에이전트입니다.

      import (
          "math/rand"
          "time"
      )
      
      type mockAgent struct {
          name string
      }
      
      func NewMockAgent() Agent {
          return &mockAgent{
              name: "Echo",
          }
      }
      
      func (m *mockAgent) Run(input Request) <-chan ResponseChunk {
          // 출력 채널 생성
          out := make(chan ResponseChunk)
      
          // 고루틴을 이용한 비동기 작업 수행
          go func() {
              defer close(out)
      
              // 입력 문자열의 각 문자마다 랜덤 지연 후 출력 채널로 전송 (긴 지연시간, 중간 스트리밍 시뮬레이션)
              for _, v := range input.Message {
                  time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
                  out <- ResponseChunk{Name: m.name, Content: string(v)}
              }
          }()
      
          return out
      }

      Run 함수는 입력을 받아 즉시 채널을 반환합니다. 실제 작업은 백그라운드 고루틴에서 수행되고, 결과는 채널을 통해 스트리밍됩니다.

      이것이 Golang의 동시성 철학입니다. "Don't communicate by sharing memory; share memory by communicating"

      SSE 스트리밍: Gin 프레임워크

      이제 이 채널을 SSE(Server-Sent Events)로 스트리밍하는 HTTP 핸들러를 보겠습니다. 우리는 Gin 프레임워크를 선택했습니다.

      package main
      
      import (
          "github.com/gin-gonic/gin"
          "io"
          "net/http"
      )
      
      func main() {
          r := gin.Default()
      
          // POST /echo 엔드포인트
          r.POST("/echo", func(c *gin.Context) {
              var req Request
              if err := c.ShouldBind(&req); err != nil {
                  c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                  return
              }
      
              agent := NewMockAgent()
              out := agent.Run(req)	// 에이전트 실행하고 즉시 응답 채널 반환
              sse(c, out)				// 채널을 SSE 응답으로 스트리밍
              return
          })
      
          r.Run()
      }
      
      func sse(c *gin.Context, ch <-chan ResponseChunk) bool {
          c.Writer.Header().Set("Content-Type", "text/event-stream")
          c.Writer.Header().Set("Cache-Control", "no-cache")
          c.Writer.Header().Set("Connection", "keep-alive")
      
          if f, ok := c.Writer.(http.Flusher); ok {
              f.Flush()
          }
      
          return c.Stream(func(w io.Writer) bool {
              if chunk, ok := <-ch; ok {
                  // 채널에서 읽은 데이터를 SSE 이벤트로 전송
                  c.SSEvent("", chunk)
                  return true
              } else {
                  // 채널이 닫히면 스트리밍 종료
                  return false
              }
          })
      }

      해당 서버를 띄워서 curl 로 실행해보겠습니다.

      ➜  curl -N -H 'Content-Type: application/json' \
         -d '{"message": "안녕하세요"}' \
         'localhost:8080/echo'
      data:{"name":"Echo","content":"안"}
      
      data:{"name":"Echo","content":"녕"}
      
      data:{"name":"Echo","content":"하"}
      
      data:{"name":"Echo","content":"세"}
      
      data:{"name":"Echo","content":"요"}
      
      ➜  

      Gin의 c.Stream()과 c.SSEvent()메서드는 SSE 프로토콜의 세부사항을 처리합니다.

      코드는 단순하고 직관적입니다. Spring WebFlux 나 Python asyncio 같은 이벤트 기반 비동기 구조에서 볼 수 있는 Promise나 Operator 체인의 복잡한 추상화가 없습니다.

      에이전트는 채널을 반환하고, 핸들러는 그 채널을 읽어 스트리밍합니다.

      전통적인 절차적 코드 흐름처럼 읽히지만, 내부적으로는 완전한 비동기 처리를 수행합니다.

      고루틴+채널을 이용한 SSE 스트리밍 구조

      LLM 에이전트 구현: Golang SDK 생태계

      위의 에이전트는 모의 워크로드였습니다.

      실제 LLM 기반 에이전트를 구현할 때는 어떨까요? 주요 LLM Provider들이 이미 완성도 높은 공식 Golang SDK를 제공하고 있어서 구현에 어려움이 없습니다.

      SDK들을 이용해 각 에이전트들의 인터페이스를 구현해보겠습니다.

      OpenAI ChatGPT

      type chatgptAgent struct {
          name   string
          client openai.Client
      }
      
      func NewChatGptAgent() Agent {
          apiKey := "..."
      
          return &chatgptAgent{
              name:   "ChatGPT",
              client: openai.NewClient(option.WithAPIKey(apiKey)),
          }
      }
      
      func (c *chatgptAgent) Run(input Request) <-chan ResponseChunk {
          out := make(chan ResponseChunk)
      
          go func() {
              defer close(out)
      
              stream := c.client.Chat.Completions.NewStreaming(context.TODO(), openai.ChatCompletionNewParams{
                  Messages: []openai.ChatCompletionMessageParamUnion{openai.UserMessage(input.Message)},
                  Model:    openai.ChatModelGPT4_1Mini,
              })
      
              for stream.Next() {
                  chunk := stream.Current()
      
                  if len(chunk.Choices) == 0 {
                      continue
                  }
      
                  contentChunk := chunk.Choices[0].Delta.Content
                  if contentChunk == "" {
                      continue
                  }
      
                  out <- ResponseChunk{
                      Name:    c.name,
                      Content: contentChunk,
                  }
              }
          }()
      
          return out
      }

      NewStreaming() 메서드로 스트림 객체를 얻고, Next() 와 Current() 로 청크를 순회합니다.

      이를 채널로 변환해 통일된 인터페이스를 제공합니다.

      Anthropic Claude

      type claudeAgent struct {
          name   string
          client anthropic.Client
      }
      
      func NewClaudeAgent() Agent {
          apiKey := "..."
      
          return &claudeAgent{
              name:   "Claude",
              client: anthropic.NewClient(option.WithAPIKey(apiKey)),
          }
      }
      
      func (c *claudeAgent) Run(input Request) <-chan ResponseChunk {
          out := make(chan ResponseChunk)
      
          go func() {
              defer close(out)
      
              stream := c.client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
                  MaxTokens: 1024,
                  Model:     anthropic.ModelClaudeSonnet4_0,
                  Messages:  []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(input.Message))},
              })
      
              for stream.Next() {
                  event := stream.Current()
      
                  switch eventVariant := event.AsAny().(type) {
                  case anthropic.ContentBlockDeltaEvent:
                      switch deltaVariant := eventVariant.Delta.AsAny().(type) {
                      case anthropic.TextDelta:
                          if deltaVariant.Text == "" {
                              continue
                          }
      
                          out <- ResponseChunk{
                              Name:    c.name,
                              Content: deltaVariant.Text,
                          }
                      }
      
                  }
              }
          }()
      
          return out
      }

      OpenAI SDK 와 동일하게 NewStreaming()과 Next() 패턴을 따르고 있습니다.

      Google Gemini

      type geminiAgent struct {
          name   string
          client *genai.Client
      }
      
      func NewGeminiAgent() Agent {
          apiKey := "..."
          client, _ := genai.NewClient(context.TODO(), &genai.ClientConfig{
              APIKey:  apiKey,
              Backend: genai.BackendGeminiAPI,
          })
      
          return &geminiAgent{
              name:   "Gemini",
              client: client,
          }
      }
      
      func (g *geminiAgent) Run(input Request) <-chan ResponseChunk {
          out := make(chan ResponseChunk)
      
          go func() {
              defer close(out)
      
              iter := g.client.Models.GenerateContentStream(context.TODO(), "gemini-2.5-flash-lite", genai.Text(input.Message), nil)
      
              for chunk := range iter {
                  if len(chunk.Candidates) == 0 || len(chunk.Candidates[0].Content.Parts) == 0 {
                      continue
                  }
      
                  contentChunk := chunk.Candidates[0].Content.Parts[0]
                  if contentChunk.Text == "" {
                      continue
                  }
      
                  out <- ResponseChunk{
                      Name:    g.name,
                      Content: contentChunk.Text,
                  }
              }
          }()
      
          return out
      }

      GenerateContentStream() 으로 이터레이터를 반환하며, range로 청크를 순회합니다.

      세 SDK 모두 유사한 스트리밍 패턴을 제공합니다.

      Langchain 등의 고수준 프레임워크에 의존하지 않고 간단하고 효율적으로 LLM 기반 비즈니스 로직을 작성할 수 있습니다.

      다중 에이전트 오케스트레이션: 고루틴과 채널로 구현하기

      앞서 소개한 내용들을 종합하여 실제로 동작하는 워크플로우 엔진을 고루틴과 채널만으로 만들어보겠습니다.

      요구사항은 다음과 같습니다.

      • 병렬 실행: 모든 에이전트를 동시에 실행해 지연시간을 최소화

      • 순서 보장: 출력은 에이전트 정의 순서대로

      • 스트리밍 최적화: 자기 차례가 오면 버퍼링 없이 즉시 스트리밍

      Step 1: 병렬 에이전트 실행

      func spawnAgents(input Request, agents []Agent) []<-chan ResponseChunk {
          var chs []<-chan ResponseChunk
      
          for _, a := range agents {
              ch := a.Run(input)	// 각 에이전트를 비동기로 시작
              chs = append(chs, ch)
          }
      
          return chs
      }

      모든 에이전트를 병렬로 실행합니다. 각 에이전트는 독립적인 고루틴에서 실행되고, 자신의 채널로 결과를 스트리밍합니다.

      Step 2: 채널 병합

      // FanInChunk 병합 채널용 스트리밍 구조체
      type FanInChunk struct {
          Order int           // 에이전트 순서
          Chunk ResponseChunk // 실제 데이터
          Done  bool          // 완료 신호
      }
      
      func fanIn(chs []<-chan ResponseChunk) <-chan FanInChunk {
          var wg sync.WaitGroup
          wg.Add(len(chs))
      
          out := make(chan FanInChunk)
          for i, v := range chs {
              // 각 채널을 동시에 읽기
              go func(order int, ch <-chan ResponseChunk) {
                  for chunk := range ch {
                      // 읽은 데이터를 채널 인덱스와 함께 출력 채널로 전달
                      out <- FanInChunk{Order: order, Chunk: chunk, Done: false}
                  }
      
                  // 채널이 닫히면 완료 신호 전달
                  out <- FanInChunk{Order: order, Chunk: ResponseChunk{}, Done: true}
                  wg.Done()
              }(i, v)
          }
      
          go func() {
              wg.Wait()
              close(out)
          }()
      
          return out
      }

      여러 입력 채널을 하나의 출력 채널로 병합합니다.

      각 채널마다 독립적인 고루틴이 할당되어 동시에 읽기를 수행하고, 각 청크에 원래 채널의 순서(Order)를 태그합니다.

      Step 3: 오케스트레이션

      오케스트레이터는 병합된 채널에서 무작위 순서로 도착하는 청크들을 받아, 에이전트의 원래 순서대로 재배열하여 출력합니다.

      첫 번째 에이전트는 버퍼링 없이 즉시 스트리밍되고, 나머지는 자기 차례가 올 때까지 버퍼에 대기합니다.

      func orchestrate(in <-chan FanInChunk) <-chan ResponseChunk {
      	out := make(chan ResponseChunk)
      
      	go func() {
      		defer close(out)
      
      		buf := make([][]ResponseChunk, 0) // 각 에이전트의 청크를 저장할 버퍼
      		cur := 0                          // 현재 출력 대상 에이전트 인덱스
      
      		for chunk := range in {
      			// 0. 버퍼 확장
      			for len(buf) <= chunk.Order {
      				buf = append(buf, []ResponseChunk{})
      			}
      
      			// 1. 현재 순서가 아니면: 버퍼에 저장
      			if chunk.Order != cur {
      				if !chunk.Done {
      					buf[chunk.Order] = append(buf[chunk.Order], chunk.Chunk)
      				}
      
      				continue
      			}
      
      			// 2. 현재 순서: 버퍼 flush 후 처리
      			for _, buffered := range buf[chunk.Order] {
      				out <- buffered
      			}
      			buf[chunk.Order] = nil
      
      			if chunk.Done {
      				cur++
      			} else {
      				out <- chunk.Chunk
      			}
      		}
      
      		// 3. 남은 버퍼 flush
      		for cur < len(buf) {
      			for _, buffered := range buf[cur] {
      				out <- buffered
      			}
      			cur++
      		}
      	}()
      
      	return out
      }

      병합된 채널에서 무작위로 도착하는 청크들을 원래 순서대로 재배열하는 로직이 단순한 버퍼 관리와 채널 읽기로 표현됩니다.

      채널 자체가 thread-safe하기 때문에 복잡한 동기화 라이브러리나 락(lock)이 없습니다.

      최종 통합: 3줄의 파이프라인

      func main() {
          r := gin.Default()
      
      	// 출력 순서는 아래 정의된 대로
          agents := []Agent{
              NewChatGptAgent(), 
              NewClaudeAgent(),
              NewGeminiAgent(),
          }
      
          r.POST("/orchestrate", func(c *gin.Context) {
              var req Request
              if err := c.ShouldBind(&req); err != nil {
                  c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                  return
              }
      
              chs := spawnAgents(req, agents)	// 병렬 실행
              merged := fanIn(chs)				// 채널 병합
              out := orchestrate(merged)		// 순서 조율
      
              sse(c, out)
              return
          })
      
          r.Run()
      }

      3줄의 함수 호출로 복잡한 다중 에이전트 오케스트레이션이 완성됩니다.

      각 함수가 하나의 명확한 역할을 담당하고, 채널로 연결됩니다.

      [Agent1] ───────┐
      [Agent2] ───────┼──────→ [fanIn] ───────→ [orchestrate] ───────→ [SSE Stream]
      [Agent3] ───────┘
      
                  (병렬 실행)             (무작위)                (순서 보장)

      실제 호출 예시입니다.

      ➜  curl -N -H 'Content-Type: application/json' \
         -d '{"message": "What is the meaning of life?"}' \
         'localhost:8080/orchestrate'
      data:{"name":"ChatGPT","content":"The"}			# ChatGPT 에이전트 응답
      
      data:{"name":"ChatGPT","content":" meaning"}
      
      data:{"name":"ChatGPT","content":" of"}
      
      data:{"name":"ChatGPT","content":" life"}
      
      data:{"name":"ChatGPT","content":" is"}
      
      data:{"name":"ChatGPT","content":" a"}
      
      data:{"name":"ChatGPT","content":" deeply"}
      
      data:{"name":"ChatGPT","content":" personal"}
      
      data:{"name":"ChatGPT","content":" and"}
      
      data:{"name":"ChatGPT","content":" philosophical"}
      
      data:{"name":"ChatGPT","content":" question"}
      
      ...
      
      data:{"name":"Claude","content":"This"}			# Claude 에이전트 응답
      
      data:{"name":"Claude","content":" is one of humanity"}
      
      data:{"name":"Claude","content":"'s oldest and most profound questions, an"}
      
      data:{"name":"Claude","content":"d there's no single answer"}
      
      ...
      
      data:{"name":"Gemini","content":"Ah"}			# Gemini 에이전트 응답
      
      data:{"name":"Gemini","content":", the age-old question! \"What is the meaning of life?\" It"}
      
      data:{"name":"Gemini","content":"'s a question that has been pondered by philosophers, theologians, scientists, artists, and everyday people for millennia. And the truth is, there's no single, universally agreed-upon answer.\n\nHere's a breakdown of why it's"}
      
      ...
      
      ➜   
      2025/10/13 14:54:44 orchestrator started
      2025/10/13 14:54:44 ChatGPT agent started
      2025/10/13 14:54:44 Claude agent started
      2025/10/13 14:54:44 Gemini agent started
      2025/10/13 14:54:46 ChatGPT agent ended (elapsed: 2.481247291s)
      2025/10/13 14:54:47 Gemini agent ended (elapsed: 3.520516333s)
      2025/10/13 14:54:50 Claude agent ended (elapsed: 6.467166166s)
      2025/10/13 14:54:50 orchestrator ended (elapsed: 6.469606583s)	    # 가장 느린 에이전트 시간과 동일
      [GIN] 2025/10/13 - 14:54:50 | 200 |  6.471028125s |             ::1 | POST     "/orchestrate"

      병렬로 실행되지만 출력은 순서대로, 첫 번째 에이전트(ChatGPT)의 응답은 버퍼링 없이 즉시 클라이언트로 스트리밍됩니다.

      모든 에이전트가 백그라운드에서 병렬 실행되므로 전체 지연시간은 가장 느린 에이전트의 시간과 같습니다.

      이 예제는 핵심 개념을 보여주기 위해 단순화한 것이지만, 실제 프로덕션 환경에서도 동일한 패턴을 적용했습니다.

      고루틴과 채널을 중심으로 한 이러한 단순한 구조가, 앞서 본 것처럼 응답 시간 단축과 서버 대수 감축이라는 실질적인 성능 개선으로 이어졌습니다.

      결론: 문제와 도구의 정합성

      LLM 기반 에이전트 워크플로우는 밀리초가 아닌 초 단위 응답, 스트리밍, 그리고 병렬 처리가 핵심 특징입니다.

      Golang의 고루틴과 채널은 이러한 요구사항에 자연스럽게 부합하며, 외부 메시지 큐 없이도 복잡한 오케스트레이션을 단순하게 구현할 수 있습니다.

      구조의 단순함은 효율성을 높여 성능을 극대화합니다.

      Python은 데이터 과학과 ML 개발에서 여전히 가장 강력한 선택지입니다. 

      그러나 에이전트 오케스트레이션처럼 프로덕션 환경에서 안정성과 병렬성이 중요한 워크로드에서는 Golang이 더 적합했습니다. 

      도구의 우열이 아니라, 문제에 가장 잘 맞는 도구를 선택하는 것이 핵심입니다.


      비슷한 워크플로우 로직을 고민하고 있다면, 우리의 경험이 기술 선택에 작은 참고가 되기를 바랍니다.

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      isaac.park 님의 최신 블로그

      더보기
      동영상 기고하기