1. 초기 상황 — AWS 싱가포르, 복잡하게 얽힌 실험의 흔적
REINDEERS 초기 버전은 AWS ap-southeast-1 (Singapore) 리전 위에서 운영되었다. 하지만 이는 서비스 운영을 위한 구조가 아니라 "학습" 중심의 환경에 가까웠다. EC2, S3, RDS, CloudFront, Lambda 등 AWS 주요 기능이 모두 혼재했지만, 어떤 구성요소도 서로 명확히 연결되어 있지 않았다.
인프라 구성 예시 (기존 AWS)
VPC (10.0.0.0/16)
├── Subnet-Public (10.0.1.0/24)
│ ├── EC2: react-build-server
│ └── EC2: spring-api-server
├── Subnet-Private (10.0.2.0/24)
│ ├── RDS: mysql-v1
│ └── ElasticCache: disabled
└── SecurityGroup: default (80/443 open to 0.0.0.0/0)
IAM은 Root Key로 접근했고, Auto Scaling Group은 설정만 되어 있을 뿐 실제 트리거가 비활성화되어 있었다. CloudFront는 S3 버킷에 연결되어 있었지만, TTL(캐시 수명) 설정이 24시간으로 고정되어 실시간 반영이 어려웠다. API 응답 속도는 태국에서 평균 620ms, 말레이시아에서 710ms로 측정됐다. 이 수치는 글로벌 B2B 플랫폼으로는 치명적이었다.
감사(audit) 과정에서 가장 먼저 확인한 것은 인프라의 가동률이었다. Auto Scaling이 비활성화되어 있다는 것은, 트래픽 증가에 대한 대응 능력이 사실상 없다는 의미였다. 태국과 말레이시아의 바이어가 동시에 접속하는 시간대(오전 9시~11시 현지 시간)에 서버 부하가 집중되었고, 이때 간헐적으로 타임아웃이 발생하고 있었다. SecurityGroup이 default로 설정되어 있다는 점도 문제였다. 포트 80/443이 전체 IP에 개방된 상태는 운영 환경에서 용납될 수 없는 구성이다.
2. 기술 스택 한계 — React + Java 구조의 제약
프런트엔드는 React, 백엔드는 Java(Spring Boot) 조합이었다. 초기 구축 목적은 "기능 구현 속도"였으나, 이 구조는 시간이 지날수록 관리 복잡성이 기하급수적으로 커졌다.
문제점 요약
- React: SSR(Server-Side Rendering) 미지원 → SEO 불가, 첫 페이지 로딩 지연
- Java(Spring Boot): API 버전별 배포가 불가, 빌드 속도 느림(평균 4~6분)
- 프런트와 백이 강하게 결합되어, 코드 한 줄 수정에도 전체 배포 필요
- 다국가 언어(i18n), 화폐, 세율 관리가 하드코딩 상태
당시 코드는 기능 단위로는 "완성"되어 있었지만, 운영 단위로는 "분리 불가능한 덩어리"였다. 서버 장애가 발생하면 전체 서비스가 멈췄다.
코드 감사를 진행하면서 기술 부채의 규모를 정량적으로 파악하려 했다. 결과는 예상보다 심각했다. i18n 처리가 프런트엔드 컴포넌트 곳곳에 문자열 리터럴로 흩어져 있었고, 태국어/한국어/중국어/영어를 지원하려면 수백 개의 파일을 일일이 수정해야 하는 구조였다. 통화(THB, KRW, CNY, MYR, USD) 변환 로직도 마찬가지였다. 환율 값이 코드 내부에 상수로 박혀 있는 곳이 여러 군데 발견되었다. 이런 구조에서 4개국 동시 운영은 불가능하다.
3. 감사 방법론 — 어떻게 4년치 코드를 평가했는가
코드 감사는 단순히 소스를 읽는 것이 아니었다. 우리는 세 가지 축으로 기존 시스템을 평가했다.
- 운영 가능성(Operability): 장애 발생 시 자동 복구가 가능한가? 모니터링이 존재하는가?
- 확장 가능성(Scalability): 새로운 국가, 통화, 언어를 추가할 때 코드 변경 범위는 어느 정도인가?
- 유지보수성(Maintainability): 새로운 개발자가 합류했을 때 이 코드를 이해하고 수정할 수 있는가?
각 축에 대해 1~5점 척도로 평가한 결과, 세 항목 모두 2점 이하였다. 모니터링 시스템이 사실상 없었고, 장애 알림은 사용자 민원이 들어와야 인지되는 수준이었다. 새 국가를 추가하려면 프런트엔드, 백엔드, 데이터베이스 전부를 건드려야 했다. 문서화는 거의 되어 있지 않았고, 코드 주석도 최소한이었다.
특히 심각했던 것은 데이터 정합성 문제였다. 견적(Quote)에서 주문(PO)으로 이어지는 흐름에서 금액 불일치가 발견되었고, 이는 환율 변환 로직이 프런트엔드와 백엔드에서 각각 별도로 계산되고 있었기 때문이었다. 동일한 거래인데 화면에 표시되는 금액과 서버에 저장된 금액이 다른 경우가 존재했다. 이런 종류의 버그는 B2B 무역 플랫폼에서 치명적이다. 바이어와 공급사 간 신뢰 문제로 직결되기 때문이다.
4. 리팩토링 vs 리라이트 — 왜 새로 만들기로 했는가
감사 결과를 놓고 두 가지 선택지를 비교했다. 기존 코드를 점진적으로 개선하는 리팩토링과, 처음부터 다시 만드는 리라이트. 일반적으로 리라이트는 위험한 선택이다. 기존 시스템이 가진 암묵적 지식(implicit knowledge)을 잃을 수 있기 때문이다.
그러나 우리의 경우는 달랐다. 첫째, 기존 시스템의 아키텍처가 우리가 목표로 하는 구조와 근본적으로 달랐다. 단일 리전, 모놀리식 배포, 하드코딩된 다국어/다통화 처리. 이것들을 점진적으로 바꾸는 것은 사실상 전체를 다시 만드는 것과 비용이 같았다. 둘째, 11년간 축적된 25,000건 이상의 거래 데이터와 4,300개 이상의 파트너 데이터는 코드가 아니라 데이터베이스에 있었다. 코드를 새로 쓰더라도 핵심 자산인 데이터는 보존할 수 있었다.
셋째, 팀 자체가 교체되는 시점이었다. 기존 코드에 익숙한 개발자가 남아 있지 않은 상황에서, 새 팀이 낯선 레거시 코드를 해독하며 리팩토링하는 것보다 검증된 아키텍처 위에 새로 구축하는 것이 시간과 품질 모두에서 유리했다.
5. 백엔드 정책 — 내부 전용(Private Service Layer)
이번 개편에서 우리는 백엔드를 완전히 비공개 구조로 전환했다. 외부에서 접근할 수 있는 공개 API는 존재하지 않으며, 모든 통신은 내부 게이트웨이를 거쳐야 한다. 내부 인증은 JWT + IAM Key로 제한되며, 외부 방화벽(WAF)과 보안그룹으로 이중 보호된다.
백엔드 접근 구조 (Simplified)
[ Client (Nuxt/Vue3) ]
↓ HTTPS (443)
[ CDN Edge (COS/CDN) ]
↓ API Proxy (Internal Gateway)
[ Private Network ]
↓
[ API Services - Internal Only ]
├── User Service
├── Order Service
├── Logistics Service
└── Payment Service (Private Endpoint)
모든 API는 서브도메인 구분 없이 내부 DNS로만 라우팅된다. 외부에서 백엔드 서버에 직접 접근하는 것은 기술적으로 불가능하다. 이 결정의 배경에는 B2B 플랫폼의 보안 요구사항이 있다. 거래 금액, 파트너 정보, 물류 데이터 등 민감한 비즈니스 데이터가 오가는 환경에서 공개 API 엔드포인트를 노출하는 것은 공격 표면(attack surface)을 불필요하게 넓히는 것이다.
6. 프런트엔드 전면 교체 — Nuxt + Vue3로 재구성
새로운 프런트엔드는 Nuxt (3.x) + Vue3를 기반으로 했다. 이 조합은 SSR, CSR, SSG를 모두 지원하며, 글로벌 환경에 적합하다.
Nuxt 설정 예시
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
modules: ['@nuxtjs/i18n', '@pinia/nuxt', '@nuxt/image'],
i18n: {
locales: ['ko', 'en', 'th', 'zh'],
defaultLocale: 'en',
vueI18n: './i18n.config.ts'
},
runtimeConfig: {
public: {
apiBase: '/api'
}
}
})
빌드 후 생성된 정적 자산은 /.output/public 경로에 저장되고,
COS로 업로드된다.
이후 CDN Edge에서 자동 캐싱되어 지역별로 빠르게 반영된다.
SSR 도입은 SEO 측면에서 결정적이었다.
B2B 플랫폼에서 검색 엔진 노출은 신규 바이어 유입의 핵심 채널이다.
React 기반의 CSR(Client-Side Rendering) 구조에서는 검색 엔진이 콘텐츠를 제대로 인덱싱하지 못했다.
Nuxt의 SSR/SSG 하이브리드 렌더링으로 이 문제를 근본적으로 해결했다.
7. CI/CD 전환 — 완전 자동화된 배포
기존 Shell Script 기반의 배포는 유지할 수 없었다. Drone을 도입해 완전 자동화된 파이프라인을 구축했다. 빌드-테스트-배포-캐시 무효화까지 단일 YAML로 관리된다.
Drone 파이프라인 (요약)
kind: pipeline
type: docker
name: nuxt-deploy
steps:
- name: build
image: node:20
commands:
- npm ci
- npm run build
- name: upload
image: tencentcloud/tencentcloud-cli
environment:
COS_BUCKET: reindeers-fe
COS_REGION: ap-hongkong
commands:
- tccli cos PutObject --bucket $COS_BUCKET --region $COS_REGION \
--file ./.output/public --key /
- name: purge-cache
image: alpine/curl
commands:
- curl -X POST https://cdn.tencentcloudapi.com/purge
Drone은 Git push 이벤트에 의해 자동으로 트리거되며, COS 업로드 후 CDN 캐시를 무효화한다. 전체 배포 소요 시간은 평균 10~12초 이내다. 기존에는 배포 한 번에 개발자가 SSH로 서버에 접속하고, 수동으로 빌드 스크립트를 실행하고, 결과를 눈으로 확인해야 했다. 이 과정에서 휴먼 에러가 빈번했고, 배포 이력 추적도 불가능했다. 자동화된 파이프라인은 이 모든 문제를 제거했다.
8. 클라우드 전환 — AWS에서의 이전
클라우드 전환은 단순한 이동이 아니라, 글로벌 네트워크 성능과 운영 효율성의 재설계였다.
| 항목 | AWS (기존) | 현재 |
|---|---|---|
| 리전 | ap-southeast-1 (Singapore) | ap-hongkong (Main) / ap-seoul (DR) |
| 스토리지 | S3 + 수동 업로드 | COS + 자동 업로드 |
| CDN | 수동 캐시 관리 | 자동 캐시 무효화 |
| DB | 단일 RDS | DTS 기반 리전 간 복제 |
| DNS | Route53 | DNSPod Geo Routing |
| 배포 | Shell Script | Drone CI/CD 자동화 |
결과적으로 태국과 말레이시아 지역의 평균 응답 속도는 650ms → 180ms로 단축되었다. 또한 데이터베이스 복제와 DR 구조를 통해 장애 복구 시간을 1분 이내로 줄였다. 클라우드 전환을 결정한 핵심 이유는 타겟 시장과의 물리적 거리였다. 태국, 한국, 중국, 말레이시아 4개국을 동시에 서비스하려면 싱가포르 단일 리전으로는 네트워크 지연을 해결할 수 없었다. 홍콩을 메인 리전으로 선택한 것은 4개국 모두에 대해 균형 잡힌 네트워크 경로를 제공하기 때문이다.
9. 스타트업에서 기술 부채가 쌓이는 방식에 대한 교훈
4년간의 코드를 감사하면서 느낀 것이 있다. 기술 부채는 "나쁜 결정"에서 오는 것이 아니다. "결정을 내리지 않는 것"에서 온다. 초기 스타트업은 빠른 구현이 최우선이고, 그 판단 자체는 틀리지 않았다. 문제는 그 이후다. 임시로 만든 구조가 영구적인 표준이 되어버리고, 누구도 "이제 이것을 정리해야 한다"는 결정을 내리지 않는 동안 부채는 복리로 쌓인다.
REINDEERS의 경우, 2015년 태국에서 IMARKET Thailand로 시작한 이후 팀이 네 번 교체되면서 각 팀이 자신만의 방식으로 코드를 추가했다. 통일된 코딩 컨벤션이 없었고, 아키텍처 결정 기록(ADR)도 없었다. 왜 이 기술을 선택했는지, 어떤 트레이드오프를 고려했는지에 대한 맥락이 완전히 사라진 상태였다. 새 팀이 합류할 때마다 기존 코드를 이해하는 데 수 주가 걸렸고, 이해하지 못한 채 위에 새로운 코드를 쌓아 올리는 패턴이 반복되었다.
10. 결론 — 5%의 현실, 95%의 가능성
점검을 통해 드러난 현실은 냉정했다. 우리가 가진 것은 완성된 플랫폼이 아니라, 실험의 결과였다. 그러나 그 5%의 코드가 나머지 95%를 설계할 수 있는 기반이 되었다. 정확히 말하면, 코드 자체가 아니라 코드를 통해 축적된 도메인 지식이 자산이었다. B2B 무역에서 견적-주문-인보이스-정산이 어떻게 흘러가는지, 4개국의 세율과 통관 규정이 어떻게 다른지, 바이어와 공급사 사이의 커뮤니케이션에서 어떤 마찰이 발생하는지. 11년간 25,000건 이상의 실제 거래에서 얻은 이 지식은 어떤 코드베이스보다 가치 있는 자산이었다.
- 클라우드: AWS 싱가포르 → 홍콩(Main) / 서울(DR)
- 프런트: React → Nuxt + Vue3
- 배포: Shell Script → Drone CI/CD
- 스토리지: S3 → COS + CDN 자동화
- DB: 단일 RDS → DTS 기반 리전 간 복제
- DNS: Route53 → DNSPod Geo Routing
- 백엔드: Private Service Layer, 공개 API 없음
이 점검은 단순한 기술 감사가 아니었다. "우리가 어디에 있는가"를 정직하게 인정하고, "어디로 가야 하는가"를 설계하기 위한 출발점이었다. 4년의 코드를 다시 열어본 결과, 우리는 코드를 버리고 지식을 가져가기로 했다.