중고나라 기술 블로그 최신 글
teamblog.joonggonara.co.kr에서 발행한 기술 글을 테크곳간이 수집해 최신순으로 보여드립니다.
최신 글
웹 서버가 터졌다: Next.js SSR에서 TanStack Query gcTime이 만든 OOM
0. 들어가며 안녕하세요. 중고나라 웹개발팀입니다. 현재 중고나라 웹개발팀은 웹 서비스를 리뉴얼하는 과정에서, 기술 스택을 변경하는 큰 규모의 마이그레이션을 진행했습니다. (Next.js Page Router → App Router, React 18 → 19) 리뉴얼을 마치고 출시 전 부하 테스트를 돌렸는데 문제가 터졌습니다. 웹 서버에서 OOM이 발생하는 것입니다. 그래프를 보면 단순한 Memory Peak가 아니었습니다. 메모리가 GC 되지 않고 계속 쌓이는 패턴이었습니다. Memory Leak이 발생해 Pod OOM Killed 확인 (Datadog) 원인을 추적한 결과, Server Component에서 TanStack Query(React Query)의 QueryClient를 클로저가 오랫동안 참조하고 있어 GC되지 못하는 것이 문제였습니다. 이 글에서는 이 메모리 이슈의 원인과 트러블슈팅을 공유하고, 문제의 원인이었던 TanStack Query의 gcTime이 왜 문제가 되었는지 분석한 내용을 설명하고자 합니다. 1. 사전 배경: App Router + TanStack Query SSR 구조 본격적인 트러블슈팅에 앞서, 문제가 발생한 코드를 먼저 짚고 넘어가겠습니다. Next.js App Router에서는 layout과 page가 동일한 요청 스코프 내에서 렌더링됩니다. 이 구조에서 React.cache()로 감싼 QueryClient를 요청 단위로 공유하고, 서버 컴포넌트에서 데이터를 prefetch한 뒤 dehydrate를 통해 클라이언트에 전달하는 패턴을 사용하고 있었습니다. const getQueryClient = React.cache(() => new QueryClient()); React.cache()로 감싼 함수는 동일 요청 내에서 같은 값을 반환하므로, 하나의 요청에서는 하나의 QueryClient가 공유됩니다. 요청이 끝나면 이 QueryClient는 자연스럽게 GC 대상이 되는데, 이 글의 핵심은 바로 이 전제가 깨지는 상황입니다. 전역 layout에서 렌더링되는 GNB 헤더에서, 이 QueryClient를 통해 GNB API를 prefetch하는 구조였습니다. import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; // 서버 컴포넌트 GnbHeader에서 gnb API 응답 캐시 export default async function GnbHeader() { const queryClient = getQueryClient(); await queryClient.prefetchQuery({ queryKey: ["gnb"], staleTime: 1000 * 60 * 30, // 30분 gcTime: 1000 * 60 * 60, // 1시간 queryFn: () => fetchGnb(), }); return ( <HydrationBoundary state={dehydrate(queryClient)}> <HeaderView /> </HydrationBoundary> ); } (fyi. 신규 웹은 TanStack Query v5를 이용하고 있습니다.) GNB는 자주 변하지 않으니 캐시를 오래 유지하겠다는 의도였습니다. 페이지 레벨에서도 동일한 QueryClient를 통해 검색·상품 상세 등의 API를 prefetch했습니다. 즉, 하나의 HTTP 요청 안에서 GNB prefetch, 검색 결과, 상품 상세 등 모든 query 데이터가 하나의 QueryClient 인스턴스에 누적되는 구조였습니다. 정리하면 아래와 같습니다. QueryClient는 React.cache()로 감싸 request scope 내에서 재사용된다. GNB query에 긴 gcTime(1시간)이 설정되어 있다. app/layout.tsx └── Header.tsx (QueryClient 생성 + GNB 캐시 gcTime 1시간) └── page.tsx (동일 QueryClient 재사용, 검색/상품 API 캐시 추가) 기능적으로는 아무 문제가 없었기에, 별다른
2026년 3월 4일
웹개발팀 모노레포 — 2) Husky 설정
웹개발팀 모노레포 — 2) Husky 설정 TL;DR 1. 모노레포에서 Husky를 적용해 Git Hook을 자동화했습니다. 2. post-checkout, pre-push, pre-commit Hook을 구성해 로컬 단계에서 품질 검증을 강제했습니다. post-checkout: lockfile 변경 시 자동으로 의존성을 재설치 pre-push: 의존성이 변경된 앱이 정상적으로 빌드되는지 검사 pre-commit: lockfile 상태 검증 및 린트 실행으로 코드 품질 보장 3. 이를 통해 팀 전체가 일관된 규칙을 준수하며 코드 품질을 향상시킬 수 있었습니다. 서론 안녕하세요, 중고나라 웹개발팀 홍승연입니다. 지난 1편에서 모노레포 파이프라인을 소개했다면, 이번 글에서는 모노레포 환경에서의 Husky 기반 Git Hook 설정을 공유하고자 합니다. 모노레포 환경에서는 여러 서비스와 패키지가 한 리포지토리에 모여 있기 때문에 작은 실수도 전체 파이프라인에 영향을 줄 수 있습니다. 예를 들어 린트 규칙을 위반하거나 lockfile을 갱신하지 않은 채 커밋하면, 빌드 에러나 예기치 못한 버그로 이어질 수 있습니다. 이를 방지하기 위해 웹개발팀은 Husky를 도입해 Git Hook을 자동화하고, 팀 전체가 일관된 규칙을 지키도록 했습니다. 이번 글에서는 적용한 Git Hook 소개 및 설정 방법을 공유하고자 합니다. Husky ? Husky는 Git Hook을 쉽게 설정하고 관리할 수 있도록 도와주는 도구입니다. Git Hook이란, 특정 Git 이벤트(예: commit, push, checkout)가 발생할 때 자동으로 실행되는 스크립트를 말합니다. 원래는 .git/hooks 디렉터리에 직접 스크립트를 작성해야 하지만, Husky를 사용하면 설정이 단순해지고, 팀 단위로 일관된 규칙을 공유하기 쉬워집니다. 적용한 Hook 소개 모노레포에서는 post-checkout, pre-push, pre-commit 세 가지 Hook을 구성했습니다. 아래에서는 각 Hook을 도입한 이유와 동작 방식을 차례대로 설명하겠습니다. (모노레포 개발 환경 — Pnpm v8, Turborepo v2, Node v18) 1. post-checkout 첫 번째로 적용한 Hook은 브랜치를 전환하거나 특정 커밋을 체크아웃할 때 실행되는 post-checkout입니다. 이 단계에서는 lockfile(pnpm-lock.yaml) 변경 시 자동으로 의존성을 재설치하도록 설정했습니다. [설정 배경] 의존성 관리 자동화 모노레포에서는 여러 서비스와 패키지가 함께 관리되다 보니 package.json 업데이트가 잦습니다. 그래서 패키지가 업데이트된 사실을 모른 채 작업하다가 로컬 실행 에러가 나는 경우가 많았습니다. 매번 수동으로 설치하기 번거로웠기 때문에, checkout 시 의존성을 자동으로 설치하도록 Hook을 추가했습니다. [동작 방식] post-checkout Hook은 다음과 같이 동작합니다. 1. 브랜치/커밋 변경 시점 확인 git checkout 시, 체크아웃 직전 커밋(HEAD@{1})와 현재 커밋(HEAD)을 비교합니다 2. lockfile 변경 여부 검사 두 커밋 간 diff에서 pnpm-lock.yaml 변경 여부를 확인합니다. 3. 조건에 따른 분기 처리 변경 O → pnpm install을 실행해 의존성을 재설치 변경 X → 설치 과정 생략 lockfile이 변경되면 자동으로 의존성을 설치해, 불필요한 설치 시간을 줄이고 개발 생산성을 높일 수 있었습니다. [코드] .husky/post-checkout # 브랜치/커밋 변경 시 pnpm-lock.yaml 변경 여부 확인 if git diff --name-only HEAD@{1} HEAD | grep -q 'pnpm-lock.yaml'; then echo "pnpm-lock.yaml has changed. Running pnpm install..." pnpm install else echo "pnpm-lock.yaml unchanged. Skipping install." fi 2. pr
2025년 9월 11일
웹개발팀 모노레포 — 1) 파이프라인 구축
웹개발팀 모노레포 — 1) 파이프라인 구축 TL;DR 중고나라 웹개발팀은 멀티레포를 모노레포로 전환하며 파이프라인을 구축했습니다. 의존성 설치 시간을 줄이기 위해 pnpm store 캐시를 설정했습니다. Turborepo + Bitbucket Pipelines 기반으로 변경된 앱만 추출해 빌드/배포했습니다. Docker 빌드 시, 앱별 필요한 의존성만 포함해 이미지 크기와 빌드/캐시 효율을 높였습니다. 서론 안녕하세요, 중고나라 웹개발팀 홍승연입니다. 중고나라 웹개발팀은 기존 멀티레포로 운영하던 프로젝트를 모노레포로 전환하고 있습니다. 모노레포는 여러 프로젝트를 하나의 리포지토리에서 관리하는 방식으로, 코드 중복을 줄이고 공통 모듈을 쉽게 재사용할 수 있게 합니다. 또한 빌드와 배포를 일관된 파이프라인으로 묶어 개발 생산성을 높여줍니다. 기존에는 각 프로젝트가 별도의 리포지토리에서 관리되어 공통으로 사용하는 코드를 재사용하기 어려웠고, 프로젝트마다 별도의 설정 및 관리가 필요했습니다. 이에 공통 페이지는 웹뷰로 배포해 여러 서비스에서 iframe으로 연동했지만, iframe 특성상 UI가 제한적이고 확장성이 낮다는 문제가 있었습니다. 또한 별도 프로젝트로 관리하다 보니 관리포인트가 늘어나면서 팀 내 피로감이 증가하고 있었습니다. 이러한 문제를 해결하기 위해 모노레포로 전환을 결정했고, 크게 아래 4가지 작업을 차례대로 진행하였습니다. 파이프라인 구축 — 웹 서비스 배포를 위해 CI/CD 파이프라인 구축 Husky 세팅 — Husky로 Git Hook을 자동화해, 팀 전체가 일관된 개발 규칙을 준수 인증 통합 — 서비스별로 분산된 인증 로직 통합 도메인 모듈 분리 — 도메인별 모듈화로 재사용성 강화 이 글은 첫 번째 주제인 “파이프라인 구축”에 대해 다루며, 나머지 주제들도 순차적으로 공유할 예정입니다. 파이프라인 구축 먼저 파이프라인을 어떻게 구축했는지 설명하겠습니다. 중고나라는 여러 웹 애플리케이션을 운영합니다. 모노레포로 이관하면서 배포 대상이 늘었고, 공통 패키지 한 번만 바꿔도 여러 앱이 연쇄적으로 설치/빌드됩니다. 그래서 모노레포 파이프라인은 빨라야 했습니다. 따라서 파이프라인 구축시 속도 최적화를 위해 세 가지를 중점적으로 고민했습니다. pnpm store 캐시를 통해 의존성 설치 시간 최적화 의존성이 변경된 앱만 필터링해 빌드/배포 Docker 이미지 빌드 시 앱별 필요한 의존성만 포함 아래에서 각 항목의 구현 방법을 차례대로 설명하겠습니다. 1. pnpm store 캐시로 의존성 설치 최적화 모노레포 CI/CD에서 시간이 오래 걸리는 단계 중 하나가 의존성 설치입니다. 매 빌드마다 새로 설치하지 않도록 다운로드한 의존성을 캐시했습니다. 일반적으로는 node_modules를 캐시하지만, 모노레포에서는 pnpm을 쓰고 있으므로 pnpm store(전역 스토어)를 캐시했습니다. 그 이유는 아래와 같습니다. pnpm store를 캐시하는 이유? pnpm은 다른 패키지 매니저와 달리 3단계 링크 구조를 사용합니다. 전역 스토어: 모든 패키지를 저장하는 실제 데이터 저장소 가상 스토어(node_modules/.pnpm): 워크스페이스별 링크 관리 실제 node_modules: 전역 스토어를 가리키는 하드링크/심볼릭 링크 집합 pnpm은 패키지 파일을 content-addressable 방식으로 전역 스토어에 보관하고, 각 워크스페이스의 node_modules는 그 스토어를 링크로만 재구성합니다. 즉, 전역 스토어만 보존되면 재설치는 대부분 링크 재구성으로 끝나고, 불필요한 다운로드를 크게 줄일 수 있습니다. 반대로 node_modules 자체 캐시는 경로/OS 변경, 네이티브 바이너리 차이 등에 취약해 쉽게 무효화되거나 오염될 수 있습니다. 이는 pnpm의 링크 구조와 중복 제거 메커니즘 때문입니다. pnpm 공식 이슈에 따르면, node_modules를 캐시할 때 두 가지 문제가 발생할 수 있습니다. node_modules 대신 pnpm store만 캐시해야 하는 이유 링크 구조 손상 위험: CI에서 압축/복
2025년 9월 8일
데이터브릭스 도입을 통한 중고나라의 ML 플랫폼 전환기-2편
들어가며 지난 1편에서 중고나라의 ML 플랫폼 전환에 대한 간단한 소회를 다루었으니, 이번에는 데이터브릭스의 각종 서비스를 활용한 MLOps 예시에 대해 이야기 해 보도록 하겠습니다. 현대의 머신러닝 서비스는 단순히 모델을 학습하고 배포하는 것을 넘어서, 지속적인 모니터링, 자동화된 재학습, 그리고 안정적인 운영이 핵심입니다. 특히 실제 서비스 환경에서는 데이터 드리프트, 모델 성능 저하, 그리고 빠르게 변화하는 비즈니스 요구사항에 대응할 수 있는 유연한 MLOps 파이프라인이 필수적입니다. 이번 글에서는 MLOps를 구성하는 핵심 요소들을 실제 서비스 사례와 함께 살펴보겠습니다: 모델 학습: Unity Catalog, MLflow, Transformers를 활용한 체계적인 모델 개발 및 실험 관리 모델 서빙: 실시간 추론 서비스 구축과 성능 최적화 전략 모델 드리프트 탐지 & 재학습 자동화: 지속적인 모델 성능 관리와 자동화된 운영 체계 모델 학습/서빙 우선, 간단한 모델링 및 서빙에 대해 알아보겠습니다. 이번 유즈케이스의 메인 주제는 저희 머신러닝팀에서 운영하고 있는 것들 중 가장 간단한 서비스인 ‘상품명 기반 카테고리 추천’입니다. 문제 인식 & 서비스 기획 중고나라는 현재 500개 이상의 상품 카테고리를 보유하고 있습니다. 이는 사용자에게 다양한 선택권을 제공한다는 장점이 있지만, 동시에 상품 등록 과정에서 어떤 카테고리를 선택해야 할 지 고민하게 만드는 큰 장벽으로 작용하고 있었습니다. 이러한 고객 불편을 개선하기 위해 다음과 같은 기능을 수행하는 ML 서비스를 기획하게 되었습니다: 상품명 입력 → 실시간 카테고리 Top 5 추천 사용자가 추천된 카테고리 중 선택하거나, 기존 방식으로 직접 선택 가능 추천 결과에 대한 피드백 수집을 통한 지속적인 모델 개선 상기한 서비스를 만족시키기 위하여 Text Classification (NLP) 기반 ML/DL 모델을 활용하되, 실시간성/가용성 등을 고려할 필요가 있었습니다. 종합적으로 고려한 끝에 작은 사이즈의 Transformer 아키텍처 기반의 모델을 Fine-tuning 하는 것으로 가닥을 잡게 됩니다. 데이터 준비 상품 제목-카테고리 쌍의 학습 데이터 준비를 위해 데이터브릭스 내에서 Medallion Architecture(Bronze-Silver-Gold)를 구현했습니다. 이는 단순한 데이터 저장소가 아닌, 데이터 품질과 거버넌스를 보장하는 체계적인 데이터 관리 시스템으로, 이번 프로젝트에서 각 파트의 상세사항은 다음과 같습니다: Bronze(원천 데이터): 지난 n년간 축적된 상품 등록 데이터, 카테고리 매핑 데이터 Silver(전처리된 데이터): Bronze 데이터에서 필요 최소한의 전처리가 적용된 데이터 Gold(학습 데이터): Silver 데이터를 조합하여 창출한 비즈니스(모델 학습용) 데이터 학습 준비 학습 데이터가 준비 되었으니, 본격적인 모델 학습에 들어갈 차례입니다. 본 예제에선 다음과 같은 라이브러리들을 활용합니다: Hugging Face 라이브러리들: NLP, 특히 Transformer 아키텍처 기반 모델의 훈련 코드를 빠르게 구현할 수 있으며, 후술할 MLflow와의 연계 난이도 또한 매우 쉽기에 채택했습니다. MLflow: 데이터브릭스에서 기본적으로 제공해주는 관리형 MLflow 서비스를 이용하여 체계적인 실험/모델 관리를 간편하게 할 수 있기에 채택했습니다. 데이터/모델 로드 mlflow의 load_delta 메서드를 이용해 준비된 Gold 테이블을 불러온 뒤, 불러온 스파크 데이터프레임을 Hugging Face의 Dataset 규격으로 변환해 줍니다. import mlflow from datasets import Dataset mlflow.set_tracking_uri("databricks") # Databricks Managed MLflow 사용 mlflow.set_registry_uri("databricks-uc") # Unity Catalog 모델 레지스트리를 사용 train_dataset_delta = mlflow.data
2025년 6월 9일
Istio Ambient 도입 후기
도입 배경 안녕하세요. 중고나라 DevOps 엔지니어 조상현, 김지헌입니다. 복잡한 아키텍처를 개선하며, 자원 효율화를 위해 새롭게 도입한 Istio Ambient Mode에 대해 공유하려 합니다. 중고나라는 회사의 성장과 함께 서비스 트래픽이 증가하면서 네트워크 관리의 중요성이 더욱 커졌습니다. 이를 해결하기 위해 Istio를 도입하여 서비스 메쉬를 구축했지만, 기존 Istio Sidecar Mode 기반 아키텍처는 몇 가지 한계를 보였습니다. Why Ambient Mode? 간단하게 기존의 Sidecar Mode 형태의 아키텍처를 간단하게 소개하면 아래와 같습니다. 큰 특징으로는 각 Pod마다 Envoy proxy를 Sidecar로 주입하여 트래픽을 전달하는 방식입니다. 하지만 Sidecar Mode는 리소스 사용량 증가와 운영 복잡도 같은 문제를 야기했습니다. 리소스 증가 각 Pod마다 Envoy Proxy를 Sidecar로 주입하므로, 각 Pod의 컨테이너 개수는 (애플리케이션 컨테이너 1개 + Sidecar Proxy 컨테이너 1개)로 총 2개가 됩니다. 따라서 Pod 수가 증가할수록 CPU와 Memory 사용량(요청량)이 크게 증가했고, 이는 전체적인 인프라 비용 증가로 이어졌습니다. 2. 운영 복잡도 Pod에 문제가 발생했을 때, 애플리케이션 컨테이너의 문제인지 Envoy Proxy의 문제인지 즉시 구분하기 어려웠습니다. Envoy Proxy와 애플리케이션이 강하게 결합(Coupling)된 형태여서, 하나의 문제로 인해 전체 Pod의 장애로 확대되는 경우가 많았습니다. 저희는 이러한 문제를 해결하기 위해 새롭게 출시한 Ambient Mode로 전환하는 결정을 내렸습니다. 새롭게 도입한 Ambient Mode 아키텍처를 간단하게 소개하면 아래와 같습니다. Ambient Mode는 기존의 Sidecar Mode와 달리, Envoy Proxy Sidecar의 역할을 Layer 4(ztunnel)와 Layer 7(waypoint)로 분리하여 네트워크 트래픽을 처리하는 방식으로 모든 pod 간 통신은 ztunnel 을 통하고, L7 기반 로직 처리 필요 시 waypoint 를 경유합니다. 이러한 구조적 변화로 인해 인프라 운영이 더욱 가볍고 효율적으로 설계되었습니다. key point는 아래와 같습니다. 리소스 절감 Pod마다 Envoy Proxy Sidecar가 존재하지 않으므로 CPU와 Memory 사용량(요청량)이 감소하였고, 더 적은 Worker Node로 동일한 트래픽을 처리할 수 있게 되었습니다. 2. 운영 단순화 Istio의 정책을 별도의 Proxy(ztunnel, waypoint)로 관리하여, 네트워크 정책 적용과 디버깅이 더욱 간편해졌습니다. 애플리케이션을 Service Mesh에 포함하거나 제외할 때, 애플리케이션 Pod 재시작할 필요가 없습니다. Istio 버전을 업그레이드 할 때, 애플리케이션 Pod를 재시작할 필요가 없습니다. 물론 Ambient Mode 전환 과정에서 기존 Sidecar Mode와의 호환성을 고려해야 했으며, 트래픽 라우팅 및 보안 정책이 기대한 대로 동작하는지 충분한 테스트가 필요했습니다. 다음으로, Ambient Mode 전환 과정에서 맞닥뜨린 문제와 이를 어떻게 해결했는지, 그리고 현재 운영 방식에 대해 더 자세히 풀어보겠습니다! 도입 과정 Helm + Argo를 통한 Istio Ambient Mode 설치 초기에 Istio를 도입할 당시, 중고나라는 Istio Operator를 활용하여 배포 및 운영을 진행하였습니다. 그러나 Istio 1.18 버전 이후 Operator 기반의 방식이 Deprecated됨에 따라, 기존 방식으로는 더 이상 업그레이드가 불가능한 상황이 되었습니다. 이에 따라 다양한 배포 방법을 검토하였으며, 잦은 업그레이드, 커스텀 설정, GitOps 등 유연한 운영이 가능한 방법을 찾고자 하였습니다. 그 결과, Helm을 활용하여 환경별 세부 설정을 커스텀하고, Argo CD 기반의 GitOps 배포 방식을 도입하여 지속적인 운영 및 유지보수를
2025년 3월 20일
2024 AWS re:Invent 후기
시작하며 안녕하세요! 중고나라 DevOps 엔지니어 김지헌, ML 엔지니어 김준재입니다. AWS re:Invent는 세계 최대의 클라우드 컴퓨팅 컨퍼런스 이 행사는 AWS와 관련된 다양한 최신 기술, 서비스, 업데이트를 소개하고, 클라우드 산업의 트렌드와 혁신 사례를 공유하는 자리로 2024 AWS re:Invent에 참가하게 되는 좋은 기회를 얻어, 이 경험을 공유하고자 합니다. 이번 글에서는 re:Invent를 더 알차게 즐기고자 하는 분들, 특히 다음 번에 참가하실 분들께 유용한 팁을 전해드리겠습니다! “세상은 넓고, 라스베가스는 더 넓다!” AWS re:Invent는 라스베가스에 위치한 무려 6개 호텔에서 다양한 세션이 진행됩니다. 호텔 간 거리가 최대 1시간 정도 걸릴 수 있기 때문에, 하루에 2만 보 이상 걷는 것은 기본입니다. 즉, 체력 관리와 동선 계획이 성공적인 re:Invent의 핵심이라 할 수 있습니다! 호텔 간 이동: 무료 트램과 셔틀 버스 활용하기! AWS에서는 참가자들의 이동을 돕기 위해 호텔 간 무료 트램과 무료 셔틀 버스를 제공합니다. 하지만 여기서 중요한 점! 셔틀 버스: 평균 30분 정도 시간이 소요될 수 있습니다. (모든 호텔 이동 가능) 무료 트램: 트램간 대기 시간이 4~8분이 필요하며 이동 시간도 있어 계획이 엉킬 수 있습니다.(트램 정보) (특정 호텔만 이동 가능 확인 필수!) 👉 TIP: 세션 신청 시 이동 시간을 꼭 고려하세요! 세션이 끝난 후 다음 세션 장소까지 이동 시간이 얼마나 걸릴지 사전에 계산하는 것이 중요합니다. 시간이 빠듯하다면 같은 호텔 안에서 열리는 세션을 연달아 신청하거나, 일부러 쉬는 시간을 넣는 것도 좋은 전략입니다. 세션 예약? Full Book? 용사여, 포기하지 마라! 용기를 내라! AWS re:Invent에 참가하기 전에 가장 먼저 해야 할 일은 어떤 세션이 열리는지 확인하고, 미리 예약하는 것입니다. 하지만 매년 수많은 참가자들이 몰리면서, 원하는 세션 예약이 순식간에 마감되곤 합니다. “이미 Full Book인데 어떡하지?” 걱정하지 마세요! 여기 몇 가지 꿀팁을 알려드릴게요. Full Book? No Show를 노려보세요! AWS re:Invent는 정말 넓은 지역에 걸쳐 진행됩니다. 이동 시간이 길어지거나, 예상치 못한 일정 변경으로 인해 세션에 참석하지 못하는 No Show 참가자가 생각보다 많습니다. 👉 TIP: 예약이 다 찬 세션이라도 현장에서 대기하면 들어갈 수 있는 경우가 많습니다. 조금 일찍 가서 줄을 서보세요. 대부분 자리가 생겨, 원하는 세션을 들을 수 있는 기회를 얻을 수 있습니다! (인기 있는 AI 세션은 No Show가 거의 없고 줄도 너무 길어 사전 예약하는걸 추천합니다!) Content Hub: 세션을 놓칠거 같아도 걱정하지 마세요! AWS re:Invent에서는 Content Hub라는 특별한 공간을 제공합니다. 이곳은 현장에서 세션 간 이동이 어려운 참가자들을 위해 마련된 원격 시청 공간으로, 실시간 스트리밍으로 세션을 시청하거나, 다양한 자료를 확인할 수 있는 장소로 다음 동선에서 이동이 어려운 경우 적극 활용하면 좋습니다 AWS Events 앱 활용하기! AWS는 참가자들의 편의를 위해 AWS Events라는 전용 앱을 제공합니다. 이 앱은 사전등록 페이지보다 훨씬 편리하게 세션 관리를 할 수 있도록 도와줍니다. 세션 신청 및 관리: 실시간으로 세션 신청 가능, 겹치는 일정도 쉽게 확인 가능! 알림 기능: 예약한 세션 시작 전에 알림을 받아 놓치는 일을 방지할 수 있어요. 즐겨찾기 기능: 관심 있는 세션을 미리 저장해두고, 나중에 예약 여부를 확인할 수 있습니다. AWS Events 앱을 미리 설치하고 사용하는 것이 re:Invent를 효과적으로 즐길 수 있는 핵심 중 하나입니다! 기념품, 인사이트를 얻을 수 있는 EXPO AWS re:Invent의 하이라이트 중 하나인 Expo는 다양한 AWS 파트너사들과 기술 솔루션을 한자리에서 만날 수 있는 공간입니다. 여기서는 새로운 기술을 체험하고, 네트워킹을
2024년 12월 20일
Refine 이관 작업 ( feat. 중고나라 — 셀러지원센터 )
Refine 이관 작업 ( feat. 셀러지원센터 ) 📍 이관을 하게 된 배경 기존에 셀러지원센터는 Next.js, react-query, shadcn/ui 의 환경에서 개발을 진행했었다. 짧은 시간 안에 빠른 개발, 빌드 시간 단축, 확장 할 수 있는 커스텀 조건이 위 기술의 채택 이유이기도 했다. (참고 아티클) 1차,2차 배포까지만 해도 우리가 선택한 기술이 올바르다고 판단했다. 3차 오픈을 준비하기 전에, 이전에 빠른 개발로 인해 미뤄두었던 코드 개선작업을 시작한 것이 이관의 시초점이다. 1️⃣ 개발자마다 다른 코드 스타일 우리의 컨벤션을 정하긴 했지만, 페이지마다 작성한 개발 코드의 스타일이 온전히 일치하지는 않았다. 그래서 오류가 발생 했을때 해당 코드를 작성하지 않은 사람이 개선을 진행하려고 하면 개발 비용이 많이 필요했다. 2️⃣ 직관적이지 않은 코드 베이스 테이블 자체에 대한 공통 컴포넌트는 직접 구현해 두었고, 외부에서 테이블 리스트 데이터를 불러오고 주입하는 방식으로 사용했었다. 하지만, API마다 데이터를 변환하는 방식, 페이지 수를 가져오는 방식이 달라 페이지 내부에 단순히 리스트 컴포넌트만 보여주는 것이 아닌 데이터를 클라이언트 환경에 맞게 변환하는 로직들이 섞여 있어 페이지 동작을 직관적으로 파악하기 어려웠다. 3️⃣ API마다 다른 스펙에 맞춰 변환하는 과정의 분산화 셀러지원센터에서 호출하는 API 도메인은 아래와 같다. seller-api (셀러) partner-api (관리자) order-api (주문/결제 ) main-api (중고나라 메인) edge-api (이미지) 해당 API를 개발한 팀들이 다르다 보니 데이터를 반환하는 형식이 다르게 설계되어 있었다. 그래서 클라이언트 내부적으로 공통 컴포넌트를 만들어놓았지만, 만약 한 페이지에 여러 도메인의 API를 호출했을 경우에는 이를 클라이언트 형식에 맞게 변환하는 과정이 페이지 내부에 여러 유틸이 들어감으로써 가독성이 떨어지는 현상이 발생하였다. 더불어 변환하는 과정을 모두 모듈로 관리하지 않았기 때문에, 해당 방식 또한 개발자마다 스타일이 달라져 코드의 흐름을 파악하기 어려웠다. 하지만, 어드민 시스템의 특성상 테이블을 사용하는 페이지, 정보를 보여주는 페이지는 일관성을 가지도록 구현되는 것이 필요했고 뷰의 요소만이 아닌 내부 코드 시스템도 동일하게 관리되어야 추가 기능에 대해 여러 페이지를 한 번에 대응하는 것이 가능해진다고 느껴졌다. 이 3가지의 이유로 나는 어떤 특정 규격에 따른 코드 베이스를 가지면서 확장성을 고려한 기술의 도입이 필요하다고 판단했다. 🖤 Refine의 간략한 특징 그렇게 찾게 된 기술이 바로 Refine이다. 공식 문서에는 아래와 같이 설명되어 있다. React 기반 내부 도구, 관리자 패널, 대시보드 및 엔터프라이즈급 비즈니스 애플리케이션을 구축하도록 설계된 오픈소스 프레임워크이다. CRUD, 상태 관리와 같은 일반적인 기능을 처리하기 위한 자동화 세트를 제공하여 개발 프로세스를 간소화한다. 이를 통해 개발자는 반복적인 코딩 작업을 피하고 프로젝트의 더 복잡한 측면에 집중할 수 있다. 기본적으로 해당 기술은 Ant Design, Material UI, Shadcn/ui와 같은 UI 프레임워크와 통합도 가능했다. Next.js, React Router, Remix도 지원하는 강력한 tool로 사용되고 있었다. 그렇다면 어떤 반복적인 코딩 작업을 피할 수 있는 것일까? refine이 제시하고 있는 컨셉은 Headless, Resource, Provider, Hook 총 4가지이며 자세한 내용은 문서를 통해 확인할 수 있기에 핵심 컨셉인 provider와 resource만 살펴보겠다. 1️⃣ Provider의 개념 provider에도 data-provider, authentication-provider, access control-provider, notification-provider등 다양한 provider가 존재한다. 핵심은 data-provider이며 이는 백엔드 데이터와 get, post, pat
2024년 11월 26일
[ 셀러지원센터 ] 신입 개발자의 프로젝트 첫 시작
2023.09–2023.12 , 중고나라 웹 개발팀에서의 4개월 이라는시간 동안 인턴십을 수행하고 나서, 정규직 전환이 이루어져 2024.02.01에 신입 웹 개발자로 입사를 하게 되었다. 2024년도 중고나라의 핵심 프로젝트인 셀러지원센터를 웹 개발팀 리딩의 자리에서 이끌어가면서 느꼈던 부분들을 회고로 기록하고자 이 글을 작성하였다. 셀러 지원센터의 시작 한 달의 공백기 이후 입사하게 되었지만, 인턴의 경험이 있었기 때문에 그간의 중고나라 웹 개발팀의 작업 내용들을 수월하게 따라올 수 있었다. 올해의 중고나라 핵심 프로젝트는 셀러 지원센터와 J쿠폰 이었다. 물론 두 가지 프로젝트에 관심이 있었지만, 웹 개발팀 전체가 비슷한 수요를 가지고 있었기에 우리는 공정하게 사다리 타기를 진행해서 어떤 프로젝트를 맡게 될지 선정하게 되었다. 그렇게 나와 다른 매니저님들과 함께 셀러 지원센터에 합류하게 되었고, 그때부터 해당 서비스에 대한 설계와 프로젝트 방향성을 잡는 과정이 시작되었다. 우리 팀은 해당 서비스를 온전히 이해하기 위해 PM 팀 과의 회의에 선택적으로 참여하곤 했다. 개발자인데 굳이 참여해야 하나..?라는 생각이 들 수도 있겠지만, 서비스를 온전히 이해하고 장기적인 개선 방향을 도출해내는데 기여하기 위해서는 이 과정이 필요하다고 생각했다. 신입 개발자의 프로젝트 리딩 사내에서 진행하는 프로젝트에 참여하는 것도 처음이어서 설레는 마음이면서도 중요한 프로젝트에서 웹 개발팀의 리딩의 자리를 맡게 되어서 책임감을 느끼고 있기도 했다. 우선, 셀러 지원센터가 어떤 특성을 가진 서비스이며, 우리는 어떤 기술을 선정해야 하고, 개발 일정과 방식은 어떻게 진행할 예정인지 수립하는 과정이 필요했다. Tech Stack 논의 셀러 지원센터의 기술적인 특징은 아래와 같다. 유저는 셀러 / 관리자로 나누어지므로 2개의 페이지가 필요 로그인 이후 접속해야 하는 페이지들이기에 SEO 대응이 중요하지 않음 다만, 추후 커뮤니티성이 도입된다면 미리 고려는 해야 하는 부분 빠른 작업 속도를 위해 빌드 시간 단축 필요 디자인 투입 리소스가 없으므로 웹 개발팀 내부적으로 쉽게 만들 수 있는 컴포넌트와 페이지 제작 필요 위 특징들을 토대로 우리는 또 다른 질문들을 아래와 같이 도출했다. 어떤 패키지 매니저를 선택할 것인가..? Headless UI로 shadcn/ui 가 있는데, react-admin 보다 용이할까? SEO가 중요하지는 않지만, Next.js 를 사용해야 할까? 페이지를 나눠야 한다면, repository를 2개를 생성해야 할까? 총 4가지 안건에 대해 레퍼런스를 찾고 회의를 진행했다. ✅ 어떤 패키지 매니저를 선택할 것인가? npm, yarn, yarn-berry, pnpm 중에서 패키지 매니저를 선정하기로 했다. 우선, npm은 기본적인 패키지 관리가 모두 가능하며 package.json안에서 관리할 수 있다는 장점이 있었지만, 일관적이지 않은 패키지 버전을 사용하고 있고, 순차적인 설치로 인한 긴 소요 시간을 가진다는 단점이 있었다. 그래서 yarn을 살펴보았고, 해당 패키지 매니저는 yarn.lock을 사용하기에 사용할 모듈의 버전 지정이 가능하다는 것과 캐시를 사용한 설치 속도 향상이라는 장점을 가지고 있었다. 하지만 여전히 무거운 node_modules를 가진다는 것과 유령 의존성이 발생한다는 단점으로 인해 우리는 yarn-berry로 접근하게 되었다. yarn berry는 yarn v2 이상의 버전인 yarn이며 Plug’n’Play 기능을 지원하여 기존의 node_modules 대신 패키지에 대한 정보를 .zip 파일로 압축해 .yarn/cache 폴더에서 관리한다. 큰 이점이 있었지만, 만약 PnP를 지원하지 않는 패키지가 존재한다면 버전을 올리거나 다른 패키지로 이전하는 방법을 선택해야 한다. 개발 도중 위와 같은 문제가 발생하면 어떻게 해결할 수 있을까..? 에 대해 웹 개발팀 내부적으로 완벽한 답을 도출해내지 못했다. 그래서 우리는 yarn berry의 이점을 가지면서 다른 라이브러리 사용에 덜 영향을 줄 수
2024년 11월 26일
데이터브릭스 도입을 통한 중고나라의 ML 플랫폼 전환기-1편
TL;DR 중고나라는 데이터브릭스로 ML 플랫폼을 전환하여 통합된 데이터 관리, 직관적인 모델링, 효율적인 서빙 인프라로 작업/운영 효율 증가와 비용 절감을 실현했습니다. Intro: In the Middle of Our Journey “국경의 긴 터널을 빠져나오자, 설국이었다.” - 가와바타 야스나리, 『설국』 마치 긴 여정을 거쳐 새로운 경관을 목도한 것처럼, 시스템의 전환이란 대개 인고의 세월을 거쳐 단숨에 큰 변화와 감동을 선사하곤 합니다. 2003년부터 대한민국의 중고 거래 시장을 지탱해 온 중고나라는 최근 이러한 전환의 순간을 맞이했습니다. 20년 이상 서비스를 제공하며 축적한 방대한 양의 C2C 데이터를 바탕으로, 고객이 신뢰할 수 있는 보다 나은 서비스 제공을 위해 데이터브릭스 기반의 ML 플랫폼 전환이라는 여정을 본격적으로 시작한 것입니다. 중고나라 머신러닝팀은 데이터 수집부터 모델링, 서빙까지 전 단계를 아우르는 ML 워크로드를 구축하여 운영 중에 있었고, 이를 데이터브릭스 기반 환경으로 이전하여 기존 대비 실용성과 비용 효율을 높이고자 했습니다. 이번 글에서는 중고나라가 어떤 환경에서 데이터와 머신러닝 모델을 관리해 왔는지, 그리고 왜 데이터브릭스를 선택했는지 ML 엔지니어의 관점에서 소개하려 합니다. As-is: Technical Debt 중고나라의 기존 ML 인프라는 데이터 수집부터 모델링 및 서빙까지 다양한 AWS 서비스들을 활용해 구축되었습니다. 데이터 관리 학습 데이터는 클라우드 서비스에서 제공하는 빅데이터 처리 도구들을 통해 수집했습니다. 비즈니스성 데이터 분석과 리포트는 데이터 웨어하우스와 BI 도구를 사용하였습니다. 이 과정에서 관계형 데이터베이스, 객체 스토리지, 로그 데이터, 검색엔진 등 다양한 데이터 원천에 대한 입수 경로가 필요했으며, EDA와 학습 데이터 구성에도 상당한 자원을 투입했습니다. 모델링 단발성 혹은 PoC 프로젝트는 클라우드 서비스에서 제공하는 머신러닝 개발 환경을 사용했습니다. 주기적인 학습 파이프라인은 워크플로우 관리 도구와 배치 처리 서비스를 조합해 관리했습니다. 학습된 모델의 저장과 관리는 모델 레지스트리를 활용했습니다. 서빙 배치성 추론은 워크플로우 관리 도구와 배치 처리 서비스를 조합해 관리했습니다. 실시간 추론은 Kubernetes 기반 환경에서 AWS Inferentia 인스턴스를 기반으로 운영했습니다. 추론 결과의 일부는 객체 스토리지에 적재, 일부는 검색엔진에 색인하여 대고객 서비스와 모니터링 등에 활용했습니다. 기존 ML 인프라는 다양한 클라우드 서비스들이 독립적으로 상호작용하며 운영되었으나, 복합적이며 고도화된 MLOps 시스템을 구축하는 데는 여러 한계가 있었습니다. 추론 결과의 일부는 객체 스토리지에 적재, 일부는 검색엔진에 색인하여 대고객 서비스와 모니터링 등에 활용했습니다. 기존 ML 인프라는 다양한 클라우드 서비스들이 독립적으로 상호작용하며 운영되었으나, 복합적이며 고도화된 MLOps 시스템을 구축하는 데는 여러 한계가 있었습니다. 관리 포인트 증식: 모델 추론 결과와 데이터 분석을 위한 각종 데이터가 여러 저장소에 분산되어 있어 관리가 복잡했습니다. 가파른 러닝커브: 팀 내의 ML 엔지니어들은 클라우드 인프라에 대한 깊은 이해가 요구되었습니다. 누적된 기술 부채로 인해 보다 고도화된 모델 드리프트 감지 시스템을 구축하는 데도 어려움이 따랐습니다. 모델 레지스트리 활용성 저하: 클라우드 서비스의 추론 엔드포인트를 사용하지 않았기에 모델 레지스트리가 가진 대부분의 편의 기능을 제대로 활용할 수 없었습니다. 높은 비용: 일반 가상머신 인스턴스 대비 고비용인 ML 전용 인스턴스 역시 부담이 되었습니다. To-be: With Databricks 기존의 한계를 극복하고자 중고나라 머신러닝팀은 데이터브릭스의 ML 플랫폼으로의 전환을 결정했습니다. 지난 4개월간의 전환 과정을 통해 기존 시스템의 여러 이슈들을 해결하며, 중고나라의 데이터 파이프라인과 MLOps의 전반적인 프로세스를 혁신할 수 있는 기회를 마련할 수 있었습니다. 통합된 데이터
2024년 11월 1일
[Flutter] Riverpod을 사용하여 간단하고 효과적인 캐싱 시스템 구축하기
Riverpod을 사용하여 간단하고 효과적인 캐싱 시스템 구축하기 Flutter 개발자라면 데이터 관리는 앱의 성능과 사용자 경험에 직접적인 영향을 미친다는 것을 잘 알고 있을 것입니다. 특히, 외부 API(OpenAPI)를 통해 데이터를 가져오는 경우 네트워크 지연이나 서버 상태에 따라 앱의 반응 속도가 달라질 수 있습니다. 이러한 상황에서 백엔드에게 기본적인 캐싱 기능을 부탁할 수 없는 경우, 앱 내부에서 간단한 캐싱 시스템을 구현하여 성능을 향상시키는 것이 중요합니다. 이번 글에서는 Riverpod을 사용하여 로컬 스토리지(Hive 등)를 사용하지 않고도 효과적인 캐싱 시스템을 구현하는 방법을 단계별로 알아보겠습니다. 🍄 캐싱의 필요성 a. API 호출의 문제점 외부 API(OpenAPI)를 사용하여 데이터를 가져오는 경우, 다음과 같은 문제점이 발생할 수 있습니다. 네트워크 지연 : 사용자의 네트워크 상태에 따라 API 호출 시간이 달라집니다. 서버 응답 지연 : 백엔드 서버의 상태나 부하에 따라 응답 시간이 변동될 수 있습니다. 비용 문제 : 빈번한 API 호출은 비용을 증가시킬 수 있습니다. 불필요한 데이터 동기화: 실시간 데이터가 필요하지 않은 경우에도 불필요한 데이터 요청이 발생할 수 있습니다. b. 내부 캐싱의 장점 이러한 문제점을 해결하기 위해 앱 내부에서 간단한 캐싱 시스템을 구현하는 것이 유용합니다. 내부 캐싱의 주요 장점은 다음과 같습니다. 성능 향상 : 캐시된 데이터를 즉시 제공하여 앱의 반응 속도를 높입니다. 네트워크 비용 절감 : 동일한 데이터에 대한 반복적인 API 호출을 줄여 네트워크 비용을 절감합니다. 오프라인 지원 : 네트워크 연결이 불안정하거나 없는 상황에서도 캐시된 데이터를 제공할 수 있습니다. 데이터 최신성 유지 : 캐싱 만료 시간을 설정하여 데이터의 최신성을 유지할 수 있습니다. c. 로컬 스토리지 없이 캐싱하기 로컬 스토리지(Hive, SharedPreferences 등)를 사용하지 않고도 Riverpod만으로 캐싱 시스템을 구현할 수 있습니다. 이는 다음과 같은 장점을 제공합니다. 간결성 : 추가적인 패키지 설치나 설정 없이 Riverpod만으로 캐싱을 관리할 수 있습니다. 유연성 : 메모리 내에서 캐시를 관리하므로, 다양한 형태의 데이터를 손쉽게 캐싱할 수 있습니다. 성능 : 메모리 기반 캐싱은 디스크 기반 캐싱보다 빠르게 데이터를 접근할 수 있습니다. 🧙♂️캐시 매니저 구현 효과적인 캐싱 시스템을 구축하기 위해서는 먼저 캐시 매니저(CacheManager) 를 구현해야 합니다. 이 매니저는 다양한 타입의 데이터를 키를 기반으로 저장하고, 캐시의 만료 시간을 관리합니다. a. CacheItem 클래스 CacheItem 클래스는 캐시된 데이터를 나타내며, 데이터 자체와 함께 데이터를 저장한 시점을 기록합니다. // cache_item.dart class CacheItem<T> { final T data; final DateTime timestamp; CacheItem(this.data, this.timestamp); } b. CacheManager 클래스 CacheManager는 모든 캐시 항목을 관리하는 클래스입니다. 이 클래스는 데이터를 설정하고 가져오는 기능뿐만 아니라, 특정 키의 캐시를 무효화하거나 모든 캐시를 초기화하는 기능도 제공합니다. // cache_manager.dart import 'cache_item.dart'; class CacheManager { final Map<String, CacheItem<dynamic>> _cache = {}; // 데이터 설정 void setData<T>(String key, T data) { _cache[key] = CacheItem<T>(data, DateTime.now()); } // 데이터 가져오기 CacheItem<T>? getData<T>(String key) { final item = _cache[key]; if (item is CacheItem<T>) { return item; } return nul
2024년 10월 28일