부제: 옵티미스틱 UI의 자기 에코, 피처 플래그로 켠 게스트 채팅, 그리고 무료 arm64 OKE 위에서의 싱크 검증
이 글은 OKE 인프라 학습 시리즈의 마지막 매듭이다. arm64 노드에 실제 서비스
얹기에서 무료 클러스터에 서비스를
올리는 데까지 갔다면, 이번엔 그 위에 채팅을 붙여 실제로 메시지가 양쪽에서
같게 보이는지를 확인한다. Cloud Run에서 OKE로 굳이 다시 상시 가동 환경으로
돌아온 이유가 바로 이 지점이었다.
1. 왜 상시 가동이어야 채팅을 테스트할 수 있나
Cloud Run은 요청 기반 과금에 스케일 투 제로라, 트래픽이 없으면 노드가 아예
없다. 요청-응답 API엔 이상적이지만 채팅은 다르다. 채팅은 요청-응답이 아니라
상시 연결(WebSocket) 위에 얹히는 stateful한 기능이라, 요청 수명·콜드
스타트·연결 유지 제약 아래서는 "실제 서비스처럼" 검증하기가 어렵다.
그래서 목표를 이렇게 잡았다 — 상시 가동되는 노드 위에 채팅을 붙이고, 서로 다른
두 클라이언트가 같은 방에서 메시지를 주고받을 때 싱크가 정확히 맞는지를
눈으로 확인한다. 그 전에 두 가지를 먼저 고쳐야 했다.
2. 테스트 전에 고친 것 ① — 자기 메시지가 되돌아오는 문제
채팅 프런트는 옵티미스틱 UI를 쓴다. 사용자가 전송을 누르면 서버 응답을
기다리지 않고 화면에 메시지를 먼저 그린다. 체감 반응이 즉각적이라는 장점이
있지만, 여기엔 함정이 하나 있다.
방 게이트웨이(room-gateway)의 브로드캐스트는 방에 들어온 모든 클라이언트에게
메시지를 뿌린다. 그러면 보낸 사람에게도 자기 메시지가 되돌아온다. 프런트는 이미
옵티미스틱하게 한 번 그렸는데, 서버 에코로 같은 메시지를 한 번 더 그리게
된다. 화면에 내 말풍선이 두 개씩 뜨는 것이다.
해결은 단순하다. 브로드캐스트 루프에서 발신자만 건너뛰면 된다.
case job := <-r.queue:
r.mu.RLock()
for _, client := range r.clients {
// 보낸 사람은 이미 옵티미스틱하게 자기 메시지를 그렸으므로
// 에코를 돌려받으면 안 된다.
if client.ID == job.sender {
continue
}
client.send(job.data)
}
r.mu.RUnlock()
포인트는 "중복을 프런트에서 제거"하는 게 아니라 서버가 애초에 발신자에게
에코를 안 보내는 것이다. 프런트에서 dedup하려면 메시지에 클라이언트 임시
ID를 실어 보내고 되돌아온 걸 매칭해 지워야 하는데, 그건 상태를 양쪽에 두는
일이라 더 깨지기 쉽다. 브로드캐스트 소스에서 발신자를 빼는 한 줄이 가장 얕은
지점에서 문제를 끝낸다.
3. 테스트 전에 고친 것 ② — 게스트끼리 채팅
싱크를 테스트하려면 서로 다른 두 사람이 필요하다. 회원가입·로그인을 두 번 하는
대신, 익명 게스트끼리 바로 대화할 수 있게 열었다. 다만 이건 테스트를 위한
기능이라, 나중에 통째로 걷어낼 수 있도록 처음부터 격리해서 넣었다.
피처 플래그로 감쌌다. FEATURE_GUEST_CHAT(기본 true) 하나로 게스트 인증과
사용자 탐색을 한꺼번에 켜고 끈다. 안전 기본값이라, 환경변수를 안 넣어도 운영
동작이 그대로 유지된다.
type FeatureConfig struct {
GuestChat bool
}
func LoadFeatureConfig() FeatureConfig {
return FeatureConfig{
GuestChat: getBoolEnv("FEATURE_GUEST_CHAT", true),
}
}
게스트 인증 엔드포인트는 라우트를 지우지 않고 플래그로만 막는다. 꺼지면
라우트는 살아 있되 403을 돌려준다 — 코드 변경 없이 환경변수만 뒤집으면 기능이
사라진다.
func (h *AuthHandler) GuestAuth(c *gin.Context) {
if !h.guestChatEnabled {
middleware.RespondWithError(c, errcode.Forbidden("guest access is disabled"))
return
}
// ... 게스트 계정 발급
}
게스트 계정은 일반 유저와 역할(Role)을 분리했다. 예전엔 게스트도 USER
역할로 만들었는데, 이러면 나중에 게스트 채팅을 걷어낼 때 어느 계정이
게스트였는지 구분이 안 된다. 그래서 GUEST 역할을 따로 뒀다.
const (
RoleUser = "USER"
// 게스트 채팅을 나중에 통째로 식별·필터·퍼지하기 위해 역할을 분리한다.
RoleGuest = "GUEST"
)
마지막으로 게스트가 서로를 찾을 수 있어야 대화가 시작되니, 사용자 탐색
엔드포인트(GET /api/ums/users?q=&limit=)를 더했다. 테넌트 안에서만 조회하고,
본인은 결과에서 빼고, 페이지 크기는 50/100으로 제한했다. 본인을 제외하면 한
칸이 비니 limit+1로 하나 더 읽어 짧은 페이지가 나오지 않게 했다.
이 세 조각(플래그·역할 분리·탐색)이 모두 가역성을 염두에 둔 설계다. 실험용
기능은 실험이 끝나면 흔적 없이 빠질 수 있어야 한다. 서비스 경계를 나눠 두는
방식은 Provider 계층만 교체한다에서
정리한 헥사고날 사고와 같은 결이다.
4. 자동화 스킬로 인프라 올리기 — oke-init
채팅을 얹을 무대는 무료 arm64 OKE다. 이 클러스터는 손으로 만들지 않고, 직접
만든 oke-init 스킬로 프로비저닝했다. OCIR(레지스트리) · VCN(네트워크) · OKE
클러스터 · IAM(배포 유저/토큰)을 한 스크립트에서 만든다. GCP용 gke-init과
대칭이라, 클라우드만 바꿀 땐 이 provider 계층 스킬만 갈아 끼우면 그 위(k8s
연결·인그레스·배포)는 그대로 재사용된다.
무료 조건은 세 가지다 — basic 클러스터(관리비 $0), Ampere A1.Flex 노드(무료
한도 4 OCPU / 24GB), 춘천 리전(서울은 A1 무료 용량 부족이 잦다). A1은 arm64라,
서비스 이미지도 linux/arm64로 빌드해야 한다.
자동화라고 매끈했던 건 아니고, OCI만의 함정을 여럿 밟았다. 그 삽질을 스킬 문서에
그대로 적어 뒀다.
| 함정 | 증상 | 해결 |
|---|---|---|
| 세션 토큰 인증 | 60분 만료로 프로비저닝 10분+ 도중 끊김 | ~/.oci/config API 키로 만료 없이 전 구간 |
| IDCS username | docker login이 401 |
<네임스페이스>/oracleidentitycloudservice/<이메일> 접두어 그대로 |
| Auth Token | 발급 시 값이 딱 한 번만 표시됨 | 못 챙기면 재발급 (콘솔 목록엔 설명만 남음) |
| A1.Flex = arm64 | 노드 cri-o가 unqualified 이미지명 거부 | docker.io/library/postgres:16-alpine처럼 fully-qualified |
| 네트워크 선행 | 클러스터 생성 실패 | OCI는 클러스터보다 VCN이 먼저 (GCP와 다름) |
이 함정들과 arm64 빌드 이야기는 arm64 노드에 실제 서비스 얹기에서
더 자세히 다뤘다. 이번에 고친 두 서비스(user-management, room-gateway)는 이미지
태그를 v2로 올려 다시 밀어 넣었다.
5. 싱크 테스트 — 실험 결과
무대가 준비됐으니 본론이다. 상시 가동되는 OKE 노드 위에 배포한 채팅에, 게스트
두 명으로 접속해 같은 방에서 메시지를 주고받아 봤다. 확인하려던 건 딱 세
가지였다.
- 에코가 사라졌나 — A가 보낸 메시지가 A 화면에 두 번 뜨지 않는다(2번 수정).
- 상대에게 정확히 한 번 도착하나 — B는 A의 메시지를 빠짐없이, 중복 없이 받는다.
- 연결이 유지·복구되나 — 상시 노드라 연결이 끊기지 않고, 재접속 후에도 방이 이어진다.
결과는 의도대로 동작했다. 발신자에게 에코가 돌아오지 않아 자기 말풍선이 하나만
뜨고, 상대 클라이언트에는 메시지가 한 번씩만 도착했다. 게스트끼리 서로를 찾아
방을 열고 대화가 오가는 것까지 상시 환경에서 확인됐다. Cloud Run 시절엔 연결
수명 때문에 애매하게 끊기던 지점이, 상시 노드 위에서는 자연스럽게 유지됐다.
한 가지는 분명히 해 둔다. 이번 테스트는 기능 동작 확인 수준이다. 동시 접속
수·메시지 처리량·지연 분포 같은 부하/성능 측정은 아직이고, 그건 다음 과제로
남겨 둔다. 그래도 "상시 가동 환경에서 채팅 싱크가 실제로 맞는다"는, OKE로 옮겨
온 애초의 목적은 이 지점에서 충족됐다.
마무리
정리하면 이렇다. 옵티미스틱 UI의 자기 에코를 서버 브로드캐스트에서 발신자를
빼는 한 줄로 없앴고, 테스트를 위해 게스트끼리의 대화를 피처 플래그 뒤에
가역적으로 열었으며, 그 무대를 자동화 스킬로 올린 무료 arm64 OKE로 삼아
두 클라이언트의 메시지 싱크를 확인했다.
Cloud Run에서 OKE로 돌아온 여정은 여기서 한 매듭이 지어진다 — 상시 가동 환경에
채팅을 붙여, 실제로 싱크가 맞는 걸 눈으로 봤다. 다음은 텍스트 채팅을 넘어선
영역(WebRTC 음성/영상)인데, 그건 이번 범위 밖으로 두고 별도로 이어 간다.
관련 글: 방이 두 pod에 흩어졌을 때 — RabbitMQ fan-out과 멀티테넌시 ·
WebSocket은 무엇으로 지키나 — 보안과 셀프호스팅 MQ
참고: OCI Always Free 한도와 A1.Flex 무료 자원은 Oracle Cloud Free Tier 문서를 확인하면 된다.
답글 남기기