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

신고하기

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

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

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

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

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

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

      임시저장함

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

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

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

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

      회원정보 연결

      Go로 만드는 실시간 음성 챗봇: OpenAI Realtime API를 가장 쉽게 쓰는 법 (Go routine + Go channel)

      지수 25.06.13
      4,032 14 5
      DEVOTEE 요약
      음성 챗봇의 기존 구조는 음성을 텍스트로 변환하고 응답을 생성한 뒤 이를 다시 음성으로 합성하는 방식으로 진행되지만, 실시간 응답이나 사용자 끼어들기를 자연스럽게 처리하지 못해 한계가 있었습니다. 이를 개선하기 위해 OpenAI Realtime API를 활용해 실시간 음성 대화를 구현했으며, Go의 channel과 goroutine을 활용해 WebSocket 스트리밍을 명확하고 직관적으로 처리할 수 있었습니다. OpenAI Realtime API는 초저지연 양방향 통신을 지원하며, 앞으로 확장성과 최적화를 통해 더 많은 가능성을 제공할 것으로 기대됩니다.
      DEVOTEE 추천 블로그

      1. 음성 챗봇의 다음 단계

      우리는 다양한 상황에서 음성으로 AI와 대화하고 있습니다. 대부분의 음성 챗봇은 사용자의 입력이 끝나야 다음 단계가 진행되는 순차 처리 방식입니다.

      일반적인 음성 챗봇: STT → LLM → TTS

      1. STT (Speech-to-Text): 사용자의 음성을 텍스트로 변환

      2. LLM (Large Language Model): 변환된 텍스트를 기반으로 AI 응답 생성

      3. TTS (Text-to-Speech): 생성된 텍스트 응답을 다시 음성으로 합성

      이 구조는 음성을 → 텍스트로 → 생각하고 → 다시 음성으로 바꾸는 방식으로 동작하고 있습니다.

      그런데 이 구조에는 두 가지 한계를 가지고 있습니다.

      1. 음성 응답 중 말하기 불가

      • 챗봇이 음성으로 응답하는 동안에는 사용자가 말을 시작할 수 없습니다.

      • 즉, 사람이 대화 중 끼어들 듯 말하는 barge-in 기능이 지원되지 않아, 실제 대화처럼 자연스럽게 주고받는 흐름이 어렵습니다.

      1. 누적되는 지연 시간

      • 음성 입력 → 텍스트 변환(STT) → 응답 생성(LLM) → 음성 합성(TTS) 과정을 순차적으로 거치기 때문에, 각 단계의 처리 시간이 누적되며 전체 응답 속도에 영향을 줍니다.

      실시간 음성 챗봇: OpenAI Realtime API

      사람과의 자연스러운 대화처럼, 말이 겹치고 바로 반응하는 진짜 실시간 대화란 어떤 모습일지에 대해 고민해보게 되었습니다.

      여기서 말하는 ‘실시간’은 단순히 빠른 응답이 아니라, 사용자와 챗봇이 동시에 말하고, 끊고, 반응하는 자연스러운 상호작용을 의미합니다.

      이러한 경험이 가능할지 탐색하는 과정에서, 우리는 OpenAI의 Realtime API를 테스트해보며 그 가능성을 살펴보게 되었습니다.


      2. OpenAI RealTime API란?

      OpenAI Realtime API는 멀티모달 모델(GPT-4o 및 mini) 을 기반으로 한 초저지연(ultra low-latency) 양방향 인터페이스입니다.

      WebSocket 또는 WebRTC 연결로 텍스트와 오디오를 동시에 스트리밍하고, 실시간 음성 대화를 구현할 수 있게 만들어졌습니다. (Speech-to-Speech)

      기능

      설명

      실시간 STT

      오디오 스트림을 실시간 텍스트로 변환 (GPT-4o Transcribe)

      LLM

      입력 텍스트 기반 GPT-4o/mini 모델의 실시간 응답 생성

      실시간 TTS

      응답 텍스트를 자연스러운 음성으로 합성

      Voice Activity Detection (VAD)

      사용자의 발화 시작, 종료 시점을 자동 감지

      응답 도중 사용자가 끼어들면 바로 입력 전환

      OpenAI Realtime API는 다음 두가지 연결 방식을 지원합니다.

      방법

      용도

      WebRTC

      클라이언트에서 바로 오디오를 보내고 싶은 경우, 별도 백엔드 구축 없이 빠르게 데모 구현할 때

      • 클라이언트 측에서 직접 연결할 때 유리(브라우저, 모바일 SDK)

      • 오디오 캡처/스트림 처리 지원

      • 와이파이 상태가 불안정하거나, 모바일 데이터 연결이 자주 바뀌는 환경에서도 끊김 없이 연결 가능

      WebSocket

      백엔드에서 OpenAI Realtime API를 붙이는 경우

      • 기존 백엔드 시스템(e.g. 음성 지원 에이전트, 통화 서비스)과 통합 및 상태 관리 유리

      • 연결 유지/복구 로직 직접 구성 가능

      우리는 기존의 백엔드 시스템에서 관리할 수 있도록 WebSocket을 활용하여 서버 - 서버 연동을 테스트해보았습니다.


      3. 전체적인 아키텍처


      클라이언트에서 실시간으로 음성 데이터를 서버로 전송하면, 서버는 이를 OpenAI Realtime API에 전달하고, 응답 결과(음성 또는 텍스트)를 다시 실시간으로 클라이언트에 전달하는 구조입니다.

      서버는 이 과정에서 중계와 함께 DB에서 프롬프트 설정을 조회하거나, 로그를 저장하는 등 비즈니스 로직도 함께 처리할 수 있습니다.

      그리고 OpenAI Realtime API와는 WebSocket 연결을 유지한 채, 스트림 기반으로 상시 통신하고 있습니다.


      4. Go Channel과 Goroutine으로 WebSocket 양방향 스트림을 쉽게 다뤄보자

      우리가 목표로 했던 것은 WebSocket 기반 OpenAI Realtime API와의 실시간 양방향 통신을 안정적이고 명시적으로 다루는 것이었습니다.

      WebSocket을 사용하면 다음과 같은 이유로 구현이 복잡해질 수 있는데요.

      • 하나의 스트림에서 동시에 송수신이 일어나야 합니다. (별도의 스레드, 비동기 핸들링 구조 필요)

      • 연결 종료, 네트워크 오류, 타임아웃 등 다양한 실패 케이스가 송수신 양쪽에서 다르게 발생하므로 예외 처리가 분산될 수 있습니다.

      • 연결 상태를 mocking하거나 제어하기 어렵습니다.

      이를 위해 Go에서 제공하는 채널(channel) 과 고루틴(goroutine) 을 활용했습니다.

      Go Channel

      Go의 chan(채널)은 Goroutine 간 데이터를 주고받을 수 있는 안전한 통신 수단입니다.

      간단히 말하면, 두 개의 흐름 사이에 데이터를 흘려보내는 파이프 같은 개념입니다.

      ch := make(chan string)
       
      go func() {
        ch <- "Hello" // 채널에 메시지 전송
      }()
       
      msg := <-ch      // 채널에서 메시지 수신
      fmt.Println(msg) // "Hello"

      Goroutine

      goroutine Go의 초경량 스레드로, 함수 하나를 동시에 실행시키는 비동기 작업 단위입니다.

      수천 개를 띄워도 메모리 부담이 거의 없기 때문에, WebSocket처럼 동시에 여러 작업이 일어나는 환경에서 특히 유용합니다.

      go handleWebSocket() // 이 함수는 메인과 별도로 실행됨

      그래서 우리는 Go의 chan과 goroutine 조합을 사용해 읽기와 쓰기, 에러 처리, 종료 흐름을 명확히 분리하고 구조화했습니다.


      그 결과, 내부 로직은

      • 채널에 밀어 넣고

      • 채널에서 꺼내는

        단순한 구조로 구성할 수 있었습니다.

      이처럼 데이터 흐름을 명확히 분리한 덕분에 WebSocket의 읽기/쓰기 처리뿐 아니라 에러 핸들링과 종료 처리까지도 직관적으로 구현할 수 있었고, 테스트 작성 역시 수월해졌습니다

      type RTSession struct {
          Conn *websocket.Conn
          In   chan []byte // Backend → Realtime
          Out  chan []byte // WebSocket → Realtime
      }
       
      func (s *RTSession) Listen(ctx context.Context) {
          for {
              select {
              case <-ctx.Done():
                  logger.Info().Msg("Listen: context done")
                  return
              default:
                  _, msg, err := s.Conn.ReadMessage()
                  if err != nil {
                      if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
                          logger.Info().Msg("Listen: WS closed normally")
                          return
                      }
                      logger.Error().Err(err).Msg("Listen: read error")
                      continue
                  }
                  s.Out <- msg // Realtime으로부터 메시지를 받음
              }
          }
      }
       
      func (s *RTSession) Write(ctx context.Context) {
          for {
              select {
              case <-ctx.Done():
                  logger.Info().Msg("Write: context done")
                  return
              case msg := <-s.In: // Realtimed으로 메시지를 전송
                  if err := s.Conn.WriteMessage(websocket.BinaryMessage, msg); err != nil {
                      logger.Error().Err(err).Msg("Write: write error")
                      continue
                  }
              }
          }
      }
       
      // Listen: Realtime에서 받은 메시지를 session.Out으로 계속 밀어 넣음
      go session.Listen(ctx)
       
      // Write: session.In에 들어온 메시지를 Realtime으로 계속 보냄
      go session.Write(ctx)

      이후 서비스 로직에서는 session의 In과 Out 채널을 통해 메시지를 주고받기만 하면 되므로, WebSocket 처리에 직접 관여하지 않고도 실시간 흐름을 간결하고 편리하게 구현할 수 있습니다.

      // 1. 클라이언트 오디오 입력을 Realtime API로 전송
      go func() {
          for {
              session.In <- clientAudioStream
          }
      }()
       
      // 2. Realtime API 응답을 클라이언트에 전송
      go func() {
          for msg := range session.Out {
              sendToClient(msg)
          }
      }()


      마무리

      이번 글에서는 OpenAI Realtime API가 무엇인지, 그리고 어떤 상황에서 활용할 수 있는지부터 이를 Go의 goroutine과 channel을 활용해 실시간으로 연결하는 구조까지 함께 살펴보았습니다.

      복잡할 수 있는 WebSocket 스트림도, Go의 채널을 활용하면 직관적인 흐름으로 추상화할 수 있다는 것을 확인할 수 있었고,

      이를 통해 실시간 음성 응답 흐름을 안정적이고 효율적으로 구현할 수 있었습니다.


      Realtime API는 현재 베타(Beta) 단계로, 지속적으로 발전 중인 기술입니다.

      실제 서비스에 안정적으로 적용하기 위해서는 프롬프트 설정, 보이스 옵션, 그외 여러 파라미터 등 다양한 기능들을 상황에 맞게 테스트하고, 최적의 조합을 찾아가는 과정이 필요했습니다.

      하지만 그만큼 앞으로 더 많은 기능이 추가되고, 더 나은 성능과 커스터마이징 옵션들이 제공될 것이라는 기대도 큽니다.


      Go의 channel과 goroutine은 이러한 실시간 스트리밍 구조에 매우 강력한 도구입니다.

      이번 예제를 바탕으로, 여러분의 프로젝트에서도 채널 기반의 구조를 다양하게 시도해 보시길 추천드립니다. 😄


      참고

      댓글 0

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

      지수 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기