펫프렌즈 기술블로그 최신 글
techblog.pet-friends.co.kr에서 발행한 기술 글을 테크곳간이 수집해 최신순으로 보여드립니다.
최신 글
병목은 테스트 실행이 아니었다: Appium E2E 자동화에서 실행과 운영을 분리하기까지
모바일 E2E 테스트 자동화를 떠올리면 처음 목표는 대개 단순합니다. 사람이 직접 확인하던 시나리오를 자동으로 돌리고, 실패하면 리포트를 남긴다. 여기까지가 출발점입니다. 저희도 비슷하게 시작했습니다. PetFriends 모바일 앱의 주요 시나리오를 Appium으로 자동화하고, GitHub Actions에서 iOS와 Android 테스트를 실행하고, Mochawesome 리포트를 남기는 구조를 만들었습니다. 로그인, 결제, 검색, 카테고리, 마이페이지, 상품상세 같은 흐름을 시나리오 테스트로 구성했고, 정기 실행과 수동 실행을 모두 지원하도록 했습니다. 그런데 실제로 운영해보니 병목은 테스트 실행 자체가 아니었습니다. 테스트는 어떻게든 돌릴 수 있습니다. 어려운 건 실패한 뒤였습니다. 실패가 앱 문제인지, 테스트 코드 문제인지, 실행 환경 문제인지 판단하고 기록하는 일이 매번 사람 몫으로 남았습니다. 테스트 자동화가 “실행”을 넘어 “운영”의 문제가 된 순간이었습니다. 앞선 글에서는 Appium으로 웹뷰 테스트를 구현하는 과정과 GitHub Actions, self-hosted runner 기반의 E2E 자동화 구축 과정을 다뤘습니다. 이 글은 그 다음 이야기입니다. 테스트를 어떻게 작성하고 실행 환경을 어떻게 만들었는지보다, 실제 운영 단계에서 왜 GitHub Actions와 n8n의 책임을 나눴는지, 어떤 기준으로 경계를 그었는지, 그리고 이 구조가 이후 AI 기반 분석과 Self-Healing의 기반이 된 이유를 정리합니다. 이전 글에서 출발한 구조 출발점은 이미 구축해둔 Appium E2E 실행 환경이었습니다. Appium, WebdriverIO, TypeScript, Mocha, Mochawesome 조합으로 iOS와 Android 시나리오를 실행하고, GitHub Actions의 self-hosted runner에서 디바이스 자원과 Appium 서버를 관리하는 구조였습니다. 실행 흐름은 아래처럼 단순화할 수 있습니다. GitHub Actions ├─ checkout ├─ pnpm install ├─ runner 환경 초기화 ├─ Appium 서버 상태 확인 ├─ iOS 또는 Android 시나리오 실행 ├─ 인프라 오류 여부 확인 ├─ Mochawesome 리포트 생성 확인 ├─ 녹화 artifact 업로드 ├─ HTML 리포트 배포 └─ webhook으로 결과 전송 여기까지만 보면 자동화가 어느 정도 완성된 것처럼 보입니다. 테스트가 실행되고, 리포트가 남고, 성공과 실패도 확인할 수 있으니까요. 하지만 운영을 시작하면 곧 다른 질문들이 생깁니다. 이 실패는 제품 결함인가, 테스트 코드 결함인가? 같은 실패가 최근에도 있었나? 실패한 시나리오의 리포트와 GitHub Actions run은 어디에 있나? 정기 실행 결과는 Notion에 남겼나? Slack 알림은 누가 보내고, 어떤 포맷으로 보내야 하나? 실패를 AI로 분류하려면 어디에서 호출해야 하나? 나중에 알림 채널이나 리포팅 대상을 바꾸려면 어디를 수정해야 하나? 이 질문들까지 GitHub Actions 안에서 해결하려고 하면 workflow가 금방 복잡해집니다. 실행 스크립트와 운영 규칙이 섞이고, 작은 알림 변경에도 PR이 필요해집니다. 경계만 코드로 보면 더 명확하다 GitHub Actions가 어디까지 맡는지 코드 경계로 보면 더 명확합니다. name: Mobile E2E Scenario Test on: workflow_dispatch: inputs: platform: description: ios | android required: true scenario_group: description: 시나리오 그룹 (비우면 전체 실행) required: false jobs: scenario-test: runs-on: self-hosted # 모바일 디바이스와 Appium 서버는 동시에 여러 세션이 붙으면 불안정해질 수 있다. # 그래서 테스트 세션 단위로 동시 실행을 제한한다. concurrency: group: mobile-e2e-session
2026년 6월 25일
LLM은 모델보다 하네스가 먼저다: 만득이 한 달 수습기
안녕하세요. 펫프렌즈 Post-Order 팀 백앤드 개발자 테디입니다. 저의 첫 기술 블로그 포스팅으로 Post-Order 팀의 10번째 동료인 “만득이” 를 소개시켜 드리고자 합니다! 만득이는 펫프렌즈 내 Post-Order 팀에서 주문/결제/쿠폰 도메인과 사용자 도메인 업무를 돕는 봇입니다. 주문, 결제, 배송, 쿠폰, 포인트, 클레임처럼 주문 이후에 이어지는 흐름과 사용자, 인증, 주소, 약관, 메시지 같은 사용자 도메인 질문을 주로 검토합니다. 필요할 때는 코드, Jira, Notion, Slack 히스토리, Sentry, OpenSearch 에서 근거를 확인하고, 확인한 사실과 추정을 나눠 답하는 역할을 하고 있습니다. 만득이를 만들고, 처음 대화했던 순간 한 달차 봇에게 업무를 맡긴다는 건 살짝 위험한 일입니다. 아직 수습 티가 나고, 가끔은 자신이 아는 것과 확인한 것을 헷갈릴 수 있기 때문입니다. 그래도 한 달 동안 Post-Order 팀원들이 PR 코드 리뷰, 장애 조사, 로그 확인, Jira 와 Notion 문서 확인, Slack 질의응답을 시켜보니 꽤 선명해진 결론이 하나 있습니다. 좋은 업무용 LLM 봇은 모델 하나로 만들어지지 않는다. 모델을 둘러싼 하네스가 봇의 실제 품질을 결정한다. 업무 관련 질문이 하네스와 모델을 거쳐 검증된 답변으로 정리되는 흐름 그림 1. 모델이 바로 답하는 구조가 아니라, 하네스가 앞뒤에서 범위와 근거를 잡아주는 구조입니다. 여기서 하네스는 단순한 실행 스크립트가 아닙니다. 어떤 질문에 답할 수 있는지 정하는 범위, 어떤 근거를 먼저 확인할지 정하는 순서, 실패했을 때 어디까지 공유할지 결정하는 안전 장치, 같은 실수를 반복 하지 않게 만드는 기억 생성과 교정 루프까지 포함합니다. 이번 포스팅에서 다루는 내용 이 포스팅 에서는 “만득이가 어떤 모델 위에서 동작 하는가” 보다 “업무용 봇이 안정적이고 효율적으로 동작 하려면 어떤 하네스가 필요한가” 에 더 초점을 둡니다. Post-Order 업무에서 만득이가 실제로 맡은 일: PR 코드 리뷰, 운영 로그/문서 조사, Slack 질의응답, Notion/Jira 정리 왜 모델 호출 루프보다 컨텍스트와 피드백 루프가 더 중요해지는지 Codex 런타임이 Claude 계열 봇과 구조적으로 어떻게 다른지 한 달차 봇이 아직 조심해야 할 것들 Slack 에서 확인한 실제 사용 사례 실제 사용 사례는 포스팅 아래쪽에 “실제 Post-Order 팀에서 어떻게 쓰였나” 에서 따로 정리했습니다. Post-Order 에서 먼저 만난 문제들 만득이는 처음부터 범용 챗봇으로 쓰인 것이 아닙니다. 주로 주문 이후 흐름과 사용자 도메인 옆에서 반복적으로 호출됐습니다. PR 에서 주문, 쿠폰, 클레임 흐름의 회귀 가능성을 찾기 Sentry 와 OpenSearch 로그를 보고 장애 범위와 원인 후보를 분리하기 Jira, Notion, Slack 히스토리에서 정책의 현황과 출처를 확인하기 긴 설명은 Notion 에 정리하고, Slack 에는 결론과 링크만 남기기 Sync PR 생성이나 PR 코드 리뷰 및 리뷰 확인처럼 반복되는 운영 작업을 안전하게 처리하기 이런 일은 모델이 문장을 잘 쓰는 것만으로는 부족합니다. 질문의 범위, 근거의 출처, 사용했던 도구의 상태, 답변 해도되는 정보와 노출하면 안 되는 정보를 매번 나눠야 하기 때문입니다. 대표 사례를 하나만 먼저 꺼내보면, 주문 프로젝트 레포지토리에 master -> develop/stage sync PR 을 생성하는 요청이 있었습니다. 겉으로는 단순한 PR 생성 이지만 실제로는 기준 브랜치, 대상 브랜치, 기존 PR 중복 여부, 충돌 여부, 라벨 할당 여부, 최종 링크 공유까지 해야 하는 작업이었습니다. 이 흐름에서 중요한 것은 모델이 PR 본문을 잘 작성하는 능력보다, 하네스가 어떤 쓰기를 허용하고 어디서 멈춰야 하는지를 정해주는 구조였습니다. 하네스 엔지니어링이란 무엇인가 하네스 엔지니어링은 AI 에이전트가 복잡한 업무를 안정적으로 수행하도록 모델 밖의 환경을 설계하는 일입니다. 여기에는 어떤
2026년 5월 29일
창식이와 함께하는 물류 개발 라이프(with 하네스)
펫프렌즈 물류 창고 지킴이 창식이 안녕하세요. 펫프렌즈 인터널서비스 물류개발파트 백엔드 개발자 포비입니다. 🙂 저의 두 번째 기술 블로그는 Slack 봇을 실제 운영하면서, 에이전트가 장기·복잡한 작업을 “그럴듯하게”가 아니라 꾸준히 정확하게 수행하게 만드는 설계를 어떻게 적용했는지 정리한 기록입니다. 이번 블로그에서 다루는 내용! 왜 “에이전트 루프”보다 “컨텍스트/피드백 루프”가 더 중요해지는지 지식 베이스, 규칙 인덱스, 교정 로그로 운영 품질을 끌어올리는 방식 MCP 채널을 이용해 Slack ↔ Claude Code를 연결한 아키텍처/운영 방식 저희 인터널서비스 물류개발파트는 주문 이후 물류 전반을 담당하는 여러 레포를 관리하며, 팀에서 반복적으로 하는 작업이 몇 가지 있습니다. Sentry 에러 + Loki 로그 분석 스택트레이스를 확인하고, trace_id로 로그를 추적해 어떤 사용자가 어떤 요청을 했는지 흐름을 재구성하는 작업 레포 영향도 분석 “반품 플로우 수정으로 인한 로직 수정”이 각 레포에 어떤 영향을 미치는지 파악하는 작업 단순 반복처럼 보이지만, 정확한 답을 내려면 도메인 규칙을 알아야 합니다. 박스 분할 로직은 특정 필드 기준으로 계산하고, 재고 데이터는 전용 배치가 관리합니다. 외부 인터페이스 연동은 API 스펙이 달라 각각 다르게 처리해야 합니다. 때마침 OpenAI의 Harness Engineering과 Anthropic의 Effective harnesses for long-running agents, Effective context engineering for AI agents 문서를 읽으며 “하네스 엔지니어링” 개념을 알게 됐습니다. 이를 Slack 봇에 적용해보자는 생각으로 창식이라는 봇을 만들게 되었습니다. 1. 하네스 엔지니어링이란? 정의 하네스 엔지니어링(Harness Engineering)은 AI 에이전트가 복잡한 작업을 안정적으로 수행할 수 있도록 환경, 컨텍스트, 피드백 루프를 설계하는 분야입니다. 에이전트를 단순히 “실행하는 것”을 넘어, 에이전트가 동작하는 조건 자체를 설계하는 것이 핵심입니다. “소프트웨어 엔지니어링팀의 주된 역할이 코드를 직접 작성하는 것에서, 환경을 설계하고 의도를 명세하고 피드백 루프를 구축하는 것으로 바뀐다.” — OpenAI, Harness Engineering: leveraging Codex in an agent-first world (2026) 왜 하네스가 필요한가? 에이전트를 무턱대고 실행하면 두 가지 문제가 생깁니다. ① 컨텍스트 불일치 (Context Drift) 컨텍스트 윈도우가 채워질수록 에이전트는 초기 목표에서 멀어집니다. 장기 작업에서는 자신의 작업을 과도하게 긍정적으로 평가하는 자기 편향이 생깁니다. Anthropic 연구에 따르면, 에이전트는 자신의 작업을 평가할 때 과도하게 긍정적으로 판단하는 경향이 있다. 이를 해결하기 위해 작업 담당 에이전트와 평가 담당 에이전트를 분리하는 것이 효과적이다. — Anthropic, Effective harnesses for long-running agents ② 잘못된 전제에서의 출발 에이전트가 시스템 구조, 도메인 규칙, 코딩 컨벤션을 모르면 아무리 정교한 파이프라인도 결과가 틀립니다. 에이전트에게 얼마나 좋은 컨텍스트를 주느냐가 에이전트 루프 설계보다 더 중요합니다. — Anthropic, Effective context engineering for AI agents OpenAI Codex 하네스 (2026) OpenAI는 Codex 에이전트만으로 프로덕션 시스템을 구축했습니다. 핵심 설계 원칙은 세 가지입니다. AGENTS.md는 ~100줄 인덱스만 제공하고, docs/ 폴더를 단일 진실 공급원으로 운영 CI/linting으로 지식 베이스의 최신성, 교차 링크, 구조를 자동 검증 “doc-gardening” 에이전트가 주기적으로 낡은 문서를 정리 Anthropic — 장기 실행 앱 하네스 (2025) Anthropic 엔지니어링팀은 GAN(Generative Adversar
2026년 5월 27일
AI 워크플로우 자동화 : 펫프렌즈 n8n 해커톤
참여 신청부터 온보딩, 시상까지 모든 진행 과정을 n8n 워크플로우로 진행한 첫 사내 해커톤 “펫프렌즈 n8n 해커톤”을 소개합니다. Hello 안녕하세요! 펫프렌즈 커머스개발팀 백엔드 개발자 메이슨입니다. 👋 하루가 다르게 AI를 활용할 수 있는 도구들이 마구 쏟아지고 효율적인 도구를 찾아 도입하여 개발 생산성을 높이는 시기인것 같습니다. 하지만 새로운 도구를 팀 전체에 도입하는 과정은 여전히 험난하기만 합니다. 이 글은 n8n이라는 AI 통합 워크플로우 자동화 도구를 사용해보고 좋은 경험을 얻어 적극적으로 도입하기 위해 사내 n8n 해커톤 진행 경험에 대해서 공유하려고 합니다. n8n에 대해서 상세하게 설명하는 글은 아닙니다. 비슷한 고민을 하는 많은 분들에게 제 경험이 도움이 되었으면 좋겠습니다. 해커톤 개최 동기 사실 이번 해커톤은 사내에서 진행하는 첫 번째 해커톤이었습니다. 처음 해커톤 개최를 떠올리게 된 계기는 센트리 오류를 자동으로 분석하는 슬랙 봇을 만들면서부터였습니다. 각 서비스에서 센트리 오류가 발생했을 때 담당 개발자가 로그를 분석해서 긴급한 장애인지 판단하는 과정을 AI를 이용해 오류를 분석하고 서비스 로그 확인 없이 장애를 판단할 수 있지 않을까? 하는 아이디어였고 AI 통합 파이프라인을 구축할 때 어떤 오케스트레이터를 사용할지 고민 중에 놀고있는 사내 n8n이 있었습니다. 사내 n8n 서버는 몇 개월전에 구축되었지만, 실제로는 사용되지 않고 있었는데요. 첫 번째로 만들어서 배포하게 된 센트리 오류 분석 봇(aka. 센트리탐지견)입니다. 이모지를 달면 어디선가 나타나는 센트리탐지견 슬랙 센트리 오류 알림에 👀(:eyes:) 이모지가 달리면 Claude AI 모델이 이슈를 분석하고 액션에 따라 github 이슈에 등록해주는 n8n 워크플로우입니다. 단순하지만 개발자의 업무, 특히 주말에 발생하는 장애 대응 시간을 줄여주는 효과를 얻었습니다. n8n을 직접 사용해보기 전까지는 비개발자가 사용하는 노코드 업무 자동화 도구 정도로만 알고있었는데요. 실제로 사용해보니 다른 도구와의 통합 뿐 아니라 AI를 통합한 오케스트레이터의 역할도 충실하게 수행할 수 있는 강력한 도구라는 생각이 들었습니다. 어떻게 하면 개발본부 내에서 n8n을 적극적으로 활용할 수 있을지 고민하다가, 백 번 보는것보다 한 번 경험해보는게 좋을 것이라고 생각하여 직접 n8n 워크플로우를 만들어보는 온보딩을 포함한 해커톤을 준비하게 되었습니다. 이번 해커톤의 목적은 해커톤이 끝난 후에도 참여자들이 스스로 n8n 워크플로우를 만들어 업무를 개선하기를 바랐습니다. 그렇기에 해커톤에서 n8n의 활용 범위를 자연스럽게 알려주기 위해 참여 신청부터 수상까지 “해커톤의 모든 진행 과정을 n8n 워크플로우로 만들어버리자!” 라는 슬로건을 정하고 해커톤을 준비했습니다. 초대장 n8n 해커톤 진행에 대한 설명이 담긴 초대장도 워크플로우로 만들었습니다. 슬랙 슬래시커맨드만 입력하면 n8n 워크플로우가 트리거되서 자동으로 해커톤 채널에 초대하고 참여자 데이터베이스에 추가됩니다. 팀 빌딩 팀 빌딩 역시 n8n 워크플로우가 담당했습니다. 비밀아닌 비밀은 모두 랜덤으로 배치되었다는 점! 온보딩 n8n 해커톤 온보딩의 미션은 “본인만의 Slack App을 만들어 n8n 워크플로우와 연결”하는 것입니다. 슬랙을 사용하는 IT 회사라면 이미 많은 사내 슬랙 앱이 통합되어 있을텐데요. 슬랙 앱은 한번도 안 만든 사람은 있지만 한번만 만든 사람은 없다는 점인데요. (사실 과장입니다) 참여자에게 동기를 부여하고 미션을 달성하는 경험을 주면서 앞으로 유용한 슬랙 앱이 많이 생성되는 바라는 마음에서 미션을 정했습니다. 각 참여자는 슬랙 슬래시커맨드를 입력하면 n8n 워크플로우가 트리거되며 온보딩이 시작됩니다. n8n을 처음 접하는 참여자도 온보딩에 성공할 수 있도록 온보딩 워크플로우를 생성하여 제공합니다. 자동으로 생성된 온보딩 워크플로우 Slack Trigger - Claude Code - Slack Reply 노드로 구성하였으며 처음 n8n을 접하는 참여자도 온보딩
2026년 3월 25일
Next.js에서 QueryClient를 전역변수로 사용해도 될까요?
들어가며 안녕하세요. 펫프렌즈 프리오더팀 프론트엔드 개발자 박재훈입니다. 저희 프론트엔드 챕터에서는 오랜 시간 해결하지 못한 과제가 있었습니다. 바로 Next.js 서버의 메모리 누수 현상인데요. 펫프렌즈 서비스는 상품 상세 페이지를 시작으로 점진적으로 Vue.js에서 Next.js로 전환해왔으며, 현재는 대부분의 화면이 Next.js 기반으로 동작하고 있습니다. 트래픽이 증가하면서 메모리 사용량이 늘어나는 것은 자연스러운 현상입니다. 하지만 저희가 마주한 상황은 달랐습니다. 메모리 사용량이 시간이 지날수록 계속 증가했고, GC가 제대로 동작하지 않았으며, 결국 서버가 OOM(Out of Memory) 직전까지 메모리를 사용하여 서버를 재실행하는 일이 반복되었습니다. 제목을 보시면 어떤 문제였는지 짐작하셨을 겁니다. 먼저 밝히자면 저희는 Next.js 서버 환경에서 export된 전역 변수로 QueryClient를 생성하였습니다. 이는 명백히 잘못된 패턴이었고, 메모리 누수의 직접적인 원인이었습니다. 이 글에서는 저희가 겪은 메모리 누수 현상의 발견부터 원인 분석, 그리고 해결 과정을 공유하고자 합니다. 이 글은 특정 라이브러리나 프레임워크 자체의 결함을 다루지 않습니다. Next.js와 TanStack Query는 모두 훌륭한 도구이며, 문제의 원인은 공식 문서에서 권장하는 사용 패턴을 따르지 않은 저희의 구현 방식에 있습니다. 이 글을 통해 같은 실수를 반복하지 않길 바라며, 올바른 사용 패턴의 중요성을 함께 나누고자 합니다. 메모리 누수 현상 브라우저 환경에서는 탭을 닫으면 메모리가 해제되고, 사용자가 페이지를 새로고침하면 모든 것이 초기화됩니다. 설령 메모리 누수가 있더라도 사용자 개개인의 브라우저에서 일시적으로 발생하는 문제에 그치는 경우가 많습니다.그러나 SSR(Server-Side Rendering) 환경은 완전히 다릅니다. Next.js 서버는 24시간 365일 동작하며, 수많은 사용자의 요청을 처리합니다. 브라우저처럼 ‘닫으면 끝’이 아닙니다. 한 번 발생한 메모리 누수는 서버가 재시작되기 전까지 계속 누적되며, 이는 모든 사용자에게 영향을 미치는 치명적인 문제로 이어집니다. 펫프렌즈 서비스 Next.js 서버 메모리 사용량 위 그래프를 보시면 문제가 명확하게 파악되실겁니다. 메모리 사용량이 계단식으로 올라가다가 서버를 재시작하는 패턴이 반복되고 있는데요, 정상적인 서버라면 메모리 사용량이 일정 수준에서 유지되거나, GC가 동작하면서 톱니바퀴 모양을 그려야 하는데 저희 서버는 그렇지 않았습니다. 증상은 다음과 같았습니다: 1. 지속적인 메모리 증가 서버 시작 후 시간이 지날수록 메모리 사용량이 선형적으로 증가 Garbage Collection이 실행되어도 메모리가 해제되지 않음 2. 서버 성능 저하 메모리 사용률이 높아질수록 응답 시간 증가 CPU 사용률 증가 (GC가 메모리를 회수하려고 과도하게 시도) 간헐적인 서버 응답 지연 발생 3. 주기적인 서버 재시작 필요 메모리 사용률이 임계치(약 60%)에 도달하면 수동으로 서버 재시작 재시작하지 않으면 OOM으로 인한 서버 크래시 위험 모니터링 부담 가중 무엇보다 사용자가 많아질수록 문제가 더 빨리, 더 심각하게 발생한다는 점이 가장 큰 문제였습니다. 저희는 이 문제를 더 이상 미룰 수 없었고, 근본 원인을 찾아 해결하기로 결심했습니다. Node.js 메모리 구조 메모리 누수 현상에는 다양한 원인이 있을 수 있습니다. 디버깅을 하기 위해서는 어디서부터 시작해야 할까요? 먼저 Node.js의 메모리 구조를 간단히 살펴볼 필요가 있습니다. Node.js 프로세스의 메모리는 크게 다음과 같이 구성됩니다: 대부분의 JavaScript 객체와 데이터는 Heap Memory에 저장됩니다. 왜 Heap Memory부터 확인하는가? 세 가지 이유가 있습니다: 애플리케이션 코드가 직접 관리하는 영역 Stack Memory는 함수가 종료되면 자동으로 해제되고, Native Memory는 Node.js 내부나 네이티브 모듈이 관리합니다. 하지만 Heap은 다릅니다. 저희
2026년 1월 27일
MongoDB 커넥션 풀 모니터링과 알림 시스템 구축기
안녕하세요. 펫프렌즈 Pre-Order 팀 백엔드 개발자 제니입니다. 트래픽이 급증하는 순간, “혹시 우리 데이터베이스 커넥션이 부족하진 않을까?” 걱정해보신 적 있으신가요? 저희 팀은 이런 불안을 해소하기 위해 DocumentDB 커넥션 풀 모니터링 시스템을 구축했습니다. 문제가 발생하기 전에 미리 알림을 받아 대응할 수 있게 된 과정을 공유합니다. 왜 만들게 되었나? DocumentDB, 전시 영역의 핵심 데이터베이스가 되다 pre-order 팀은 상품 전시 영역을 담당하고 있습니다. 처음엔 RDS를 주로 사용했지만, CQRS 패턴을 도입 후 전시 데이터의 유연한 스키마 처리가 필요해짐에 따라, DocumentDB 사용이 확대되었습니다 (CQRS 파이프라인 도입기가 궁금하신 분들은 “MSA를 가기 위한 첫걸음 CQRS 파이프라인 도입기” 를 읽어보세요!) 상품 메타데이터, 카테고리 정보, 전시 레이아웃 설정 등 다양한 데이터가 DocumentDB로 옮겨갔고, 어느새 전시 영역에서 가장 중요한 데이터베이스가 되어 있었죠. 자연스럽게 DocumentDB의 안정성과 성능이 서비스 전체의 안정성에 직결되는 상황이 되었습니다. 성능 개선의 시작: 슬로우 쿼리 모니터링 DocumentDB의 중요성이 높아지면서, 단순히 잘 작동하는 것을 넘어 최적화가 필요했습니다. 가장 먼저 시작한 건 슬로우 쿼리 모니터링이었습니다. 이와 관련된 내용은 **DocumentDB 성능 최적화: 슬로우쿼리 수집부터 AI를 활용한 인덱스 추천까지 블로그를 참고해주세요!** 슬로우 쿼리로 집계된 개수가 30% 줄어드는 걸 보면서 모니터링의 중요성을 체감했죠. 다음 단계: 커넥션 풀 모니터링 슬로우 쿼리를 잡고 나니, 다음 병목이 보이기 시작했습니다. 바로 커넥션 풀이었습니다. 이벤트 발송으로 유저 인입이 급증할 때마다 “혹시 커넥션이 부족하진 않을까?” 걱정했지만, 실제로 현재 상태를 확인할 방법이 없었습니다. 슬로우 쿼리는 발견하고 고칠 수 있었지만, 커넥션 풀은 블랙박스나 다름없었거든요. 미리 풀 사이즈를 늘려두는 것도 한계가 있었습니다. 리소스 낭비일 수도 있고, 정확히 얼마나 필요한지 알 수 없으니까요. “문제가 터지기 전에 미리 알 수는 없을까?” 슬로우 쿼리 모니터링처럼, 커넥션 풀도 모니터링하고 싶었습니다. 80% 도달 시점에 알림을 받는다면, 장애 전에 스케일 아웃이나 설정 조정으로 대응할 수 있을 것 같았습니다. 쿼리 성능 최적화에 이어, 이제는 리소스 사용량 최적화로 한 걸음 더 나아가는 시점이었습니다. MongoDB 커넥션 풀, 왜 모니터링하기 어려울까? MongoDB 드라이버는 내부적으로 커넥션 풀을 관리합니다. 요청이 들어오면 풀에서 커넥션을 빌려주고(Check-out), 작업이 끝나면 다시 반환받습니다(Check-in). 문제는 이 풀의 상태를 외부에서 볼 방법이 없다는 점입니다. JMX로 메트릭을 볼 수 있지만 주기적 폴링이라 실시간 스파이크를 놓치기 쉽습니다. 게다가 임계치 초과를 자동으로 알려주지도 않죠. 그래서 ConnectionPoolListener를 직접 구현하기로 했습니다. 어떻게 구현했나? 1. ConnectionPoolListener 구현 MongoDB Java Driver가 제공하는 인터페이스를 구현하면 커넥션 풀의 모든 이벤트를 실시간으로 받을 수 있습니다. @Slf4j @Component @RequiredArgsConstructor public class MongoConnectionPoolListener implements ConnectionPoolListener { private static final double WARNING_THRESHOLD = 0.8; private final AtomicInteger checkedOutCount = new AtomicInteger(0); private int maxPoolSize; @Override public void connectionCheckedOut(ConnectionCheckedOutEvent event) { int current = checkedOu
2025년 12월 9일
AOP 기반 Feature Toggle 라이브러리
안녕하세요 펫프렌즈 백엔드 개발자 팀버(Timbre/박기오) 입니다 :) 펫프렌즈 백엔드 서비스에서는 A/B 테스트, 배포 후 버전 전환(v1 → v2) 등 다양한 상황에서 서비스의 기능을 유연하게 제어하기 위해 Feature Toggle 도입을 검토했고, 펫프렌즈 서비스에 맞는 방식으로 Feature Toggle 라이브러리를 상용 서비스 적용을 목표로 하고 있습니다. 이 글에서는 feature toggle의 자체 개념이나 라이브러리의 구조보다는, AOP 기반의 Feature Toggle 구현을 중심으로 정리했습니다. 0. 멀티모듈 기반 라이브러리 구조 Feature Toggle 라이브러리는 Spring 기반으로 만들어졌고, 서비스 아키텍처에 따라 두 가지 모듈로 나누어 제공됩니다. (internal 모듈은 mvc, webflux 모듈에서만 사용하는 내부 전용 모듈로, 외부 서비스에서는 직접 의존할 수 없도록 설계 했습니다.) mvc : 기존 Spring MVC 기반 서비스용 webflux : Reactive 환경(Spring WebFlux)기반 서비스용 Internal : 공통 인터페이스, 저장소 통신용 이를 위해 프로젝트를 멀티모듈로 구성했습니다. feature-toggle/ ├── internal ├── mvc └── webflux internal 모듈에는 공통적으로 사용될 내부 저장소와 연결하는 인터페이스를 정의했습니다. mvc와 webflux 모듈은 internal를 의존하며 각 환경에 맞는 구현체를 제공합니다. 서비스에서는 필요한 모듈만 선택적으로 의존시켜 사용 가능합니다. 이렇게 구성함으로써, 서비스 입장에서는 불필요한 의존성을 최소화하면서 원하는 환경에 맞는 Feature Toggle 기능을 바로 사용할 수 있습니다. 1. 전체 흐름 2–1. ToggleRouter Annotation — 기능 토글의 시작점 @ToggleRouter는 메서드 단위로 Feature Toggle을 적용하기 위한 커스텀 어노테이션입니다. 이 어노테이션은 AOP와 결합하여, 특정 기능을 활성화하거나 비활성화하고, 필요 시 다른 메서드로 라우팅하는 기능을 제공합니다. @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ToggleRouter { String value(); String routeKey(); boolean existFallback() default true; boolean forceFetch() default false; } 주요 필드 설명 value 토글 플래그의 고유 키를 지정합니다. 이 키로 FeatureToggleConfiguration에서 상태를 조회합니다. routeKey 특정 기능이 비활성화될 경우, 대체 메서드를 연결하기 위한 식별자입니다. AlternativeRouter와 매칭되어 기능 라우팅이 가능하게 합니다. existFallback 최초 배포 시, 토글이 없어도 기본 기능을 사용할지 여부를 결정합니다. 초기 배포에는 false 로 설정하면 안전하게 기본 기능을 제공합니다. forceFetch 토글 상태를 매번 저장소에서 직접 조회할지, 아니면 싱글톤 객체의 캐시를 사용할지를 결정합니다. true → 저장소 직접 조회 false → 메모리 캐시 사용 (기본값) 2–2. ToggleAlternative Annotation @ToggleAlternative는 AOP 기반 라우팅에서, @ToggleRouter로 지정된 routeKey와 매칭될 경우 실행될 대체 메서드를 지정하는 어노테이션입니다. @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ToggleAlternative { String routeKey(); } 주요 필드 설명 routeKey @ToggleRouter의 routeKey와 동일하게 설정해야 합니다. 예: @ToggleRouter(routeKey = "newFlow") → 대체 구현 메서드에는
2025년 12월 3일
App Store 5.1.1 가이드라인 리젝 대응기: SwiftUI Repository 패턴으로 게스트 기능 구현하기
소개 안녕하세요! 오랜만에 포스팅하러 또 찾아온, iOS 앱 개발자 넬로입니다. 저희 회사에는 사내 카페라는 좋은 복지가 있습니다. 그리고 저는 이 카페를 이용할 수 있는 임직원용(?)앱을 관리하고 있습니다. 배경 어느날과 다름없이 이 앱의 개발을 마치고 심사를 넣었습니다. 그런데 다음 날 아침, App Store Connect에 빨간 불이 들어와 있었습니다. iOS 개발자들의 심장을 자극하는 짤.Jpg 😮💨 요약하자면 로그인 안 해도 앱 좀 둘러보게 해줘라!는 것이었습니다. 사내 임직원 전용 앱이라 로그인이 필수라고 생각했지만, Apple의 가이드라인은 냉정했습니다. 빨리 돌아가서 고쳐와 그러나, 단순히 if isGuest { return } 처리만 하기에는 기존 코드가 너무 복잡하게 얽혀있었습니다. 특히 최근에 SwiftUI로 막연히 전환하면서 화면간 연결이 강하게 되어있는 상태였습니다. 결국 기존 로직을 망가뜨리지 않으면서, 가짜 데이터로 앱을 구동시켜야 한다는 미션이 주어졌습니다. 이 글은 Repository 패턴과 DI Container를 도입해 게스트모드를 구현하여 이 미션을 극복한 후기입니다. 문제 게스트 모드를 막상 넣으려니 세 가지 큰 벽에 부딪혔습니다. 스파게티 코드 위험: 기존 ViewModel들이 API 서비스를 직접 호출하고 있어, 여기에 if/else를 넣다간 지옥문이 열릴 것 같았습니다. 권한 에러: 게스트는 실제 API를 호출하면 401 Unauthorized를 반환합니다. 따라서, 자연스러운 Mock 데이터가 필요했습니다. 데이터 증발: 게스트 모드에서 장바구니에 담은 커피들은 로그인을 하면 날아가버리니 데이터 전환 전략이 필요했습니다. 해결 어떻게 해야할지 막막했지만, 급할수록 돌아가라는 말처럼 차근차근 아키텍처를 뜯어고치기로 했습니다. 1. Repository 패턴으로 ‘데이터 책임’ 떠넘기기 가장 먼저 ViewModel이 NetworkService를 직접 알지 못하게 막았습니다. Repository를 중간 관리자로 두기로 했습니다. 이게 Swift식 POP다! M̶y̶ ̶l̶i̶t̶t̶l̶e̶ ̶s̶o̶d̶a̶P̶O̶P̶!̶ 이제 ViewModel은 데이터가 서버에서 오는지, 제가 만든 가짜 파일(Mock)에서 오는지 알 필요가 없습니다. 그저메뉴 줘라고 요청하면 끝입니다. 이렇게 분리해두니 디버깅할 때도 훨씬 편했습니다. protocol MenuRepository { func fetchMenuList() async throws -> [MenuModel] } final class DefaultMenuRepository: MenuRepository { private let dataSource: NetworkDataSource // 핵심! 프로토콜에 의존 init(dataSource: NetworkDataSource) { self.dataSource = dataSource } } 2. DI Container로 ‘태세 전환’ 자동화 게스트 모드 시작!버튼을 누르면 앱 전체가 가짜 데이터를 바라봐야 합니다. 이를 일일이 수정하는 건 비효율적이라, DI(Dependency Injection) Container를 직접 구현했습니다. 간단하게 구성한 DI Container 로직 코드로 보면 이런 식입니다. isGuestMode 플래그 하나로 앱의 모든 데이터 소스를 교체합니다. final class DIContainer { static let shared = DIContainer() // 혼자왔니? 어~ 싱글톤(?)이야..이 아니라 편의상 사용 private var isGuestMode: Bool = false private var networkDataSource: NetworkDataSource { if let dataSource = _networkDataSource { return dataSource } let dataSource: NetworkDataSource = isGuestMode ? MockNetworkDataSource() // 게스트면 가짜 : NetworkService
2025년 12월 3일
Kafka에서 S3로 실시간 데이터 수집 파이프라인 설계와 구축기
안녕하세요, 펫프렌즈 데이터플랫폼 파트의 데이터 엔지니어 루카스입니다. 펫프렌즈의 데이터 수집과 가공은 대부분 배치 방식으로 운영되어 왔습니다. 하지만 최근 데이터의 활용 맥락이 다양해지면서 실시간 이벤트 데이터 수집 필요성이 나타났습니다. 이번 글에서는 실시간 수집 파이프라인을 도입하게된 배경과 설계 과정에서 고민했던 주요 의사결정에 대해 공유드리려 합니다. 이번 글의 구성 실시간 수집이 필요해진 이유 Connector vs Consumer 핵심 설계 실패처리 운영 모니터링 결과 앞으로의 계획 실시간 수집이 필요해진 이유 첫 번째 이유는 MariaDB Trigger 기반으로 운영되던 로그 테이블의 한계였습니다. 기존에는 상품 변경 이력을 TRIGGER 로 감지해 LOG 테이블에 적재했는데, 컬럼이 추가될 때마다 Trigger 문과 로그 테이블 스키마를 함께 수정해야 했습니다. 운영 중인 DB에서 스키마 변경이 자주 일어나다 보니 관리 부담이 커졌고, 결국 기존 로그 테이블을 유지하지 못한 채 아예 새 버전을 만들어야 하는 상황까지 이어졌습니다. 이 과정에서 “이런 로그성 데이터를 RDB에 계속 쌓는 게 맞을까?” 하는 생각이 들었습니다. 데이터량이 늘수록 스토리지와 인덱스 관리 비용이 커지고, 확장성에도 한계가 있었기 때문입니다. 그래서 Trigger 대신 CDC(Change Data Capture) 기반으로 로그 이벤트를 Kafka 토픽으로 발행하고, 이를 별도의 컨슈머가 읽어 S3에 적재하는 방향으로 전환했습니다. 펫프렌즈에는 기존에 MSK + Debezium으로 CDC 토픽을 이미 보내고 있어 이를 활용해 컨슈머를 개발하기로 했습니다. 두번째 이유는 광고 및 사용자 행동 이벤트 데이터를 장기 보관하고 분석하기 위해서입니다. 광고 노출, 클릭이나 주문 완료 같은 데이터를 수집해 광고 성과 분석이나 리포트에 활용하고자 하는 요구가 있었습니다. 이벤트 실시간으로 발생하기 때문에 Kafka 토픽을 직접 소비해 저장하는 파이프라인이 필요했습니다. 이런 요구를 충족하기 위해 데이터팀에서 레이크하우스로 운영 중인 S3 + Athena 조합을 선택했습니다. S3는 Schema-on-Read 구조를 제공하기 때문에 데이터를 원형 그대로 저장한 뒤, 필요할 때 스키마를 정의해 읽을 수 있습니다. 별도의 테이블 스키마 변경이나 컬럼 추가 작업이 필요 없어 운영 부담이 적고 확장성도 높습니다. Connector vs Consumer Kafka의 토픽 데이터를 S3에 저장할때 일반적으로 S3 Sink Connector를 사용하는 방법이 있습니다. Connector를 사용하면 큰 운영 부담없이 설정만으로 토픽의 데이터를 S3에 적재할 수 있는 장점이 있습니다. 하지만 저희는 컨슈머 애플리케이션을 직접 개발했는데 이유는 다음과 같습니다. 1) Parquet 포맷 저장을 위한 제약 Athena에서 효율적으로 쿼리하기 위해서는 열 지향 포맷(Parquet) 사용이 사실상 필수입니다. 하지만 현재 구조에서 사용하는 Debezium + S3 Sink Connector 조합은 기본적으로 JSON 포맷만 지원하고 있었습니다. Parquet으로 변환하려면 Schema Registry 등 추가 구성이 필요했지만, 이는 여러 팀의 CDC 파이프라인에 영향을 주기 때문에 도입이 어렵다고 판단했습니다. 다음 대안으로 Connector로 JSON을 저장한 뒤 Parquet으로 변환하는 별도 파이프라인을 두는 방법도 검토했습니다. 하지만 이 경우 처리 지연(latency)이 한 번 더 생기고 운영 복잡도도 올라갑니다. 또한 Connector의 파일 flush 로직이 “Rotation Flush” 방식이라 새로운 이벤트가 일정 시간 동안 없으면 파일이 닫히지 않아 S3 업로드가 지연되는 문제도 있었습니다. 2) 세부 제어와 비즈니스 로직 반영 필요 Trigger 방식에서는 특정 컬럼이 업데이트 되는 경우 제외되는 등 세부적인 조건 로직이 존재했습니다. 이런 로직을 CDC 기반으로 전환하면서도 동일하게 반영하려면 Debezium의 SMT(Single Messa
2025년 11월 7일
국밥 같은 단위테스트 시나리오로 마음 든든하게 개발하기
안녕하세요. 펫프렌즈 백엔드 개발자 에이미입니다. 펫프렌즈 개발팀은 테스트 코드 작성을 적극 권장하고 있으며, 테스트 커버리지를 지속적으로 관리하고 있습니다. 저 역시 업무에 단위 테스트를 적극적으로 도입하면서 코드 품질은 물론, 유지보수성과 협업 효율까지 여러 면에서 긍정적인 변화를 경험할 수 있었습니다. 단순히 테스트 커버리지를 높이는 데 그치지 않고, 코드에 대한 신뢰도를 높이는 데에도 큰 도움이 되었는데요. 이 글에서는 제가 실제로 경험한 사례들을 중심으로, 단위 테스트를 통해 얻은 장점들과 테스트를 보다 효율적으로 작성하는 팁들을 공유해보고자 합니다. 사전에 알아두면 좋은 개념들 단위 테스트(Unit Test) 테스트 스텁(Test Stub) JUnit Mockito 단위 테스트에 대한 기본 개념은 생략하고 실무 적용 사례 위주로 공유하고자 합니다. 대니스가 먼저 기고해주신 “테스트 코드를 효과적으로 작성하기 위한 여정”도 함께 봐주시면 감사하겠습니다~ 단위 테스트, 이렇게 작성했습니다 실제 업무에서 작성한 테스트 코드 예시를 통해, 어떻게 테스트 커버리지를 높이고 신뢰도를 확보했는지 공유합니다. 업무 코드의 테스트 실행 결과 중 일부 코드 공통화로 다양한 케이스를 효율적으로 작성하고자 했습니다 조회시간 별 결과가 달라지는 업무 로직의 테스트 결과 중 일부 안정적으로 잘 개발하고 싶은 마음에 테스트 케이스를 상당히 많이 만들었습니다 🥹 코드 2줄 수정에 테스트코드 183줄 추가한 광인이 여기있습니다 ㅎ_ㅎ 단위 테스트를 도입하며 좋았던 경험들 1. 사이드이펙트 없이 핵심 비즈니스로직 구현 및 배포 AI를 이용해 복잡한 비즈니스 요구사항을 구현하다보니 초기에 코드의 신뢰도에 대한 우려가 있었습니다. 이에 따라 테스트 시나리오를 57건 작성하였습니다. 다소 극단적인 수준이었지만, 그만큼 안심할 수 있었습니다. 테스트 시나리오를 많이 작성했던 로직에서는 QA 이슈는 한 건도 발생하지 않았으며, 테스트 코드가 부족했던 부분에서만 이슈가 발생하였습니다. QA에서 발견된 문제는 테스트 코드로 재현하고 수정하면서 전체적인 코드 신뢰도를 높일 수 있었습니다. 특히 QA 중에 유효성 검사 관련 이슈가 지속적으로 발생하고 정책도 자주 변경되었는데, 이미 충분히 작성된 테스트 코드 덕분에 사이드 이펙트를 방지할 수 있었습니다. 2. 안전한 레거시 개편 개편할 API는 1개였지만, api 내부의 유효성검사 케이스가 매우 다양해 12가지의 케이스의 테스트코드를 작성하며 로직을 파악했습니다. 실패 케이스 9건, 성공 케이스 2건을 먼저 작성하였으나 QA 중 성공 케이스에서 버그가 1건 발견됐습니다. 즉, 테스트코드가 다소 부족했던 부분에서 버그가 발견됐습니다. QA 기간 동안 발견된 버그는 단 2건이었습니다. 테스트 코드 작성을 더 효율적으로 만드는 도구들 JUnit, Mockito, Fixture Monkey는 테스트 코드의 생산성과 유지보수성을 크게 높여주는 강력한 도구들입니다. 실제로 사용하면서 유용했던 팁들을 정리해 보았습니다. JUnit https://junit.org/ RepeatedTest @RepeatedTest(5) void testWithRepeatedTest(RepetitionInfo repetitionInfo) { int random = new Random().nextInt(10); assertTrue(random >= 0); // 예시: 조건 충족 여부 확인 } 테스트를 지정한 횟수만큼 반복 실행합니다. 랜덤값에 따라 간헐적으로 실패하는 테스트 케이스를 잡을 때 유용합니다. ParameterizedTest 다양한 파라미터 기반 테스트를 작성할 수 있습니다. 주요 어노테이션: @ValueSource: 기본 타입 배열 (int, String, boolean 등)을 넣어 테스트할 수 있습니다. @ParameterizedTest @ValueSource(strings = {"apple", "banana", "cherry"}) void testWithValueSource(String fruit) { assertNot
2025년 9월 25일