<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[구름고래 공방]]></title><description><![CDATA[구름고래 공방]]></description><link>https://blog.aqudi.me</link><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 14:52:03 GMT</lastBuildDate><atom:link href="https://blog.aqudi.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Gemini 3 Seoul Hackathon 후기]]></title><description><![CDATA[🔗 https://cerebralvalley.ai/e/gemini-3-seoul-hackathon?tab=guest-list

TL;DR

만든 것: Gemini 3를 활용한 서울 랜드마크 방탈출 게임 "Seoul Adventure"

핵심 기술: Gemini 3 Flash Preview로 시나리오 자동 생성 + 이미지 인증

가장 큰 교훈: 해커톤에서는]]></description><link>https://blog.aqudi.me/2026-gemini-3-seoul-hackathon</link><guid isPermaLink="true">https://blog.aqudi.me/2026-gemini-3-seoul-hackathon</guid><category><![CDATA[gemini3]]></category><category><![CDATA[hackathon]]></category><category><![CDATA[2026]]></category><category><![CDATA[Seoul]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sat, 28 Feb 2026 14:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/65556e531147f173adab7767/e304facb-96a4-472a-ac4a-6a0892ebe0c6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/65556e531147f173adab7767/ac59540d-2a7d-4838-9788-64b6a8613af5.jpg" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>🔗 <a href="https://cerebralvalley.ai/e/gemini-3-seoul-hackathon?tab=guest-list">https://cerebralvalley.ai/e/gemini-3-seoul-hackathon?tab=guest-list</a></p>
</blockquote>
<h2>TL;DR</h2>
<ul>
<li><p><strong>만든 것</strong>: Gemini 3를 활용한 서울 랜드마크 방탈출 게임 "Seoul Adventure"</p>
</li>
<li><p><strong>핵심 기술</strong>: Gemini 3 Flash Preview로 시나리오 자동 생성 + 이미지 인증</p>
</li>
<li><p><strong>가장 큰 교훈</strong>: 해커톤에서는 익숙한 스택 + 데모 중심 개발이 정답</p>
</li>
</ul>
<hr />
<h2>프로젝트: Seoul Adventure</h2>
<h3>아이디어</h3>
<p>서울 랜드마크에서 방탈출을 해본다면 어떨까? 라는 생각을 시작으로 서울에 있는 여러 랜드마크들에 방문해서 미션을 수행하는 서비스를 만들어봤다. 약간의 스토리텔링과 인증, 퀴즈 형식을 가미해서 군데 군데를 돌아보고 추억을 남기는 서비스다.</p>
<p>인증 방식은 사진, 퀴즈, GPS 위치 정보 세가지로 구성했다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/65556e531147f173adab7767/e021c07b-5b3e-40dc-bd25-1d6f29c5b04e.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>🔗 <a href="https://github.com/Aqudi/seoul-adventure">https://github.com/Aqudi/seoul-adventure</a></p>
</blockquote>
<h3>기술 스택</h3>
<table>
<thead>
<tr>
<th>영역</th>
<th>기술</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Frontend</strong></td>
<td>Next.js 16, React 19, Tailwind CSS 4, Zustand, Google Maps</td>
</tr>
<tr>
<td><strong>Backend</strong></td>
<td>Fastify 5, MikroORM, PostgreSQL</td>
</tr>
<tr>
<td><strong>AI</strong></td>
<td>Google Gemini 3 Flash Preview</td>
</tr>
<tr>
<td><strong>Infra</strong></td>
<td>Turborepo, pnpm, Docker, GCP Compute Engine, Cloudflare Tunnel</td>
</tr>
</tbody></table>
<h3>Gemini 3 활용</h3>
<p>Gemini 3를 세 가지 핵심 기능에 활용했다:</p>
<ol>
<li><p><strong>시나리오 자동 생성</strong>: 랜드마크 이름만 입력하면 역사적 스토리라인, 퀴즈, GPS 좌표가 포함된 탈출 시나리오가 자동 생성된다.</p>
</li>
<li><p><strong>이미지 인증</strong>: 사진 퀘스트 완료 시 AI가 사진을 분석하여 정확한 장소 방문 여부를 검증한다.</p>
</li>
<li><p><strong>퀘스트 구성</strong>: GPS 좌표 기반 위치 인증, 역사 퀴즈, 사진 인증 등 다양한 미션 타입을 지원한다.</p>
</li>
</ol>
<h3>팀 구성</h3>
<p>사이프에서 만난 형, 누나와 3인 팀(백엔드 2, 프론트 1)으로 참가했다.</p>
<hr />
<h2>개발 타임라인</h2>
<table>
<thead>
<tr>
<th>시간</th>
<th>활동</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>09:00</td>
<td>도착, 아침 식사</td>
<td>세빛둥둥섬 첫 방문</td>
</tr>
<tr>
<td>09:30-11:30</td>
<td>아이디어 회의</td>
<td>⚠️ 2시간 소요 (예상보다 오래 걸림)</td>
</tr>
<tr>
<td>11:30-12:00</td>
<td>기술 스택 세팅</td>
<td>Turborepo, MikroORM 설정에서 헤맴</td>
</tr>
<tr>
<td>12:00-13:00</td>
<td>점심 식사</td>
<td></td>
</tr>
<tr>
<td>13:00-18:00</td>
<td>핵심 기능 개발</td>
<td>5시간 집중 개발</td>
</tr>
<tr>
<td>18:00-19:00</td>
<td>저녁 식사</td>
<td></td>
</tr>
<tr>
<td>19:00-19:30</td>
<td>시연 준비</td>
<td>⚠️ Gemini API <code>too many requests</code> 에러 발생</td>
</tr>
</tbody></table>
<hr />
<h2>삽질 기록</h2>
<h3>1. Turborepo + MikroORM 설정 삽질 (30분+)</h3>
<p>안 써봤던 Turborepo(monorepo)와 MikroORM을 선택했는데, 초반에 설정 때문에 30분 이상을 소요했다. 알고 보니 root에 있는 <code>tsconfig</code> 문제였는데, MikroORM 설정 문제인 줄 알고 한참 헤맸다.</p>
<p><strong>교훈</strong>: 해커톤에서는 익숙한 스택을 쓰는 게 정답이다. 새로운 기술을 배우고 싶으면 해커톤 전에 미리 연습하자.</p>
<h3>2. Gemini API Rate Limit</h3>
<p>시연 직전에 <code>too many requests</code> 에러가 발생해서 라이브 데모가 순탄치 않았다.</p>
<p><strong>원인</strong>: 크레딧을 충전했는데도 무료 플랜의 rate limit이 적용되고 있었다. 플랜을 올려봐도 해결이 안 돼서 결국 데모 중에 에러가 났다.</p>
<p><strong>교훈</strong>: API 사용시에는 결제 플랜 꼭 확인하고 사용하자.</p>
<h3>3. 불필요한 기능에 시간 낭비</h3>
<p>회원 인증과 실제 배포에 시간을 썼는데, 데모 영상이나 라이브 데모만 보여주면 되는 상황이었다.</p>
<p><strong>교훈</strong>: 해커톤에서는 "동작하는 데모"가 "배포된 반쪽짜리"보다 낫다.</p>
<hr />
<h2>회고</h2>
<h3>좋았던 점</h3>
<ol>
<li><p><strong>3년 만의 해커톤</strong>: 7~8시간 몰입해서 개발하는 경험 자체가 값졌다. 평소에는 업무와 일상에 쫓기다 보면 이렇게 한 가지 일에 집중하기 어려운데, 해커톤이라는 환경이 강제로 몰입 모드를 만들어줬다.</p>
</li>
<li><p><strong>좋은 팀원들과 함께</strong>: 서로 역할 분담도 잘 되고, 막히는 부분 있으면 바로바로 논의할 수 있어서 혼자 할 때보다 훨씬 재밌었다.</p>
</li>
<li><p><strong>재밌는 아이디어 발굴</strong>: "서울 랜드마크에서 방탈출"이라는 컨셉이 생각보다 확장성이 높았다. 따로 사이드로 개발하고 싶다.</p>
</li>
<li><p><a href="http://Pencil.dev"><strong>Pencil.dev</strong></a> <strong>사용</strong>: 해커톤 때 10분 활용해봤는데 생각보다 훨씬 결과물이 좋게 나왔다. 이제 디자이너 없이도 사이드프로젝트 디자인 정도는 뚝딱 할 수 있을 것 같다. 다만 최초 생성 이후에는 성능이 급격하게 떨어지는 느낌이었다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/65556e531147f173adab7767/b99f8ca9-a6cb-4d3c-86d3-684687497b02.png" alt="Pencil.dev로 만든 초안" style="display:block;margin:0 auto" />
</li>
<li><p><strong>세빛둥둥섬에서의 코딩</strong>: 한강 위에 떠 있는 건물에서 코딩하는 경험은 처음이었다. 창밖으로 보이는 한강뷰가 생각보다 집중력에 도움이 됐다. 약간의 힐링되는 느낌도 있었다.</p>
</li>
</ol>
<h3>아쉬웠던 점</h3>
<ol>
<li><p><strong>아이디어 회의에 2시간</strong>: 오전 9시에 가서 아침밥 먹고 아이디어 회의를 시작했는데, 2시간이나 걸렸다. 최소한 방향성은 미리 정하고 왔어야 했다.</p>
</li>
<li><p><strong>행사 사전 조사 부족</strong>: 공지 페이지를 꼼꼼히 안 봐서 구글 크레딧을 못 받았다.</p>
</li>
<li><p><strong>새로운 기술 스택</strong>: Turborepo, MikroORM 등 제한된 시간 내에 개발하기도 벅찬데 새로운 기술까지 학습하려니 시간이 배로 들었다.</p>
</li>
<li><p><strong>비효율적인 작업 프로세스</strong>: 프론트/백엔드 나눠서 작업했는데, 요즘 AI 코딩 도구가 워낙 잘되어 있으니 기능 단위로 동시에 하는 게 나았을 것 같다.</p>
</li>
<li><p><strong>네트워킹 부족</strong>: 정신없이 개발하느라 다른 팀들과 교류를 못 했다. 해커톤을 네트워킹의 장으로 더 활용할 수 있었을 텐데.</p>
</li>
</ol>
<hr />
<h2>다음 해커톤을 위한 액션 아이템</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>구체적인 실천 방안</th>
</tr>
</thead>
<tbody><tr>
<td><strong>템플릿 준비</strong></td>
<td>Next.js + Fastify 보일러플레이트를 미리 만들어두기. 인증, DB 연결, 배포 스크립트 포함.</td>
</tr>
<tr>
<td><strong>아이디어 사전 확정</strong></td>
<td>해커톤 1주일 전에 팀원들과 아이디어 3개 정도 후보 정하기</td>
</tr>
<tr>
<td><strong>데모 중심 개발</strong></td>
<td>배포는 나중에. 시연 가능한 핵심 화면 3개에 집중하기</td>
</tr>
<tr>
<td><strong>기능 단위 개발</strong></td>
<td>프론트/백 분리 대신 기능 단위로 작업. AI 코딩 도구 적극 활용</td>
</tr>
<tr>
<td><strong>API 폴백 준비</strong></td>
<td>외부 API 사용 시 rate limit 대비해서 캐싱 + 데모 데이터 준비</td>
</tr>
</tbody></table>
<hr />
<h2>부록: 행사 운영 리뷰</h2>
<h3>아쉬웠던 운영</h3>
<ol>
<li><p><strong>대회 3~4일 전에 장소 공지</strong></p>
</li>
<li><p><strong>Discord 질문에 대한 답변이 거의 안됨</strong></p>
</li>
<li><p><strong>신청이 선착순이 아니라 행사장 도착이 선착순</strong></p>
<ul>
<li><p>총 427명이 지원했는데 그 중 250명만 들어갈 수 있다고 했음</p>
</li>
<li><p>전 날 미리 서울로 올라오는 지방 분들은 어떻게 해야 하냐고 당황</p>
</li>
</ul>
</li>
<li><p><strong>전력 부족</strong>: 문의했을 때 임시로 벽에 붙어서 충전하라는 말 밖에...</p>
</li>
</ol>
<h3>좋았던 점 (가성비 최고)</h3>
<ol>
<li><p><strong>무료인데 퀄리티 좋은 식사</strong>: 행사 장소가 연회장으로 쓰던 곳이라 식사가 맛있었다. 삼시세끼 + 커피, 주스, 과자, 에너지바까지 제공해줬다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/65556e531147f173adab7767/4a9bf9a9-7c3b-47bb-b5fc-151c23e0d487.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p><strong>네트워킹 분위기가 좋았다</strong>: 개발하느라 바빴지만 네트워킹하면서 더 알차게 보내신 분들이 많았다.</p>
</li>
</ol>
<hr />
<h2>마치며</h2>
<p>3년 만의 해커톤이었다. 오랜만에 업무 외 개발에 푹 빠져서 할 수 있어서 좋았다. 운영은 약간 아쉬운 부분들이 있었지만, 무료임에 불구하고 맛있는 밥을 세 끼나 챙겨줬다는 게 감동적이었다. 결론적으로는 한강 위에서 코딩하고, 좋은 팀원들과 재밌는 프로젝트를 만들면서 즐겁게 보낸 시간이었다.</p>
<p>다음 해커톤에서는 이번에 배운 교훈들을 적용해서 더 완성도 높은 결과물을 만들어보고 싶다. 그리고 Seoul Adventure는 사이드로 계속 개발해봐도 좋을 것 같다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/65556e531147f173adab7767/3c72609b-74e1-419f-a630-0c0986ebece2.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[오픈소스 기여모임 10기 후기 - 첫 Pr을 올리기까지]]></title><description><![CDATA[개발자라면 누구나 한 번쯤 오픈소스 기여에 대한 환상을 가져본 적 있을 거다. 하지만 막상 시작하려면 어디서부터 해야 할지 막막하고, 괜히 대단한 걸 해야 할 것 같은 부담감에 선뜻 시작하기는 어려운 것 같다. 나 또한 해보고 싶다는 마음만 가지고 계속 미뤄왔다.
그러다 2025년 말 쯤에 오픈채팅방과 글또 슬랙 채널에서 "오픈소스 기여모임" 10기 모집글을 봤다. 2년 넘게 500명 이상의 참가자와 함께 1000개 이상의 PR을 만들어온 커뮤...]]></description><link>https://blog.aqudi.me/opensource-contribution-group-10-review</link><guid isPermaLink="true">https://blog.aqudi.me/opensource-contribution-group-10-review</guid><category><![CDATA[오픈소스기여모임]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[community]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Thu, 05 Feb 2026 14:37:03 GMT</pubDate><content:encoded><![CDATA[<p>개발자라면 누구나 한 번쯤 오픈소스 기여에 대한 환상을 가져본 적 있을 거다. 하지만 막상 시작하려면 어디서부터 해야 할지 막막하고, 괜히 대단한 걸 해야 할 것 같은 부담감에 선뜻 시작하기는 어려운 것 같다. 나 또한 해보고 싶다는 마음만 가지고 계속 미뤄왔다.</p>
<p>그러다 2025년 말 쯤에 오픈채팅방과 글또 슬랙 채널에서 "오픈소스 기여모임" 10기 모집글을 봤다. 2년 넘게 500명 이상의 참가자와 함께 1000개 이상의 PR을 만들어온 커뮤니티라고 했다. 운영진도 Next.js, Spring, OpenJDK, ESLint 같은 메이저 오픈소스 기여자/메인테이너 분들이라 믿음이 갔다.</p>
<p>오픈소스 기여 모임 팀 블로그도 살펴봤다. 운영진인 김인제님이 작성하신 모임 소개글과 기여 가이드를 읽어보니, "누구나 쉽게 오픈소스 기여를 시작할 수 있도록 돕겠다"는 진심이 느껴졌다. 모임 내에서 처음부터 끝까지 1:1로 운영진이 도와준다고 하고, PR 올리고 기부하면 보증금 환급도 해준다고 한다. 안 할 이유가 없는 모임이라고 생각해서 바로 지원했다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770300471031/29bd8964-f994-4131-ad72-3cc59ce9c884.png" alt="운영진이신 인제님에 대한 간증까지 있었다" class="image--center mx-auto" /></p>
<h2 id="heading-64ky7j2yioq4soyxrcdqsrdqs7w">나의 기여 결과</h2>
<p>오픈소스 기여모임의 필수 인증 조건은 두 가지다.</p>
<ol>
<li><p><strong>PR 오픈</strong></p>
</li>
<li><p><strong>오픈소스 프로젝트에 5$ 기부</strong></p>
</li>
</ol>
<p>나는 <a target="_blank" href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a>라는 모니터링 서비스에 PR을 올렸다.<br />홈서버를 운영하면서 이 서비스를 직접 쓰고 있어서 기여할 프로젝트로 선택했다.</p>
<p><strong>PR</strong>: <a target="_blank" href="https://github.com/louislam/uptime-kuma/pull/6805">RSS Pub Date 타임존 버그 수정</a></p>
<p>그리고 <strong>약 26시간 만에 머지</strong>됐다! 🎉</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770301313442/deac1f97-f4a4-44a0-b64d-1e5756110b55.png" alt class="image--center mx-auto" /></p>
<p>기부는 회사에서 주로 쓰고 있는 웹 프레임워크 <a target="_blank" href="https://github.com/fastify/fastify">fastify</a>에 했다. 5$가 큰 금액은 아니지만, 내가 매일 쓰는 생태계에 조금이라도 보탰다는 느낌이 생각보다 뿌듯했다. 덤으로 GitHub 프로필에 스폰서 뱃지도 달렸다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770301027535/b0628e5a-1070-4b08-9a5f-46ea5b185730.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-3">기여 과정: 마감 3일 전부터 시작한 스프린트</h2>
<p>솔직히 말하면 끝까지 미루다가 마감 3일 전에야 본격적으로 시작했다. 모임에 들어온 건 12월 말이었는데, 내가 먼저 시작해야 운영진이 도와줄 수 있는 구조라 결국 2~3주를 날린 셈이다. 다행히 인제님이 올려주신 <a target="_blank" href="%EB%A7%81%ED%81%AC">AI와 함께 오픈소스 기여하기</a> 블로그 글 덕분에 빠르게 방향을 잡을 수 있었다.</p>
<h3 id="heading-1-eba781ed81ac">1. 이슈 수집기로 후보 이<a target="_blank" href="%EB%A7%81%ED%81%AC">슈 모으기</a></h3>
<p><a target="_blank" href="%EB%A7%81%ED%81%AC">블로그에서 소개된</a> <a target="_blank" href="https://contribution-issue-collector.streamlit.app/">이슈 수집기</a>를 사용해서 Uptime Kuma의 이슈들을 수집했다. 이슈 수집기는 GitHub 이슈 페이지의 내용을 페이지별로 가져온 후 AI에게 분석을 요청할 수 있는 프롬프트를 생성해주는 도구다. 손쉽게 수십개의 이슈를 모아서 볼 수 있어서 매우 편리하다.</p>
<p>수집한 이슈들을 ChatGPT와 Gemini에 넣어서 난이도별, 종류별로 분류하고 분석했다. AI가 "이 이슈는 첫 기여에 적합하다", "이건 난이도가 높다" 같은 판단을 내려줘서 탐색 시간을 많이 줄일 수 있었다.</p>
<h3 id="heading-2">2. 이슈 탐색 후 디스코드에 "이슈 선정 도움" 포스팅</h3>
<p>분석된 이슈들을 하나씩 읽어보면서 할 만한 것을 골랐다. 그리고 오픈소스 기여모임 디스코드에 "이슈 선정 도움" 글을 올렸다.</p>
<p>내가 올린 글에는 AI 분석 결과를 그대로 포함했다. "왜 기여하기 좋은지", "원인 추정", "해결 방향", "기준 적합도/난이도" 같은 항목들이 정리되어 있어서 운영진이 빠르게 판단할 수 있었던 것 같다.</p>
<h3 id="heading-3-1">3. 히스토리 있는 이슈, 최소 수정으로 접근</h3>
<p>내가 선택한 이슈는 RSS 피드의 <code>pubDate</code>가 서버 타임존만큼 잘못된 시간을 표시하는 버그였다.<br />예를 들어 UTC+9 서버에서는 실제보다 9시간 앞당겨진 시간이 표시되는 식이었다.</p>
<p>이 이슈에는 이미 다른 기여자가 시도했다가 중단한 PR이 있었다.<br />간단한 이슈임에도 여러 방식을 시도하다가 논쟁이 길어진 케이스였다.</p>
<ul>
<li><p>“문자열에 Z 붙이기 방식” → 동작하지만 메인테이너가 찝찝해함</p>
</li>
<li><p>“regex 조건부 Z” → 더 복잡하고 리뷰가 늘어남</p>
</li>
<li><p>“dayjs.utc로 강제 파싱” → hacky하다는 논쟁</p>
</li>
</ul>
<p>결국 작성자가 "더 이상 논쟁하기 싫다"며 PR을 닫았다. 이 히스토리를 보면서 오픈소스 기여도 결국 존중에서부터 시작하는 거구나 느꼈다.</p>
<p>인제님이 리뷰해주시면서 **"닫힌 PR도 보면 방향성을 메인테이너와 싱크하는 게 중요할 것 같다"고** 조언해주셨다. 그리고 "최소변경으로 리뷰거리를 줄여야 한다", "버그 티켓이니 로컬에서 버그 재현부터 해보라"는 피드백도 주셨다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770301764886/7216f04c-d7a7-45a0-add0-15fe0ff69987.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-4-pr">4. 최소 수정 + 테스트 코드로 PR 완료</h3>
<p>피드백을 받고 바로 로컬에서 버그를 재현했다. 재현 후 수정 코드를 내 fork에 올리고, 인제님께 공유드렸더니 테스트 작성하고 PR open하면 될 거라고 말씀주셨다. 이런 빠른 피드백 덕분에 자신감을 가지고 파바박 진행할 수 있었다.</p>
<p>버그의 원인은 DB에 저장된 <code>heartbeat.time</code>이 타임존 정보 없는 UTC 타임스탬프인데, JavaScript의 <code>Date</code> 생성자가 이를 로컬 시간으로 해석하면서 발생한 것이었다. 해결은 단 1줄이었다.</p>
<pre><code class="lang-js"><span class="hljs-comment">// 변경 전</span>
<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(heartbeat.time)

<span class="hljs-comment">// 변경 후</span>
dayjs.utc(heartbeat.time).toDate()
</code></pre>
<p>이전 기여자가 복잡하게 접근했던 것과 달리, 핵심만 건드리니까 리뷰도 빠르게 진행됐다. 메인테이너도 <strong>"That is what I wanted"라며</strong> 긍정적으로 반응해줬고, 인제님도 축하해주셨다. 😊</p>
<h2 id="heading-7iau7keb7zwcio2bhoq4sa">솔직한 후기</h2>
<h3 id="heading-7kkl7jwy642yioygka">좋았던 점</h3>
<p>가장 좋았던 건 <strong>"혼자가 아니다"라는</strong> 점이었다. 이슈 선정부터 PR까지, 내가 놓치고 있는 포인트를 운영진이 계속 잡아줬다.</p>
<ul>
<li><p>다른 참가자들 보니까 이슈가 너무 크거나 이미 진행 중인 건 미리 걸러주시더라</p>
</li>
<li><p>내 경우는 간단한 이슈였지만, "버그 재현 → 수정 확인 → 테스트 코드 작성" 순서로 명확하게 가이드해주셨다</p>
</li>
<li><p>"첫 기여는 간단한 이슈 + 최소 변경"을 계속 강조해줘서 마음이 가벼워졌다</p>
</li>
<li><p>디스코드에서 다른 참가자들의 진행 상황도 볼 수 있어서 덜 외로웠다</p>
</li>
</ul>
<h3 id="heading-7ja066ck7jug642yioygka">어려웠던 점</h3>
<p>오픈소스는 내가 책임질 수 없는 환경(다양한 DB, 타임존, 배포 방식)까지 고려해야 해서 "어디까지가 적절한 수정인가?"의 감이 잘 안 왔다. 다행히 여러 환경에서의 테스트가 마련되어 있었고 매우 작은 변경이라 아무 문제가 없었다. 그리고 이전 PR의 내용을 보니까 메인테이너의 성향까지 고려를 해야 한다는 게 생각지도 못했던 어려운 부분인 것 같다.</p>
<h3 id="heading-67cy7isx7zwgioygka">반성할 점</h3>
<p>마감 3일 전까지 미룬 건 앞에서도 말했지만, 돌이켜보면 <strong>시작만 했으면 됐다</strong>. 첫 이슈 선정 글 올리는 데 30분도 안 걸렸는데, 그걸 2~3주나 미룬 거다. 뭔가 대단한 걸 해야 할 것 같은 부담감이 시작을 막았는데, 막상 해보니 이슈의 난이도와 상관없이 모든 기여가 의미있으니 도와준다는 느낌으로 하면 되는 것 같다.</p>
<h2 id="heading-67cw7jq0ioygka">배운 점</h2>
<ol>
<li><p><strong>이슈를 고를 때는 "증상이 명확한가"가 제일 중요하다.</strong> 스크린샷, 재현 방법, 메인테이너 코멘트가 있는 이슈가 해결하기 쉽다.</p>
</li>
<li><p><strong>AI는 분석과 초안에 도움되지만, 최종 판단은 사람이 해야 한다.</strong> 프로젝트 문화나 리뷰 논점은 결국 사람이 챙겨야 한다.</p>
</li>
<li><p><strong>기여는 거창한 기능 추가가 아니다.</strong> 작은 버그 하나를 정리해서 전 세계 사용자의 시간을 아껴주는 것도 의미 있는 기여다.</p>
</li>
</ol>
<p>다음에도 오픈소스 기여 모임에 참여한다면 이렇게 하려고 한다:</p>
<ul>
<li><p>최대한 1주차에 이슈 선정 + 재현까지 끝내기</p>
</li>
<li><p>PR 설명은 미리 초안이라도 써두기 (나중에 쓰려면 기억이 휘발됨)</p>
</li>
<li><p>작은 변경이라도 검증 근거(빌드/테스트/재현 환경)를 남기기</p>
</li>
</ul>
<h2 id="heading-7j2065wiou2houtpoyxkoqyjcdstptsspztlanri4jri6q">이런 분들에게 추천합니다</h2>
<ul>
<li><p>"해보고 싶은데 어디서부터 시작해야 할지 모르겠다" 하는 사람</p>
</li>
<li><p>혼자 PR 올리다 지치거나 재미를 못 느꼈던 사람</p>
</li>
<li><p>내가 쓰는 오픈소스에 한 번쯤은 흔적을 남기고 싶은 사람</p>
</li>
</ul>
<p>오픈소스 기여가 막막할수록 이 모임을 더 추천한다. 운영진 분들이 정말 친절하고, 체계적인 프로세스가 있어서 첫 기여의 두려움을 많이 덜어준다. 보증금 3만원도 PR 완료 후 전액 환불되고, 오프라인 밋업에 참석하면 오픈소스 키링 굿즈와 PR 썸네일 스티커도 받을 수 있다.</p>
<p>나는 참여를 못했지만, 모임 기간 중 오프라인 세션과 온라인 모임도 열어서 이슈 선정에 어려움이 있는 분들을 운영진이 직접 도와주셨다. 다음에 참여하는 분들은 이런 기회도 꼭 활용하면 좋겠다. 그만큼 오픈소스의 접근성을 높이는 데 진심인 모임이다.</p>
<h2 id="heading-66ei66y066as">마무리</h2>
<p>처음에는 오픈소스 기여가 대단한 사람들만 하는 일처럼 느껴졌는데, 이번 모임 덕분에 "그냥 한 걸음씩 하면 되는 거구나"를 몸으로 배웠다.</p>
<p>PR 하나 올리고, 5$ 기부까지 하고 나니까 2026년을 시작하는 느낌이 꽤 좋았다. 다음에는 더 여유 있게, 더 꾸준하게 해봐야겠다.</p>
<hr />
<h2 id="heading-7lc46rogioyekoujja">참고 자료</h2>
<ul>
<li><p><a target="_blank" href="https://medium.com/opensource-contributors/%EB%AA%A8%EC%A7%91%EC%A4%91-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%EB%AA%A8%EC%9E%84-10%EA%B8%B0-%EC%B0%B8%EA%B0%80%EC%9E%90%EB%A5%BC-%EB%AA%A8%EC%A7%91%ED%95%A9%EB%8B%88%EB%8B%A4-2026-01-%EC%A7%84%ED%96%89-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%ED%82%A4%EB%A7%81-%EA%B5%BF%EC%A6%88-%EC%84%A0%EB%AC%BC%EA%B9%8C%EC%A7%80-e95a8e528056">오픈소스 기여모임 10기 모집글</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/opensource-contributors/%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4%EC%9D%98-%ED%8C%90%EB%8F%84%EB%A5%BC-%EB%B0%94%EA%BF%80-ai%EB%A1%9C-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C%EC%99%80-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8-%EA%B3%B5%EC%9C%A0-2db85bf736b8">AI 활용 오픈소스 기여 가이드 (김인제)</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[😢 글또 10기 활동 회고 — “글또야, 가지 마…”]]></title><description><![CDATA[들어가며
드디어 글또 10기 활동 회고를 정리해본다.6개월간의 여정을 뒤돌아보니 정말 많은 일들이 있었다.
글또라는 커뮤니티를 8기가 한창 진행되고 있을 때 알았는데 이름부터 인상이 강렬했다.

"글쓰는 또라이가 세상을 바꾼다."

유쾌하고 독특한 문구에 피식 웃으며, '여긴 도대체 어떤 사람들이 모이는 곳이지?' 하고 넘겼었다.
재밌는 건 결국, 나도 그 "또라이들" 중 한 명이 되었다는 것이다. 😌

글또는 개발자들이 2주에 한 번 글을 ...]]></description><link>https://blog.aqudi.me/geultto-10th-review</link><guid isPermaLink="true">https://blog.aqudi.me/geultto-10th-review</guid><category><![CDATA[글또]]></category><category><![CDATA[회고]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Thu, 31 Jul 2025 14:59:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753973926246/13ad1de5-63c1-487b-9cfb-8e766c92500b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-65ok7ja06rca66mw">들어가며</h2>
<p>드디어 글또 10기 활동 회고를 정리해본다.<br />6개월간의 여정을 뒤돌아보니 정말 많은 일들이 있었다.</p>
<p>글또라는 커뮤니티를 8기가 한창 진행되고 있을 때 알았는데 이름부터 인상이 강렬했다.</p>
<blockquote>
<p>"글쓰는 또라이가 세상을 바꾼다."</p>
</blockquote>
<p>유쾌하고 독특한 문구에 피식 웃으며, '여긴 도대체 어떤 사람들이 모이는 곳이지?' 하고 넘겼었다.</p>
<p>재밌는 건 결국, 나도 그 "또라이들" 중 한 명이 되었다는 것이다. 😌</p>
<blockquote>
<p><strong>글또는 개발자들이 2주에 한 번 글을 쓰며 성장하는 커뮤니티다.</strong><br />글을 중심으로 서로 연결되고, 경험을 나누고, 함께 성장하는 따뜻한 공간이다.<br />👉 <a target="_blank" href="https://geultto.github.io/">글또 소개 페이지 바로가기</a></p>
</blockquote>
<h2 id="heading-8jklcdquidrmjasioyzncdsp4dsm5dtlojrgpjsmpq">🤔 글또, 왜 지원했나요?</h2>
<p>글또 10기에 지원했을 당시, 나는 입사한 지 7개월 된 백엔드 개발자였다. 백엔드 1, 프론트 1로 구성된 팀에서 혼자 백엔드를 전담하다 보니, 개발 일정 따라 정신없이 일은 하고 있지만 <strong>내가 잘하고 있는지 전혀 모르겠어서 너무 답답했다.</strong> 실제로 그때 썼던 회고들을 보면 참… 인정과, 관심에 목말랐다는 게 여실히 보였다.</p>
<p><strong>"내가 잘하고 있는 걸까?" "내가 원하는 커리어는 뭘까?"</strong> 하는 고민은 많고, 주변에서는 물경력이니 JSON 상하차니 이런 이야기가 들려오니까 앞으로 내 커리어 잘 만들어 갈 수 있을까 하는 걱정, 불안이 컸었다.</p>
<p>그래서 더욱 더 나와 비슷한 고민을 나눌 수 있는 사람들과의 연결이 절실했던 것 같다. 이런 시기에 글또에 들어가서 다양한 개발자들과 같이 글쓰고 이야기를 나눌 수 있었다니! 운, 타이밍 다 좋았다. 🥹🍀</p>
<blockquote>
<p><strong>"10기가 마지막이다."</strong></p>
</blockquote>
<p>물론, 처음에는 2주에 1편 글 제출과 훈련소 일정이 겹친다는 것 때문에 살짝 망설이기도 했는데 마지막이라는 문구가 이런 망설임을 단번에 날려버렸었다. 😂</p>
<p>마지막이라는데 참을 수 없지!</p>
<h2 id="heading-6">✍️ 6개월 동안 어떤 활동을 했나요?</h2>
<p>글또 시작하면서 했던 다짐 중 하나가 <strong>글 2주에 한 편씩 쓰기</strong>와 <strong>커피챗 월 2회 이상하기</strong>였는데 90% 이상은 달성했다!</p>
<p>글또 10기 활동 기간 동안 <strong>총 10편의 글</strong>을 썼다. 아쉽게도 패스와 미제출이 하나씩 있었다. 2회차 때 훈련소에 가 있어서 패스한 거랑 마지막 회차 때 지금 이 회고 쓰다가 감상에 젖어 결국 완성 못하고 미제출로 끝났다. 그래도 이 회고가 다시 글쓰기 흐름을 만들어줄 거라고 생각하니까 오히려 좋지 않나 싶다 😊</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744722923001/15004086-c4e7-4538-8270-48d9139682c8.png" alt class="image--center mx-auto" /></p>
<p>작성한 글들을 주제로 분류해보면 아래와 같다.</p>
<ul>
<li><p><strong>회고 3편</strong></p>
</li>
<li><p><strong>기술 5편</strong></p>
</li>
<li><p><strong>책, 컨퍼런스 후기 2편</strong></p>
</li>
</ul>
<p>아직 글 쓰고 올리는 게 익숙하지는 않지만 글쓰는 재미는 좀 알게 된 것 같다. 글또 끝나고 글을 안 쓰긴 했지만 그래도 “어? 이거 글 써보고 싶은데”하는 것들이 종종 보인다. 오늘 회고 글을 시작으로 조금씩 써서 올려보려고 한다!</p>
<p>퇴고또라는 소모임 활동으로 글 피드백도 서로 주고 받아봤는데 내 글들은 아직 갈 길이 멀다고 느껴졌다. 그래도 글을 썼기 때문에 부족한 부분이 보이고 채울 기회가 주어진 것이니 기쁘게 생각한다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753969552238/f1748009-a315-4bdf-b5cd-fe72749e0943.png" alt class="image--center mx-auto" /></p>
<p>커피챗도 열심히 참여했다. <strong>커피챗은 총 23회</strong> 인증했는데 글또 이후로도 슬랙은 계속 유지되고 있어서 종종 사람들과 어울리다보니 어느새 30개가 됐다. 글또 내 소모임에서 열리는 오프라인 모임, 반상회, 2024 성과 회고, 눈썰매타러 가기 등 다양하고 즐거운 활동들이 많았다. 요즘은 좀 뜸하게 들어가는데 들어갈 때마다 반가운 얼굴들이 많이 보여서 기분이 좋다.</p>
<p>커피챗하기 목표를 달성하기 위해 용기내서 신청한 1대1 커피챗도 저 중에 11회나 된다!</p>
<p>커피챗 외에도 <code>쓸모있는 10분 모각글또</code>, <code>퇴고또</code>, <code>다진마늘</code>(일간 목표 설정 챌린지) 등 다양한 인증 소모임들에도 참석했었는데 매일 꾸준히 하지는 못했지만 덕분에 나만의 글쓰기 루틴이나 일간 목표 설정 방법 등 만들어서 아직까지도 잘 써먹고 있다.</p>
<p>특히 자랑하고 싶은 건 <strong>내 생에 첫 마라톤으로 10km를 1시간 4분 만에 완주</strong>했다는 것이다. 신경써서 연습일정 만들어주시고 응원해주신 글또 분들 덕분에 시작할 수 있었고 마무리할 수 있었다고 생각한다. 뭐든 처음 시작할 때 그 일을 좋아하고 이미 해본 사람들이 있다는 건 엄청난 축복인 것 같다.</p>
<p>지금 다시 돌아봐도 글또 활동은 “글쓰기”보다 훨씬 많은 영역에서 크고 작은 변화들을 만들어줬다.</p>
<p>같이 하니까 평소에 하지 않던 일도 더 쉽게 도전해볼 수 있었고, 더 오래 지속할 수 있었다고 생각한다!</p>
<h2 id="heading-4pivioqwgoyepsdquldslrxsl5ag64ko64quio2znoupmeydgcdrrztqsidsmpq">☕ 가장 기억에 남는 활동은 뭔가요?</h2>
<p>글또 활동 중 가장 인상 깊었던 건 단연 <strong>커피챗</strong>이다. (커피챗도 엄연히 글또 메인 컨텐츠 중 하나다!)</p>
<p>처음 커피챗을 요청할 때 '내가 그 분에게 어떤 도움이 될까? 귀찮기만 한 건 아닐까?'라는 걱정이 앞서서 커피챗이 망설여졌었다. 하지만 그런 걱정이 무색하게도 만나는 분들 모두 적극적이고 따뜻하게 맞아주셨었다.</p>
<p>특히 첫 커피챗 상대였던 <strong>은찬님</strong>과의 만남은 아직도 기억에 선명하다.<br />Flutter와 1인 개발, 매일 글쓰기 이런 키워드에 은찬님에 대한 호기심이 생기면서 설렘반, 걱정반으로 DM을 보냈는데, 흔쾌히 응해주신 데다 직접 회사 근처로 찾아와주셔서 너무 감사했다.</p>
<p>'회사 없이도 살아남을 수 있는 역량'이나 '개발자로서의 성장 방향' 같은 주제들은 그때 처음 구체적으로 생각해본 것들이었고, 그 자리에서 받은 영감과 따뜻한 관심 덕분에 이후로도 커피챗을 계속 신청할 수 있는 용기가 생겼다.</p>
<p>이후에도 올라오는 글을 읽으면서 궁금해진 분들이 있으면 용기내서 말을 드려봤고 <strong>현업 고민, 글쓰기 루틴, 사이드 프로젝트</strong> 등 다양한 이야기를 나눴었다. 그 중 <strong>“나를 위해 더 열심히 해야 한다”</strong>라는 말이 특히 기억에 남는 것 같다. 글또 하기 전에도 종종 찾아봤던 블로그 ‘연로그’ 운영하시는 시연님과 나눈 이야기였는데 사람들이 열심히 일하는 원동력에 대한 궁금증이 있던 나에게 ‘열심히 일하는 게 다른 누구를 위한 게 아니구나!’하는 깨달음을 줬었다.</p>
<p>단체로 만났던 커피챗 중 기억에 남는 것은 <strong>2024년 성과 회고 커피챗</strong>이다. 처음 회사에서 성과 리뷰를 작성하게 됐는데 '이번 해에 뭘 해왔지?' 막막했다. 이런 시기에 글또 분들이랑 같이 고민하고 나눌 수 있는 자리가 생겨서 내 1년을 돌아보며 어떤 성과를 이뤘는지 고민하고 피드백 받을 기회는 내게 너무 소중했다.</p>
<p>글또가 단순히 '2주에 한 번씩 글을 제출하는 모임'이 아니라 <strong>글을 중심으로 사람들을 연결하고 따스함을 나눠주는 커뮤니티</strong>라는 걸 6개월 간의 커피챗을 통해 몸으로 직접 느낄 수 있었다.</p>
<h2 id="heading-8jklcdslytsiazsm6drjzgg7kcq7j20ioyeioucmoyald8">🤔 아쉬웠던 점이 있나요?</h2>
<p><strong>가장 아쉬운 건 시간:</strong> 6개월이 이렇게 짧은 거였나 싶다. 😱 눈 깜짝할 새에 6개월이 지났고 지금은Ï 거기에 더해 4개월이 추가로 더 흘렀다. 왜 이렇게 시간이 빠른 건지… 그저 아쉬울 따름이다. 잠깐이라도 한 눈 팔면 분기가 지나가고 ‘나 뭐했지’ 싶다. 정신차리고 제때 제때 회고하면서 알차게 보내자!</p>
<p><strong>커뮤니티 기여</strong>: 받기만 하는 것보다 다른 분들 글에 더 적극적으로 댓글을 달고 피드백을 주고 싶었는데, 막상 하려니 '내가 이런 말을 해도 될까?' 하는 망설임이 컸다. 또 내 글 쓰는데 바빴지 다른 사람들 글을 잘 안 읽은 것도 아쉽다. 이제 와서 보니까 재밌고 따라쓰고 싶은 글들이 많은데 그때 보고 더 많은 사람들과 이야기를 나눠볼 걸 싶다.</p>
<p><strong>글의 깊이, 재밌는 글</strong>: 기술적인 내용을 다룰 때 더 깊이 파고들어 정말 도움이 되는 글을 쓰고 싶었는데, 시간에 쫓겨 표면적인 내용에만 그친 것 같아 아쉬웠다. 또한 내가 쓴 글이 재미가 없다고 느껴졌었는데 퇴고하면서 피드백을 받아보니 내 이야기가 빠져있는 것 같다라는 말을 들었었다. 앞으로는 글을 쓸 때 내 경험, 내 생각이 녹아있는 글을 쓰도록 노력해야겠다.</p>
<h2 id="heading-66ei66y066as">마무리</h2>
<p>벌써 글또 10기 마무리된지 4개월이 지나고 있다. 2024년 10월부터 2025년 3월까지 바빠서 정신없기도 했었지만 글또 덕분에 더 즐겁게 보낼 수 있었고 회사 생활도 이에 힘입어 더 열심히 할 수 있었던 것 같다. 자주 마주치던 분도 아예 얼굴도 못 본 분도 언젠가 꼭 한 번은 마주칠 것 같으니 그때까지 더 열심히 살아야겠다! 그리고 그때가 되면 “요즘도 글또 덕분에 글쓰기 시작하고 꾸준 잘 쓰고 있어요”라고 당당히 말할 수 있었으면 좋겠다 😊</p>
<p>그때를 위해서 글또하면서 몸소 느낀 <strong>"완벽하지 않아도 시작하기"</strong>를 실천하며 조금씩 글을 쌓아보고 조금씩 고쳐보면서 나만의 길을 만들어가봐야겠다!</p>
<p><strong>글또야, 잘 가~</strong> 👋</p>
]]></content:encoded></item><item><title><![CDATA[Serverless 환경에서 배포 전 환경변수 검증 자동화하기: TypeBox와 Bitbucket Pipeline 활용기]]></title><description><![CDATA[들어가며
배포 직후, 환경변수가 제대로 설정되지 않아 여러 API가 제대로 작동하지 않는 일이 있었습니다. 다행히 밤에 사용자가 없을 때 문제가 있었던 거라 영향도는 크지 않았지만 앞으로도 계속해서 발생할 수 있는 문제이기 때문에 해결해야 겠다고 생각했습니다. 개발 단계에서 문제가 발견되면 가장 좋겠지만, 현재 팀 상황에서는 백엔드 개발을 혼자 담당하고 있어 코드 리뷰나 검증 프로세스를 갖추기가 쉽지 않았습니다. 그래서 최소한 배포 전에 자동으...]]></description><link>https://blog.aqudi.me/serverless-env-validation-typebox-bitbucket</link><guid isPermaLink="true">https://blog.aqudi.me/serverless-env-validation-typebox-bitbucket</guid><category><![CDATA[serverless framework]]></category><category><![CDATA[Validation]]></category><category><![CDATA[Typebox]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 16 Mar 2025 14:57:36 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-65ok7ja06rca66mw">들어가며</h2>
<p>배포 직후, 환경변수가 제대로 설정되지 않아 여러 API가 제대로 작동하지 않는 일이 있었습니다. 다행히 밤에 사용자가 없을 때 문제가 있었던 거라 영향도는 크지 않았지만 앞으로도 계속해서 발생할 수 있는 문제이기 때문에 해결해야 겠다고 생각했습니다. 개발 단계에서 문제가 발견되면 가장 좋겠지만, 현재 팀 상황에서는 백엔드 개발을 혼자 담당하고 있어 코드 리뷰나 검증 프로세스를 갖추기가 쉽지 않았습니다. 그래서 최소한 <strong>배포 전에 자동으로 환경변수를 검증</strong>하는 단계를 마련해야겠다고 판단했고, 이 글을 통해 어떤 방식으로 해결했는지 공유해보려 합니다.</p>
<h2 id="heading-66y47kccioydge2zqtog7jmcio2zmoqyveuzgoyimcdqsodspp3snyqg7j6q64z7zmu7zw07jw8io2wioucma">문제 상황: 왜 환경변수 검증을 자동화해야 했나</h2>
<h3 id="heading-7j6l7jwg7j2yioybkoydua">장애의 원인</h3>
<ul>
<li><p><strong>누락된 환경변수</strong>: 서버 실행 전에 환경변수를 제대로 확인하지 못해, Lambda를 Serverless Framework로 배포했지만 실제로 서버는 실행되지 않는 치명적인 상황이 발생했습니다.</p>
</li>
<li><p><strong>복합적인 요인</strong>: 개발 단계에서의 검토 프로세스가 미흡했고, 배포 전 검증 프로세스도 없었으며, 혼자서 밤에 서둘러 배포하다 보니 실수를 놓칠 가능성이 컸습니다. 자동화된 테스트 역시 충분치 않아 문제를 조기에 발견하기 어려웠습니다.</p>
</li>
</ul>
<h3 id="heading-7jmcio2zmoqyveuzgoyimcdsnpdrj5ntmztrpbwg6rog66ck7zai64ky">왜 환경변수 자동화를 고려했나</h3>
<ul>
<li><p><strong>백엔드를 혼자 운영</strong>: 팀 내에 백엔드 담당자가 저 한 명뿐이라, 코드 리뷰나 배포 프로세스를 고도화하기엔 무리가 있었습니다.</p>
</li>
<li><p><strong>실수 전제</strong>: 사람이 하는 일이니 언제든 실수할 수 있다고 보고, 실수가 발생해도 장애가 최소화되도록 하는 안전장치가 필요했습니다.</p>
</li>
<li><p><strong>빠른 해결책</strong>: 여러 프로세스를 동시에 개선하기는 어렵지만, 환경변수 검증만큼은 쉽고 빠르게 적용 가능하지만 영향도가 높다고 생각했습니다.</p>
</li>
</ul>
<p>이러한 이유로, 배포 파이프라인에서 <strong>환경변수를 자동 검증</strong>하는 단계를 추가하게 되었습니다. 결과적으로, 배포 시점에 유효하지 않은 환경변수나 누락된 값을 미리 잡아낼 수 있어 "아예 API 사용이 불가능 한 상황"을 방지할 수 있었습니다.</p>
<h2 id="heading-typebox-bitbucket-pipeline">해결책: TypeBox와 Bitbucket Pipeline을 활용한 환경변수 자동 검증</h2>
<p>현재 제가 관리 중인 프로젝트에서는 다음과 같은 기술 스택을 사용하고 있습니다.</p>
<ul>
<li><p>AWS Lambda</p>
</li>
<li><p>Serverless Framework</p>
</li>
<li><p>Fastify(서버 프레임워크)</p>
</li>
<li><p>TypeBox(Fastify 스키마 검증 도구)</p>
</li>
</ul>
<p>이 프로젝트에서 환경 변수 스키마를 정의하고 검증하는 작업에도 TypeBox를 도입하게 된 이유는 다음과 같습니다.</p>
<ol>
<li><p><strong>Fastify와의 호환성</strong>: 이미 Fastify에서 TypeBox로 스키마 검증을 해왔기에, 이를 확장하여 <strong>환경 변수 검증</strong>에도 손쉽게 적용할 수 있었습니다.</p>
</li>
<li><p><strong>팀 차원의 익숙함</strong>: 사내 다른 팀에서도 TypeBox를 적극적으로 활용하고 있어, 이미 팀원들이 사용법에 익숙하다는 장점이 있었습니다.</p>
</li>
<li><p><strong>TypeScript 연계 가능성</strong>: TypeBox는 TypeScript 환경에서 객체 스키마를 정의하는 데 유용하며, 타입스크립트 컴파일 단계와 런타임 단계 모두에서 검증을 지원합니다.</p>
</li>
</ol>
<h3 id="heading-typebox">간단한 typebox 스키마 정의 예시</h3>
<p>typebox를 활용해 환경변수를 검증할 때는, <code>Type.Object()</code>로 각 환경변수를 정의하고, <a target="_blank" href="https://github.com/sinclairzx81/typebox?tab=readme-ov-file#check"><code>Value.Check()</code></a>나 <a target="_blank" href="https://github.com/sinclairzx81/typebox?tab=readme-ov-file#typecheck-typecompiler"><strong>compiler</strong></a> 기능을 이용해 런타임 검증을 수행할 수 있습니다.</p>
<pre><code class="lang-ts"><span class="hljs-comment">// env-schema.ts</span>
<span class="hljs-keyword">import</span> { Type } <span class="hljs-keyword">from</span> <span class="hljs-string">'@sinclair/typebox'</span>;
<span class="hljs-keyword">import</span> { Value } <span class="hljs-keyword">from</span> <span class="hljs-string">'@sinclair/typebox/value'</span>;

<span class="hljs-comment">// 기본적인 예시</span>
<span class="hljs-keyword">const</span> EnvSchema = Type.Object({
  DYNAMO_TABLE_NAME: Type.String(),
  API_KEY: Type.String(),
  <span class="hljs-comment">// 필요한 다른 변수들...</span>
});

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">validateEnvValues</span>(<span class="hljs-params">envObject</span>) </span>{
  <span class="hljs-keyword">const</span> isValid = Value.Check(EnvSchema, envObject);
  <span class="hljs-keyword">if</span> (!isValid) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid environment variables detected.'</span>);
  }
  <span class="hljs-keyword">return</span> envObject;
}
</code></pre>
<h3 id="heading-serverless-framework">Serverless Framework 문법으로 정의된 환경변수 검증</h3>
<p>Serverless Framework에서는 <code>serverless.yml</code> 안에 <code>environment</code> 섹션을 정의해 함수별(또는 공통)로 환경변수를 주입할 수 있습니다. 예를 들어 다음과 같이 <strong>스테이지(stage)에 따라</strong> 테이블 이름을 동적으로 설정할 수도 있습니다:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">service:</span> <span class="hljs-string">my-service</span>
<span class="hljs-attr">provider:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">aws</span>
  <span class="hljs-attr">runtime:</span> <span class="hljs-string">nodejs14.x</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">${opt:stage,</span> <span class="hljs-string">'dev'</span><span class="hljs-string">}</span>
  <span class="hljs-attr">environment:</span>
    <span class="hljs-attr">DYNAMODB_TABLE_NAME:</span> <span class="hljs-string">dynamodb-table-${opt:stage}</span>
    <span class="hljs-attr">API_KEY:</span> <span class="hljs-string">${env:API_KEY}</span>

<span class="hljs-attr">functions:</span>
  <span class="hljs-attr">hello:</span>
    <span class="hljs-attr">handler:</span> <span class="hljs-string">handler.hello</span>
</code></pre>
<h4 id="heading-7ja065akiousuoygnoqwgcdsnojsnytquyw">어떤 문제가 있을까?</h4>
<ul>
<li><p><strong>동적 레퍼런스 해석 문제</strong><br />  <code>dynamodb-table-${opt:stage}</code>와 같은 문법은 <strong>Serverless Framework</strong>가 실제 배포 시점에 <code>${opt:stage}</code>를 해석해 최종 문자열로 치환합니다. 하지만 <code>.env</code> 파일이나 등에서 불러온 환경변수만 조회할 경우 실제 값을 확인할 수 없거나 누락될 수 있습니다.</p>
</li>
<li><p><strong>CI/CD 상의 검증 누락</strong><br />  배포 전에 환경변수를 검증한다고 해도, 만약 CI 파이프라인이 동적으로 생성된 환경변수를 알 수 없다면 환경변수를 제대로 검증할 수 없습니다.</p>
</li>
</ul>
<h4 id="heading-7ja065a76rkmio2vtoqyso2wioucmd8">어떻게 해결했나?</h4>
<p>이 문제를 해결하기 위해, <strong>Serverless Framework</strong>가 제공하는 <code>serverless print</code> 명령어를 활용합니다. 이 명령어는 <code>${opt:stage}</code> 등의 문법을 실제로 해석해, 최종적으로 어떤 환경변수가 정의되는지 YAML로 출력해줍니다.</p>
<p>예를 들어, 다음과 같이 <code>serverless print</code> 결과물로 <code>serverless-printed.yml</code> 파일을 생성하면</p>
<pre><code class="lang-shell">serverless print --stage $STAGE &gt; serverless-printed.yml
</code></pre>
<p>최종 해석된 문자열을 다음과 같이 얻을 수 있습니다.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">provider:</span>
    <span class="hljs-attr">encironment:</span>
        <span class="hljs-attr">DYNAMODB_TABLE_NAME:</span> <span class="hljs-string">dynamodb-table-dev</span>
        <span class="hljs-attr">API_KEY:</span> <span class="hljs-string">random-string</span>
</code></pre>
<p>이를 이용하면 다음과 같은 식으로 스크립트 파일을 만들어 <strong>Bitbucket Pipeline</strong>에서 사용 가능합니다:</p>
<pre><code class="lang-ts"><span class="hljs-comment">// scripts/validate-serverless-env.ts</span>
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'fs'</span>;
<span class="hljs-keyword">import</span> yaml <span class="hljs-keyword">from</span> <span class="hljs-string">'js-yaml'</span>;
<span class="hljs-keyword">import</span> { validateEnvValues } <span class="hljs-keyword">from</span> <span class="hljs-string">'../env-schema'</span>; <span class="hljs-comment">// typebox 스키마 함수</span>

<span class="hljs-comment">// serverless-printed.yml 파일을 읽고, YAML을 파싱한 뒤 검증을 수행하는 스크립트 예시</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span> </span>{
  <span class="hljs-keyword">const</span> printedYaml = fs.readFileSync(<span class="hljs-string">'serverless-printed.yml'</span>, <span class="hljs-string">'utf8'</span>);
  <span class="hljs-keyword">const</span> printed = yaml.load(printedYaml) <span class="hljs-keyword">as</span> Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;

  <span class="hljs-comment">// 최종 해석된 환경변수 객체 추출</span>
  <span class="hljs-keyword">const</span> environmentVars = printed?.service?.provider?.environment ?? {};

  <span class="hljs-comment">// 스키마 검증</span>
  validateEnvValues(environmentVars);

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Environment variables validated successfully.'</span>);
}

<span class="hljs-comment">// 스크립트를 실행하는 메인 함수 호출</span>
main();
</code></pre>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">step:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Validate</span> <span class="hljs-string">environment</span> <span class="hljs-string">variables</span>
    <span class="hljs-attr">script:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">serverless</span> <span class="hljs-string">print</span> <span class="hljs-string">--stage</span> <span class="hljs-string">$STAGE</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">serverless-printed.yml</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">node</span> <span class="hljs-string">scripts/validate-serverless-env.js</span>
</code></pre>
<h2 id="heading-6rkw66gg">결론</h2>
<p>이번 장애를 계기로, “배포 시점에 환경변수가 올바르게 설정되어 있는지 자동으로 점검한다”는 작업이 생각보다 중요하다는 사실을 다시금 깨달았습니다. 특히 Serverless Framework에서는 스테이지(stage)에 따라 환경변수를 동적으로 정의할 수 있는데, 이때 CI/CD 단계에서 <strong>최종 해석된 값</strong>을 검증하지 않으면, 배포 후 예상치 못한 문제가 발생할 수 있습니다.</p>
<p>하지만 이렇게 자동화된 검증 프로세스를 한 번 구축해두고 나니 사람이 검수하는 과정에서 놓치는 오류를 <strong>자동화된 단계</strong>에서 걸러내므로, 꼼꼼히 검토하기 어려운 상황에서 <strong>실수로 인한 장애</strong>를 크게 줄일 수 있습니다. 또한, Fastify와 연계해 이미 TypeBox를 사용 중이라면, 별다른 도구 학습 없이 <strong>환경변수 검증 로직</strong>도 손쉽게 만들 수 있다는 게 큰 장점이었습니다.</p>
]]></content:encoded></item><item><title><![CDATA[Cloudflare Tunnel로 포트포워딩 없이 홈서버 운영하기]]></title><description><![CDATA[이 글에서 다루는 내용

포트포워딩이 안 되는 이유 (CGNAT 환경 이해)
CGNAT 우회 방법들의 장단점 비교
Cloudflare Tunnel 설정 방법 (MacOS 기준)


외부에서 내 PC로 접근할 수 있도록 허용하는 방법을 생각하면 포트포워딩이 가장 먼저 떠오릅니다. 공유기에서 특정 포트를 열어 외부에서 서버에 접속할 수 있도록 설정하는 방식으로, 마인크래프트 멀티를 해보셨던 분이라면 분명 해보셨을 방법입니다. 😊
작년에 저는 홈서...]]></description><link>https://blog.aqudi.me/cloudflare-tunnel-home-server-without-port-forwarding</link><guid isPermaLink="true">https://blog.aqudi.me/cloudflare-tunnel-home-server-without-port-forwarding</guid><category><![CDATA[cloudflare]]></category><category><![CDATA[homeserver]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 02 Mar 2025 13:13:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740921145938/a6c4e0ff-17fc-4042-aa27-0160ad2b73d8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>이 글에서 다루는 내용</strong></p>
<ul>
<li>포트포워딩이 안 되는 이유 (CGNAT 환경 이해)</li>
<li>CGNAT 우회 방법들의 장단점 비교</li>
<li>Cloudflare Tunnel 설정 방법 (MacOS 기준)</li>
</ul>
</blockquote>
<p>외부에서 내 PC로 접근할 수 있도록 허용하는 방법을 생각하면 <strong>포트포워딩</strong>이 가장 먼저 떠오릅니다. 공유기에서 특정 포트를 열어 외부에서 서버에 접속할 수 있도록 설정하는 방식으로, 마인크래프트 멀티를 해보셨던 분이라면 분명 해보셨을 방법입니다. 😊</p>
<p>작년에 저는 홈서버에 제가 쓰고 싶은 서비스를 띄워두고 쓰려고 서버를 배포했었습니다. 그런데 포트포워딩을 하고 접속 테스트를 하려고 공유기 설정에 들어가보니 외부 IP가 <code>192.168.x.x</code> 형태로 되어 있고 외부에서 접속이 안 됐습니다. 굉장히 난감했죠...</p>
<p>이번 글에서는 제가 겪었던 CGNAT이라는 환경에 대한 설명을 포함한 포트포워딩의 주요 문제점과 여러 해결법 중 제가 선택한 Cloudflare Tunnel 사용법까지 정리해보겠습니다.</p>
<h2 id="heading-7ys7yq47ys7jum65sp7j2yioyjvoyalcdrrljsojzsoja">포트포워딩의 주요 문제점</h2>
<p>포트포워딩이 항상 동작하는 것은 아닙니다. 제가 겪었던 것처럼 안 되는 경우가 있는데, 주로 다음 세 가지 이유 때문입니다.</p>
<h3 id="heading-1-nat">1. 공유기의 NAT 설정 문제</h3>
<p>공유기는 사설 IP 주소를 공인 IP 주소로 변환하여 여러 기기가 인터넷에 접속할 수 있도록 합니다. 이 과정에서 올바른 포트포워딩 설정이 이루어지지 않거나 공유기의 제한이 있을 경우, 외부에서 서버에 접근이 어려울 수 있습니다.</p>
<p>이 경우는 설정을 다시 확인하면 대부분 해결됩니다.</p>
<h3 id="heading-2-isp-ip-cgnat">2. ISP의 공인 IP 제한 (CGNAT 문제)</h3>
<p><strong>CGNAT가 뭔가요?</strong></p>
<p>CGNAT(Carrier-Grade NAT)는 <strong>ISP가 여러 사용자에게 하나의 공인 IP를 공유하게 만드는 기술</strong>입니다. IPv4 주소가 부족해지면서 ISP들이 비용 절감을 위해 도입했습니다.</p>
<p><strong>왜 포트포워딩이 안 되나요?</strong></p>
<p>CGNAT 환경에서는 이중 NAT 구조가 됩니다:</p>
<pre><code class="lang-text">외부 → ISP의 CGNAT → 공유기 → 홈서버
</code></pre>
<p>포트포워딩을 설정해도 ISP 레벨에서 추가적인 NAT 계층이 존재하기 때문에 외부에서 서버에 직접 접근할 수 없습니다. 외부에서 들어오는 연결이 수십~수백 명의 사용자 중 누구에게 보내야 할지 ISP가 알 수 없기 때문이죠.</p>
<p><strong>내가 CGNAT 환경인지 확인하는 방법</strong></p>
<ol>
<li>공유기 관리 페이지에서 WAN IP를 확인합니다.</li>
<li>"내 IP 확인" 사이트(예: whatismyip.com)에서 공인 IP를 확인합니다.</li>
<li>두 IP가 다르면 CGNAT 환경입니다.</li>
<li>특히 WAN IP가 <code>100.64.x.x</code> ~ <code>100.127.x.x</code> 형태면 거의 확정입니다.</li>
</ol>
<p>제 경우, 자취방의 인터넷이 CGNAT 환경이었기 때문에 공유기의 WAN IP가 <code>192.168.x.x</code> 형식의 사설 IP였고, 포트포워딩을 설정해도 외부에서 접근할 수 없었습니다.</p>
<h3 id="heading-3">3. 보안 이슈</h3>
<p>포트포워딩이 되더라도 실제 IP를 노출해야 하므로 해킹 시도나 DDoS 공격에 노출될 위험이 높아집니다. 포트가 열려있다는 것 자체가 공격 표면이 되기 때문입니다.</p>
<hr />
<p>이러한 문제들을 해결하기 위해 <strong>Cloudflare Tunnel</strong>을 활용하면 <strong>포트포워딩 없이도 안전하게 홈서버를 운영할 수 있습니다.</strong></p>
<h2 id="heading-cloudflare-tunnel">Cloudflare Tunnel이란?</h2>
<p>Cloudflare Tunnel(구 Argo Tunnel)은 <strong>서버의 IP를 공개하지 않고도 외부에서 접속할 수 있게 해주는 서비스</strong>입니다. 기존 포트포워딩 방식과 가장 큰 차이점은 <strong>공유기에서 포트를 열 필요가 없다는 점</strong>입니다.</p>
<h3 id="heading-6riw7kg0io2pro2kuo2proybjouuqsdrsknsi50">기존 포트포워딩 방식</h3>
<pre><code class="lang-text">외부 클라이언트 → 공유기 포트포워딩 → 홈서버
</code></pre>
<ul>
<li>✅ 서버가 직접 외부에서 접근 가능  </li>
<li>❌ 포트가 노출되므로 해킹 위험 존재</li>
<li>❌ CGNAT 환경에서는 사용 불가</li>
</ul>
<h3 id="heading-cloudflare-tunnel-1">Cloudflare Tunnel 방식</h3>
<pre><code class="lang-text">홈서버 → Cloudflare Tunnel → Cloudflare → 외부 클라이언트
</code></pre>
<ul>
<li>✅ 서버의 IP가 노출되지 않음  </li>
<li>✅ 포트포워딩 필요 없음 → CGNAT 문제 해결  </li>
<li>✅ Cloudflare의 보안 기능 활용 가능</li>
</ul>
<p>핵심은 <strong>연결 방향이 반대</strong>라는 점입니다. 포트포워딩은 외부에서 들어오는 연결을 받아야 하지만, Cloudflare Tunnel은 홈서버가 먼저 Cloudflare로 연결을 만들고, Cloudflare가 외부 요청을 중계해줍니다. 그래서 포트를 열 필요가 없고, CGNAT 환경에서도 동작합니다.</p>
<p>게다가 Cloudflare는 대부분의 경우 무료로 사용할 수 있으며, TLS 인증서 자동 발급, DDoS 방어, WAF(Web Application Firewall)까지 기본 제공됩니다.</p>
<p>이런 이유로 홈서버를 운영할 때 포트포워딩 대신 Cloudflare Tunnel을 사용하기로 했습니다.</p>
<h2 id="heading-cgnat">CGNAT 우회 방법 비교</h2>
<p>Cloudflare Tunnel 외에도 CGNAT를 우회하는 여러 방법이 있습니다:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>방법</td><td>장점</td><td>단점</td><td>월 비용</td></tr>
</thead>
<tbody>
<tr>
<td><strong>ISP에 공인 IP 요청</strong></td><td>근본적 해결</td><td>거부당할 수 있음</td><td>무료~추가 비용</td></tr>
<tr>
<td><strong>VPS + VPN (WireGuard)</strong></td><td>완전한 제어</td><td>설정 복잡, 관리 부담</td><td>$2~10</td></tr>
<tr>
<td><strong>Ngrok</strong></td><td>빠른 설정</td><td>무료 플랜 제한 많음</td><td>$8~25</td></tr>
<tr>
<td><strong>Tailscale</strong></td><td>P2P 메쉬 네트워크</td><td>공개 웹 서비스엔 부적합</td><td>무료 (개인용)</td></tr>
<tr>
<td><strong>Cloudflare Tunnel</strong></td><td>무료, 보안 강화, 간단</td><td>속도 저하 가능성</td><td>무료</td></tr>
</tbody>
</table>
</div><p><strong>Cloudflare Tunnel을 선택한 이유:</strong></p>
<ul>
<li><strong>완전 무료</strong>: 다른 솔루션들은 유료이거나 기능 제한이 있음</li>
<li><strong>보안 자동화</strong>: TLS 인증서 자동 발급, DDoS 방어, WAF 기본 제공</li>
<li><strong>설정 간단</strong>: VPS + VPN 조합은 설정과 유지보수가 복잡함</li>
<li><strong>글로벌 CDN</strong>: Cloudflare 네트워크를 통해 전 세계 어디서든 빠른 접속</li>
</ul>
<p>물론 Cloudflare에 의존하게 된다는 단점이 있지만, 개인 홈서버 용도로는 충분히 트레이드오프할 만하다고 생각했습니다. </p>
<h2 id="heading-cloudflare-tunnel-macos">Cloudflare Tunnel 설정 방법 (MacOS 기준)</h2>
<h3 id="heading-7iks7kceioykgou5ha">사전 준비</h3>
<p>Cloudflare Tunnel을 설정하기 전에, <strong>도메인이 Cloudflare에 등록되어 있어야 합니다</strong>. Cloudflare가 도메인의 DNS를 관리해야 터널과 연결할 수 있기 때문입니다.</p>
<p><strong>1. Cloudflare에 도메인 등록</strong></p>
<ul>
<li><strong>Cloudflare에서 도메인 구매</strong>: Cloudflare를 통해 직접 도메인을 구매할 수 있습니다.</li>
<li><strong>외부에서 구매한 도메인 등록</strong>: 이미 다른 등록기관에서 도메인을 구매했다면, 해당 도메인을 Cloudflare에 추가하고 네임서버를 Cloudflare에서 제공하는 것으로 변경하면 됩니다.</li>
</ul>
<p><strong>2. 네임서버 변경 절차</strong></p>
<ul>
<li>도메인 등록기관(예: 가비아)의 관리 페이지에 로그인합니다.</li>
<li>Cloudflare에서 제공하는 네임서버 정보로 기존 네임서버를 변경합니다.</li>
<li>네임서버 변경이 완료되면, 변경 사항이 제대로 적용되었는지 확인합니다. (최대 24시간 소요)</li>
</ul>
<p><strong>참고 자료:</strong></p>
<ul>
<li><a target="_blank" href="https://customer.gabia.com/manual/domain/286/991">가비아 네임서버 변경 가이드</a></li>
<li><a target="_blank" href="https://blog.uluru.io/cloudflare/1/">Cloudflare에 도메인 추가하기</a></li>
</ul>
<h3 id="heading-1-cloudflare-tunnel">1. Cloudflare Tunnel 생성</h3>
<blockquote>
<p>터널을 생성하면 Cloudflare에서 고유한 터널 ID를 부여받습니다. 이 ID를 통해 홈서버와 Cloudflare가 연결됩니다.</p>
</blockquote>
<ul>
<li><a target="_blank" href="https://one.dash.cloudflare.com/">one.dash.cloudflare.com</a>으로 이동하여 <strong>네트워크 &gt; Tunnels</strong> 메뉴에 접속합니다.</li>
<li>터널 유형에서 <code>Cloudflared</code>를 선택합니다.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740919611637/fac2c74f-ccc4-4509-93e2-32aa116321ce.png" alt="Cloudflare Tunnel 생성 1" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740919625128/3d0d8aad-6be9-4704-901e-47248078d52f.png" alt="Cloudflare Tunnel 생성 2" /></p>
<h3 id="heading-2-cloudflare-tunnel">2. Cloudflare Tunnel 커넥터 설치 및 실행</h3>
<blockquote>
<p>커넥터(cloudflared)는 홈서버에서 실행되며, Cloudflare로 아웃바운드 연결을 유지합니다. 이 연결을 통해 외부 트래픽이 홈서버로 전달됩니다.</p>
</blockquote>
<ul>
<li>터널 구성에서 자신의 환경(MacOS/Linux/Windows/Docker)을 선택합니다.</li>
<li>표시된 명령어를 복사하여 터미널에서 실행합니다.</li>
</ul>
<pre><code class="lang-shell">brew install cloudflared
sudo cloudflared service install &lt;token&gt;
</code></pre>
<ul>
<li>설치가 완료되면 <code>Connectors</code> 섹션에 연결된 커넥터가 표시됩니다.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740919649121/bd5e03fc-f350-480e-bbaf-0171dac3763b.png" alt="Cloudflare Tunnel 커넥터 연결 확인" /></p>
<h3 id="heading-3-1">3. 공개 호스트 설정</h3>
<blockquote>
<p>어떤 도메인으로 들어온 요청을 홈서버의 어느 포트로 전달할지 설정합니다.</p>
</blockquote>
<ul>
<li>홈서버의 80번 포트(리버스 프록시)에 Cloudflare Tunnel을 연결하고 싶다면 하위 도메인을 와일드카드(*)로 선언합니다.</li>
<li><code>*.example.com</code> 형태로 설정하면 <code>test.example.com</code>, <code>home.example.com</code> 등 모든 서브도메인 요청을 받을 수 있습니다.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740919661157/1846cdd0-4c31-4b5f-bc36-51e34bf8f8b6.png" alt="공개 호스트 설정" /></p>
<h3 id="heading-4-dns-wildcard">4. DNS에 Wildcard 도메인 연결하기</h3>
<blockquote>
<p>DNS 레코드를 추가해야 Cloudflare가 해당 도메인으로 들어오는 요청을 터널로 라우팅할 수 있습니다.</p>
</blockquote>
<p><strong>터널 ID 복사:</strong></p>
<ul>
<li>터널 리스트에서 방금 생성한 터널의 ID를 복사합니다.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740919680338/a57436a1-2807-4f5a-a236-7394677ddbd0.png" alt="터널 ID 복사" /></p>
<p><strong>DNS 레코드 추가:</strong></p>
<ul>
<li><a target="_blank" href="https://dash.cloudflare.com">dash.cloudflare.com</a>에 접속하여 DNS 레코드를 추가합니다.<ul>
<li><strong>유형</strong>: CNAME</li>
<li><strong>이름</strong>: * (와일드카드)</li>
<li><strong>대상</strong>: <code>&lt;터널 UUID&gt;.cfargotunnel.com</code></li>
<li><strong>프록시 상태</strong>: 프록싱됨 (주황색 구름 아이콘)</li>
</ul>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1740919771666/895bebac-1bee-40ef-a906-ef975e66459d.png" alt="DNS 레코드 추가" /></p>
<h2 id="heading-multi-level-subdomain-ssl">주의사항: Multi-level Subdomain SSL 제한</h2>
<p>Cloudflare Tunnel 무료 플랜에서는 1단계 서브도메인(<code>sub.example.com</code>)에 대해서만 SSL 인증서가 제공됩니다. 2단계 서브도메인(<code>sub.sub.example.com</code>)은 SSL이 적용되지 않습니다.</p>
<p><strong>왜 그런가요?</strong></p>
<p>Cloudflare의 무료 SSL 인증서가 와일드카드(<code>*.example.com</code>)까지만 적용되기 때문입니다. <code>*.*.example.com</code>은 지원되지 않습니다.</p>
<p><strong>해결 방법:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>방법</td><td>설명</td><td>비용</td></tr>
</thead>
<tbody>
<tr>
<td><strong>1단계 서브도메인만 사용</strong></td><td><code>app.example.com</code> 형태로 구성 (권장)</td><td>무료</td></tr>
<tr>
<td><strong>Advanced Certificate Manager</strong></td><td>Cloudflare 유료 기능으로 2단계 지원</td><td>월 $10</td></tr>
<tr>
<td><strong>자체 인증서 업로드</strong></td><td>Let's Encrypt 등으로 발급받아 업로드</td><td>무료 (수동 관리)</td></tr>
</tbody>
</table>
</div><p>대부분의 경우 1단계 서브도메인으로 충분하므로, 굳이 복잡하게 구성할 필요는 없습니다.</p>
<p><strong>참고:</strong> <a target="_blank" href="https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/">Cloudflare Advanced Certificate Manager</a></p>
<h2 id="heading-cloudflare-tunnel-2">결론: Cloudflare Tunnel이 정답일까?</h2>
<p>Cloudflare Tunnel은 공인 IP가 없거나 사용에 어려움이 있는 사용자나 보안을 강화하고 싶은 경우에 손쉽게 쓸 수 있는 솔루션입니다. 하지만 몇 가지 고려할 점이 있습니다.</p>
<h3 id="heading-7jwm7jwe65gyioygka">알아둘 점</h3>
<p><strong>장점:</strong></p>
<ul>
<li>무료로 대부분의 기능 사용 가능</li>
<li>TLS 인증서, DDoS 방어, WAF 기본 제공</li>
<li>설정이 간단하고 관리 부담이 적음</li>
<li>포트 개방 불필요로 보안 강화</li>
</ul>
<p><strong>제약사항:</strong></p>
<ul>
<li>Multi-level subdomain (<code>sub.sub.example.com</code>)은 유료 플랜 필요</li>
<li>일부 사용자는 직접 프록시보다 속도 저하 경험</li>
<li>Cloudflare에 대한 의존성</li>
</ul>
<h3 id="heading-7iks7jqp7j6qioycoo2yleuzhcdstptsspw">사용자 유형별 추천</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>사용자 유형</td><td>추천 솔루션</td></tr>
</thead>
<tbody>
<tr>
<td>공인 IP가 없고 보안을 강화하고 싶은 경우</td><td>✅ Cloudflare Tunnel</td></tr>
<tr>
<td>공인 IP가 있으며 직접 방화벽을 관리할 수 있는 경우</td><td>🔄 포트포워딩 가능</td></tr>
<tr>
<td>완전한 제어가 필요한 경우</td><td>VPS + VPN</td></tr>
<tr>
<td>개발 테스트 목적</td><td>Ngrok</td></tr>
</tbody>
</table>
</div><p>저는 현재 n8n, karakeep, vaultwarden 등을 Cloudflare Tunnel로 운영하고 있는데, 설정 한 번으로 포트포워딩 고민 없이 안정적으로 사용하고 있습니다. 홈서버를 운영하면서 관리 리소스를 줄이고 안전하게 운영하고 싶다면 Cloudflare Tunnel을 적극 추천합니다.</p>
<p>물론 공부 목적이라면 포트포워딩을 쓰면서 잠재적인 보안 위협 요소들을 하나씩 차단해보는 경험도 좋을 것 같습니다. Cloudflare Tunnel이 안 좋은 상황이나 쓸 수 없는 상황이 더 있다면 댓글로 공유해주시면 좋겠습니다.</p>
]]></content:encoded></item><item><title><![CDATA[[후기] K-Devcon 2월엔 DevBloom]]></title><description><![CDATA[🎊 행사 소개 & 전반적인 분위기

이번 K-Devcon 2월 Dev Bloom 행사는 광화문 마이크로소프트 사옥 13층에서 열렸습니다. 시위가 있어서 교통이 혼잡했는데, 다행히 늦지 않게 도착할 수 있었어요. 현장에는 약 150~200명의 개발자와 디자이너분들이 모였지만, 운영진이 공간을 잘 구성해주셔서 쾌적하게 강연들을 들을 수 있었습니다

.
세션은 각각 40분씩 진행되었는데, 시간이 다소 짧아 질문을 충분히 못 하는 아쉬움이 있었어요....]]></description><link>https://blog.aqudi.me/k-devcon-2025-02-devbloom-review</link><guid isPermaLink="true">https://blog.aqudi.me/k-devcon-2025-02-devbloom-review</guid><category><![CDATA[밋업후기]]></category><category><![CDATA[컨퍼런스 후기]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 16 Feb 2025 10:43:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1739702535025/01df547f-747e-4781-bd1e-2b3297f1cbaa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-amp">🎊 행사 소개 &amp; 전반적인 분위기</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739694138719/21c45d28-2155-493f-bd90-bb54f7a137fb.png" alt class="image--center mx-auto" /></p>
<p>이번 <strong>K-Devcon 2월 Dev Bloom</strong> 행사는 광화문 마이크로소프트 사옥 13층에서 열렸습니다. 시위가 있어서 교통이 혼잡했는데, 다행히 늦지 않게 도착할 수 있었어요. 현장에는 약 150~200명의 개발자와 디자이너분들이 모였지만, 운영진이 공간을 잘 구성해주셔서 쾌적하게 강연들을 들을 수 있었습니다</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739701854877/caaa8bec-a2ef-4b34-80c0-bb78ce3f9a0f.jpeg" alt class="image--center mx-auto" /></p>
<p>.</p>
<p>세션은 각각 40분씩 진행되었는데, 시간이 다소 짧아 질문을 충분히 못 하는 아쉬움이 있었어요. 그래도 쉬는 시간마다 발표자분들께 직접 질문을 하고 사진도 찍을 수 있어 뜻깊었습니다. 행사 마지막에는 <strong>이지스 퍼블리싱</strong>에서 협찬한 도서 추첨이 있었는데, 운 좋게 당첨돼서 "알고리즘 코딩 테스트" 책을 받았습니다!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739701862024/8b741aff-d17f-4f40-9e2c-e4956fef76f1.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-6ro17iudio2wieycrcdsojxrs7q">공식 행사 정보</h3>
<ul>
<li><p><strong>K-DEVCON 홈페이지:</strong> <a target="_blank" href="https://k-devcon.com/">https://k-devcon.com/</a></p>
</li>
<li><p><strong>2월 DevBloom 행사 안내 페이지:</strong> <a target="_blank" href="https://k-devcon.com/entry/Event-2025-02-15-K-DEVCON-%EC%84%9C%EC%9A%B8-2%EC%9B%94%EC%97%94">https://k-devcon.com/entry/Event-2025-02-15-K-DEVCON-%EC%84%9C%EC%9A%B8-2%EC%9B%94%EC%97%94</a></p>
</li>
</ul>
<h2 id="heading-amp-1">📝 세션별 내용 &amp; 인상 깊었던점</h2>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">발표 자료의 경우에는 공개해도 되는지 확인이 필요해서, 대신 연사자 성함을 적어두도록 하겠습니다! 추후에 확인되는 대로 발표 자료 링크도 함께 올려두겠습니다.</div>
</div>

<h3 id="heading-1-rust-concurrency-42dot">(1) Rust Concurrency – 옥찬호 (42dot)</h3>
<p><strong>기대했던 바</strong></p>
<ul>
<li><p>Rust가 C++이나 Node.js와 달리 동시성을 어떻게 안전하게 처리하는지 궁금했다.</p>
</li>
<li><p>Rust의 소유권 시스템과 멀티스레드 환경에서의 데이터 보호 방식이 실제로 어떻게 작동하는지 알고 싶었다.</p>
</li>
</ul>
<p><strong>얻은 인사이트</strong></p>
<ul>
<li><p><strong>뮤텍스(Mutex)와 데이터가 하나로 묶여 있는 구조</strong>: C++에서는 데이터와 락이 분리되어 있어 주석이나 문서로 관리해야 하는데, Rust에서는 **"Mutex 자체가 데이터를 포함"**하기 때문에 안전성이 보장된다는 점이 인상적이었다.</p>
</li>
<li><p><strong>추가로 궁금해진 점: Node.js의 worker_threads와 Rust의 멀티스레드 모델의 차이점.</strong> 싱글스레드 기반인 Node.js의 worker_threads와 Rust의 멀티스레드 사이의 어떤 차이가 있는지 비교해볼 계획이다.</p>
</li>
</ul>
<p><strong>다음 액션</strong></p>
<ul>
<li><p>Rust에서 배운 <code>Arc&lt;Mutex&lt;T&gt;&gt;</code>, <code>channel</code> 등을 활용해 간단한 동시성 예제를 만들어볼 예정이다.</p>
</li>
<li><p>Rust와 Node.js의 동시성 모델을 비교하는 블로그 포스팅을 작성할 계획이다.</p>
</li>
</ul>
<hr />
<h3 id="heading-2">(2) 구해줘, 내 월세 – 은종민 (제니스 신당역 공인중개사무소)</h3>
<p><strong>기대했던 바</strong></p>
<ul>
<li><p>최근 전세 계약을 마무리하면서, 다음 월세 계약을 준비 중이라 실질적인 부동산 팁이 필요했다.</p>
</li>
<li><p>전세 사기가 사회적 이슈가 되는 만큼, 어떤 점을 체크해야 하는지 정리된 정보를 듣고 싶었다.</p>
</li>
</ul>
<p><strong>얻은 인사이트</strong></p>
<ul>
<li><p><strong>등기부 등본을 직접 확인해야 한다</strong>: 중개사가 뽑아주는 요약본만 믿지 말고, <strong>직접 등기부를 떼서 확인</strong>하는 습관이 필요하다.</p>
<ul>
<li>최신 등기부 등본 요약 조회 방법: <a target="_blank" href="https://www.youtube.com/watch?v=aU9fyVLqIEU">https://www.youtube.com/watch?v=aU9fyVLqIEU</a></li>
</ul>
</li>
<li><p><strong>집 비밀번호 관리의 중요성</strong>: 집을 부동산에 내놓을 때 기존 비밀번호를 그대로 유지하면, 점유 문제가 발생할 수도 있다.</p>
</li>
<li><p><strong>월세 계약 시 초안 확인</strong>: 계약서 초안을 미리 받아 검토해야 한다. 계약 당일 급하게 사인하면 놓치는 부분이 많아질 수 있다. 실제로 전세 계약할 때 초안을 미리 못 받아서 당일 정신없이 사인했던 경험이 있어서 더 공감됐다.</p>
</li>
</ul>
<p><strong>다음 액션</strong></p>
<ul>
<li><p>계약 전 <strong>등기부 등본 직접 떼보기</strong> 및 <strong>특약 조항 꼼꼼히 확인</strong>.</p>
</li>
<li><p>집을 누군가에게 보여주고 비밀번호 변경하기, 보증금 받기 전에는 비밀번호 알려주지 않기</p>
</li>
</ul>
<hr />
<h3 id="heading-3">(3) 멀티 에이전트의 구현 및 활용 – 정유선 (신세계)</h3>
<p><strong>기대했던 바</strong></p>
<ul>
<li><p>AI 모델이 단순한 Q&amp;A 수준을 넘어 <strong>도구(툴)와 API를 활용해 문제를 해결하는 방식</strong>이 궁금했다.</p>
</li>
<li><p>최근 AI 기술이 에이전트 기반으로 발전하는 만큼, 현업에서는 어떤 방식으로 적용하고 있는지 알고 싶었다.</p>
</li>
</ul>
<p><strong>얻은 인사이트</strong></p>
<ul>
<li><p><strong>Generative AI의 역할 변화</strong>: 단순한 챗봇이 아니라 <strong>툴을 활용해 직접 문제를 해결하는 에이전트가 대세</strong>라는 점이 인상적이었다.</p>
</li>
<li><p><strong>LLM 워크플로우</strong>(프롬프트 체이닝, 병렬화, 오케스트레이션 등) 여러 개의 에이전트를 협력시키거나 특정 작업을 효율적으로 분할 수행하는 기술에 대해서 알게 됐다. <a target="_blank" href="https://www.anthropic.com/research/building-effective-agents">LLM 워크플로우 참고자료</a></p>
</li>
</ul>
<p><strong>다음 액션</strong></p>
<ul>
<li>LangGraph를 활용해 간단한 <strong>AI 비서진</strong>을 만들어볼 계획이다. <strong>"개발 도우미", "자료조사 도우미"</strong> 등 역할을 나누어 실험해보면, 개인 생산성을 높이는 데에도 도움이 될 것 같다.</li>
</ul>
<hr />
<h3 id="heading-4-9-48">(4) 9명의 제품팀이 48명이 되기까지 – 이동욱 (인프랩)</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739701879765/edd62f18-d1f0-4f6a-a548-f2b899a086d2.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>기대했던 바</strong></p>
<ul>
<li><p>스타트업이 작은 팀에서 규모를 키우는 과정에서 조직 운영 방식이 어떤 영향을 미치는지 알고 싶었다.</p>
</li>
<li><p>현재 우리 회사 제품팀 규모도 40~50명 정도 된다고 알고 있어서 도움될만한 정보가 있을 것 같았다.</p>
</li>
</ul>
<p><strong>얻은 인사이트</strong></p>
<ul>
<li><p><strong>기능 조직 → 목적 조직 → 특수 목적 조직으로 성장</strong>: 초기에는 직무별 기능 조직이 필요하지만, <strong>성장이 빨라질수록 목적 조직이 효율적</strong>이라는 점이 공감됐다.</p>
</li>
<li><p>**"병목이 발생하면 다른 조직이 도와야 한다"**는 용병 문화가 인상적이었다. 특정 팀이 과부하를 겪을 때, 비효율을 감수하고라도 조직 전체 속도를 맞추는 것이 중요하다.</p>
</li>
<li><p><strong>특수 목적 조직의 필요성</strong>: 디자인 시스템, 내부 도구 개발 등 <strong>매일매일 개선이 필요하지 않은 영역을 관리하는 조직</strong>이 있으면 장기적인 생산성이 올라간다.</p>
</li>
</ul>
<p><strong>다음 액션</strong></p>
<ul>
<li>사내 디자인 시스템과 코어 서비스 개발을 <strong>특수 목적 조직 형태로 운영하는 방안을 리드들에게 건의해봐야겠다.</strong></li>
</ul>
<hr />
<h3 id="heading-5-stack-overflow-25000-line">(5) Stack Overflow 25,000점 달성기 – 김영재 (LINE)</h3>
<p><strong>기대했던 바</strong></p>
<ul>
<li><p>평소 Stack Overflow에서 검색은 많이 하지만, 질문이나 답변을 직접 남긴 적이 없어서 어떻게 기여할 수 있을지 알고 싶었다.</p>
</li>
<li><p>커뮤니티에서 적극적으로 활동하면 어떤 이점이 있는지도 궁금했다.</p>
</li>
</ul>
<p><strong>얻은 인사이트</strong></p>
<ul>
<li><p>커뮤니티 활동이 <strong>좋은 질문과 답변을 하기 위한 훈련</strong>이며, 이는 곧 AI 시대의 좋은 개발자가 되기 위한 방법이다.</p>
</li>
<li><p>ChatGPT 등장 이후 Stack Overflow의 질문 수가 5분의 1로 감소했다. AI가 모든 해답을 제공할 것처럼 보이지만, 실제 문제 해결 과정에서 얻는 시행착오의 가치가 오히려 높아지고 있다.</p>
</li>
<li><p>지식 노동자로서 <strong>자신의 경험을 시작과 끝이 있는 완결된 지식으로 가공</strong>하는 능력이 필수적이다.</p>
</li>
<li><p>좋지 못한 질문 사례들을 보며 나의 질문 방식을 돌아보게 되었다.</p>
</li>
</ul>
<p><strong>다음 액션</strong></p>
<ul>
<li>상반기 동안 <strong>200점 이상 획득하는 것을 목표</strong>로, 회사 프로젝트에서 겪은 오류나 삽질했던 내용을 중심으로 Stack Overflow에 질문/답변을 남겨봐야겠다.</li>
</ul>
<hr />
<h2 id="heading-4pyfioydtouyicdtlonsgqwg7lsd7yj">✅ 이번 행사 총평</h2>
<p><strong>Rust 동시성</strong>부터 <strong>부동산 계약 팁</strong>, <strong>AI 멀티 에이전트</strong>, <strong>스타트업 조직 운영</strong>, <strong>스택오버플로우 커뮤니티 참여</strong>까지 폭넓은 주제를 한 자리에서 접할 수 있어서 뜻 깊었던 5시간이었습니다.</p>
<p>각 세션 시간이 한정적이라 질의응답 시간이 부족했던 게 좀 아쉬웠지만, 추가로 질문이 있을 때 정말 기꺼이 시간 내어 답변해 주셔서 너무 감사했습니다. 특히, 제 고민을 들어주고 인프랩의 사례를 공유해주신 <strong>동욱 님</strong>, 그리고 발표장 바깥에서 자리를 지키며 도움이 필요하신 분들에게 <strong>부동산 상담까지 해주신 은종민 님</strong> 덕분에 더욱 뜻깊은 시간이 되었습니다. 따뜻함을 한껏 충전할 수 있었던 행사였어요! ☺️</p>
<p><strong>다음 K-Devcon 행사에도 참석</strong>해서 앞으로도 이런 배움의 기회를 계속 늘려가고 싶습니다. 그래서 이번에 K-Devcon에서 운영하는 <strong>멘토링 프로그램 "고투런" 2기 멘티도 모집</strong>에도 지원해볼 예정입니다!</p>
<p>궁금하신 점이나 공유하고 싶은 이야기가 있다면 언제든 댓글 달아주세요! 감사합니다!</p>
]]></content:encoded></item><item><title><![CDATA[『풀스택 테스트』 리뷰: 10가지 테스트 전략 바이블]]></title><description><![CDATA[서론
최근 여러 프로젝트를 진행하면서 테스트가 단순히 선택이 아니라 필수 요소임을 다시금 깨달았습니다. 특히 회사에서 진행된 테스트 특강을 통해 테스트가 개발 프로세스 전반에 걸쳐 중요한 전략임을 재인식하게 되었고, 이 기회에 보다 체계적이고 심도 있는 테스트 방법론을 배우고자 “풀스택 테스트”를 읽게 되었습니다.
책 소개
”풀스택 테스트”는 10가지 테스트 기술을 중심으로 웹과 모바일 애플리케이션의 품질 확보 방법을 종합적으로 정리한 책입니다...]]></description><link>https://blog.aqudi.me/review-fullstack-test-book</link><guid isPermaLink="true">https://blog.aqudi.me/review-fullstack-test-book</guid><category><![CDATA[소프트웨어테스트]]></category><category><![CDATA[풀스택테스트]]></category><category><![CDATA[보안테스트]]></category><category><![CDATA[테스트전략]]></category><category><![CDATA[성능테스트]]></category><category><![CDATA[접근성테스트]]></category><category><![CDATA[개발자도서]]></category><category><![CDATA[후기]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 02 Feb 2025 14:39:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738507495423/dd10f6b4-6277-4512-8afd-4f0b99f99c20.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-7isc66gg">서론</h2>
<p>최근 여러 프로젝트를 진행하면서 테스트가 단순히 선택이 아니라 필수 요소임을 다시금 깨달았습니다. 특히 회사에서 진행된 테스트 특강을 통해 테스트가 개발 프로세스 전반에 걸쳐 중요한 전략임을 재인식하게 되었고, 이 기회에 보다 체계적이고 심도 있는 테스트 방법론을 배우고자 <strong>“풀스택 테스트”</strong>를 읽게 되었습니다.</p>
<h2 id="heading-7lgfioygjoqwna">책 소개</h2>
<p><strong>”풀스택 테스트”</strong>는 10가지 테스트 기술을 중심으로 웹과 모바일 애플리케이션의 품질 확보 방법을 종합적으로 정리한 책입니다. 총 432쪽 분량으로, 저자인 가야트리 모한의 풍부한 실무 경험과 노하우가 담겨 있으며, 최경현 님의 번역 덕분에 쉽게 읽을 수 있습니다.</p>
<p><a target="_blank" href="https://www.hanbit.co.kr/store/books/look.php?p_code=B2618676913">한빛미디어 책 소개 페이지 바로가기</a></p>
<p>책은 13개 장으로 구성되며, 첫 장에서는 풀스택 테스트의 기본 개념과 ‘시프트 레프트’ 전략을 소개합니다. 이후 수동 탐색적 테스트, 자동화 기능 테스트, 지속적 테스트, 데이터 테스트, 시각적 테스트, 보안 테스트, 성능 테스트, 접근성 테스트, 교차 기능 요구사항 테스트, 모바일 테스트까지 다양한 영역을 순차적으로 다룬 뒤, 마지막 장에서 AI, 머신러닝, 블록체인, IoT, AR/VR 등 신기술 테스트 전략을 소개합니다.</p>
<p>각 장은 <strong>구성 요소</strong> → <strong>전략</strong> → <strong>실습</strong> → <strong>추가 테스트 도구</strong> → <strong>인사이트</strong> 순으로 전개되며, 필요한 테스트 기술을 빠르게 찾아볼 수 있어 일종의 ‘백과사전’처럼 활용하기에 매우 유용합니다.</p>
<h2 id="heading-7ko87jquiouctoyaqsdsmptslb0">주요 내용 요약</h2>
<p>이 책은 다음의 10가지 핵심 테스트 기술을 중심으로 다양한 실무 사례와 도구 사용법을 제시합니다:</p>
<ol>
<li><p><strong>수동 탐색적 테스트</strong></p>
</li>
<li><p><strong>자동화된 기능 테스트</strong></p>
</li>
<li><p><strong>지속적 테스트</strong></p>
</li>
<li><p><strong>데이터 테스트</strong></p>
</li>
<li><p><strong>시각적 테스트</strong></p>
</li>
<li><p><strong>보안 테스트</strong></p>
</li>
<li><p><strong>성능 테스트</strong></p>
</li>
<li><p><strong>접근성 테스트</strong></p>
</li>
<li><p><strong>교차 기능 요구사항 테스트</strong></p>
</li>
<li><p><strong>모바일 테스트</strong></p>
</li>
</ol>
<p>이 중 개인적으로 가장 인상 깊었던 보안, 성능, 접근성 테스트에 대해 조금 더 자세히 살펴보겠습니다.</p>
<hr />
<h3 id="heading-7">보안 테스트 (7장)</h3>
<p>개발 일정이 빠듯하다 보면 <strong>보안</strong>은 후순위로 밀리기 쉽지만, 서비스 전체의 신뢰도에 치명적 영향을 줄 수 있기에 결코 가볍게 볼 수 없는 영역입니다. 이 책에서는 <strong>STRIDE 위협 모델</strong>을 통해 보안 위협을 **스푸핑(Spoofing), 변조(Tampering), 부인(Repudiation), 정보 노출(Information Disclosure), 서비스 거부(Denial of Service), 권한 상승(Elevation of Privilege)**의 여섯 가지 범주로 나누어 체계적으로 파악할 것을 제안합니다.</p>
<blockquote>
<p><strong>STRIDE 위협 모델</strong></p>
<ul>
<li><p>마이크로소프트가 제안한 보안 위협 분석 방법론으로, 애플리케이션 및 시스템을 설계 개발할 때 어떤 위험 요소가 있는지 체계적으로 분석할 수 있습니다.</p>
</li>
<li><p>더 자세한 내용은 <a target="_blank" href="https://en.wikipedia.org/wiki/STRIDE_model">Wikipedia STRIDE 위협 모델 문서</a>를 참고해주세요.</p>
</li>
</ul>
</blockquote>
<p>또한 책에서는 <strong>OWASP ZAP</strong>이나 <strong>Dependency-Check</strong> 같은 자동화 보안 도구를 적극적으로 활용해, 개발 과정에서 지속적으로 보안을 점검하길 권장합니다.</p>
<blockquote>
<p><strong>OWASP ZAP (Zed Attack Proxy)</strong></p>
<ul>
<li><p>오픈 소스 웹 애플리케이션 보안 스캐닝 도구로, 취약점 스캐닝을 자동화할 수 있습니다.</p>
</li>
<li><p><a target="_blank" href="https://www.zaproxy.org/">OWASP ZAP 공식 웹사이트</a>에서 설치 방법과 사용 예시를 확인할 수 있습니다.</p>
</li>
</ul>
<p><strong>Dependency-Check</strong></p>
<ul>
<li><p>애플리케이션이 사용하는 라이브러리(의존성) 중 보안 취약점이 존재하는지 분석해주는 오픈 소스 도구입니다.</p>
</li>
<li><p><a target="_blank" href="https://owasp.org/www-project-dependency-check/">OWASP Dependency-Check 공식 웹사이트</a>에서 최신 버전을 다운로드하고 사용법을 배울 수 있습니다.</p>
</li>
</ul>
</blockquote>
<p>이처럼 7장에서는 위협 모델을 통해 <strong>무엇을 방어해야 하는지</strong>를 명확히 정의한 후, 보안 자동화 도구를 사용해 <strong>어떻게 방어할지</strong>를 실질적으로 보여줍니다.</p>
<h3 id="heading-8">성능 테스트 (8장)</h3>
<p><strong>성능</strong>은 사용자 경험과 매출에 직결되는 중요한 요소로, 이 책에서는 다양한 사례와 통계를 통해 성능 최적화가 왜 중요한지 설득력 있게 보여줍니다.</p>
<ul>
<li><p>페이지 로드 시간 0.5초 증가 시, <strong>사용자 이탈률이 20% 증가</strong></p>
</li>
<li><p>2018년 아마존 프라임데이 장애 시, <strong>약 1억 달러</strong>의 손실 발생</p>
</li>
<li><p>모비파이(Mobify)는 <strong>로딩 시간 100ms 감소</strong>만으로 <strong>연간 매출 38만 달러</strong> 증가 효과</p>
</li>
</ul>
<p>성능 지표를 체계적으로 관리하기 위해 책에서는 <strong>RAIL 모델</strong>을 소개합니다.</p>
<blockquote>
<p><strong>RAIL 모델</strong></p>
<ul>
<li><p>구글이 제안한 웹 성능 측정 및 최적화 가이드라인으로, <strong>Response, Animation, Idle, Load</strong> 네 가지 항목으로 나누어 성능을 모니터링하고 개선합니다.</p>
</li>
<li><p>각 단계별 목표 시간(예: 사용자의 액션에 100ms 내로 반응해야 한다 등)이 명시되어 있어 구체적인 목표 설정이 가능합니다.</p>
</li>
<li><p>자세한 내용은 <a target="_blank" href="https://developer.mozilla.org/ko/docs/Glossary/RAIL">RAIL Model (Web MDN)</a>에서 확인할 수 있습니다.</p>
</li>
</ul>
</blockquote>
<p>8장에서는 이런 성능 지표와 함께 <strong>로드 테스트, 부하 테스트</strong> 등 다양한 테스트 전략을 다루며, 실제로 어떻게 <strong>성능 병목 구간</strong>을 찾아내고 개선할 수 있는지 현실적인 가이드를 제공합니다.</p>
<h3 id="heading-9">접근성 테스트 (9장)</h3>
<p>처음에는 “이런 것까지 꼭 테스트해야 하나?” 싶을 수 있지만, 실제로는 <strong>접근성</strong>이 사용자 경험에 큰 영향을 미치며 법적·윤리적 측면에서도 점점 중요해지고 있습니다. 책에서는 <strong>WCAG 2.0</strong>의 기본 원칙을 기반으로 접근성을 보장하는 방법을 체계적으로 설명합니다.</p>
<blockquote>
<p><strong>WCAG(Web Content Accessibility Guidelines) 2.0</strong></p>
<ul>
<li><p>W3C(월드 와이드 웹 컨소시엄)에서 제정한 웹 콘텐츠 접근성 지침입니다. <strong>인식 가능(Perceivable)</strong>, <strong>운용 가능(Operable)</strong>, <strong>이해 가능(Understandable)</strong>, **견고성(Robust)**의 4대 원칙을 제시합니다.</p>
</li>
<li><p><a target="_blank" href="https://www.w3.org/TR/WCAG21/">W3C 공식 문서</a>를 통해 더 자세한 내용을 확인할 수 있습니다(현재는 2.1이 최신 버전입니다.)</p>
</li>
</ul>
</blockquote>
<p>또한 <strong>WAVE</strong>, <strong>Lighthouse</strong> 같은 자동화 도구부터 <strong>스크린 리더</strong> 테스트, <strong>키보드 전용 사용성 확인</strong> 등 실제로 접근성을 개선하기 위한 다양한 검증 방법이 소개됩니다.</p>
<ul>
<li><p><strong>WAVE</strong>: 웹 페이지 접근성을 분석하고, 시각적으로 문제가 되는 영역을 표시해주는 도구</p>
<ul>
<li><a target="_blank" href="%5Bhttps://wave.webaim.org/%5D\(https://wave.webaim.org/\)">WAVE 공식 웹사이트</a></li>
</ul>
</li>
<li><p><strong>Lighthouse</strong>: 구글이 제공하는 오픈 소스 도구로, 크롬 개발자 도구에서 바로 접근성·성능·SEO 등을 점검할 수 있음</p>
<ul>
<li><a target="_blank" href="%5Bhttps://developer.chrome.com/docs/lighthouse/overview?hl=ko%5D\(https://developer.chrome.com/docs/lighthouse/overview?hl=ko\)">Lighthouse 공식 문서</a></li>
</ul>
</li>
<li><p><strong>스크린 리더</strong>: 시각 장애인을 위한 화면 낭독 소프트웨어(JAWS, NVDA 등)</p>
<ul>
<li><p><a target="_blank" href="https://www.freedomscientific.com/products/software/jaws/">JAWS 공식 웹사이트</a></p>
</li>
<li><p><a target="_blank" href="https://www.nvaccess.org/">NVDA 공식 웹사이트</a></p>
</li>
</ul>
</li>
</ul>
<p>9장을 통해 접근성은 그저 _추가 기능_이 아니라, 서비스 전반의 <strong>설계 철학</strong>이라는 저자의 통찰을 배울 수 있습니다.</p>
<h2 id="heading-7lgfio2znoyaqsdrsknrspu">책 활용 방법</h2>
<p>개인적으로 느꼈을 때 이 책을 활용할 수 있는 가장 좋은 방법을 정리해봤습니다.</p>
<ol>
<li><p><strong>1장 먼저 정독하기</strong></p>
<ul>
<li><p>1장은 이 책의 서론 격으로, ‘풀스택 테스트’의 기본 개념과 시프트 레프트 전략 등 핵심 골자를 제시합니다.</p>
</li>
<li><p>이를 꼼꼼히 읽어본 뒤, *“아, 이런 종류의 테스트가 있구나”*라는 큰 그림을 잡으면 이후 각 장을 선택적으로 보기에 훨씬 수월해집니다.</p>
</li>
</ul>
</li>
<li><p><strong>현업(또는 현재 프로젝트)에 필요한 테스트 기술 파악</strong></p>
<ul>
<li><p>1장을 통해 전체 목록을 살펴본 뒤, 지금 당장 필요한 영역(예: 성능, 보안, 접근성 등)을 우선적으로 찾아봅니다.</p>
</li>
<li><p>목차나 장별 요약을 참고해 구체적인 전략과 도구 사용법을 빠르게 확인하고, 필요한 부분만 집중 공략해도 좋습니다.</p>
</li>
</ul>
</li>
<li><p><strong>백과사전처럼 필요할 때마다 참고</strong></p>
<ul>
<li><p>이 책은 각 챕터의 구성이 비슷한 패턴(<strong>구성 요소 → 전략 → 실습 → 추가 테스트 도구 → 인사이트</strong>)으로 되어 있어, 원하는 테스트 기술을 바로 찾아보고 참고하기 쉽습니다.</p>
</li>
<li><p>실제 프로젝트에서 “보안 취약점 스캔 방법이 궁금하다”거나 “UI 일관성 테스트를 어떻게 자동화할까?” 같은 구체적 고민이 떠오를 때마다 관련 챕터를 찾아보세요.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-6rcc7j247kcb7j24ioqwkoydge2piq">개인적인 감상평</h2>
<p>처음 이 책을 펼쳤을 때, 테스트 분야를 거의 전 영역에 걸쳐 소개한다는 점이 가장 인상적이었습니다. 특히 1~2장에서 전반적인 테스트 개념과 ‘왜 테스트가 중요한지’에 대한 설득이 잘 이루어져, 평소 테스트에 대해 피상적으로 알던 제게 많은 통찰을 주었습니다.</p>
<p>성능 테스트에 대한 8장 역시 실제 업무에서 바로 활용할 수 있을 정도로 구체적인 예시와 전략을 다뤄 큰 도움이 됐습니다. 다만 전체적으로 예제 코드나 프로젝트별 튜토리얼이 조금 더 풍부했으면 어땠을까 하는 아쉬움도 있습니다. 실제로 테스트 전략을 구성해본 경험이 적은 독자에게는 다소 이론적이라고 느껴질 수 있을 것 같습니다.</p>
<h2 id="heading-6rkw66ggiouwjydstptsspw">결론 및 추천</h2>
<p><strong>“풀스택 테스트”</strong>는 테스트 기법 전반을 폭넓게 다루고 있어, 개발자나 QA 엔지니어가 <strong>테스트의 중요성</strong>과 <strong>다양한 기법</strong>을 빠르게 파악하기에 좋습니다. 특히 <strong>보안, 성능, 접근성</strong>처럼 실무에서 간과하기 쉬운 분야를 체계적으로 설명하기 때문에, 이를 업무에 적용해보고 싶은 분들에게 권장할 만합니다.</p>
<p>백과사전형 구성 덕분에 필요한 챕터부터 골라 읽을 수 있다는 점도 장점입니다. 초중급 개발자는 물론, 테스트 전략 수립에 관심 있는 모든 IT 종사자에게 이 책을 추천합니다. 특히 1장을 통해 전체 테스트 기법을 먼저 파악한 뒤, 자신의 프로젝트 상황에 맞춰 해당 테스트 분야를 집중적으로 살펴보는 것을 권장합니다.</p>
<p><strong><em>"한빛미디어의 후원으로 책을 받아 작성합니다."</em></strong></p>
]]></content:encoded></item><item><title><![CDATA[DynamoDB Streams로 서비스 간 데이터 동기화하기]]></title><description><![CDATA[들어가며
이번 글에서는 DynamoDB Streams를 사용하여 DynamoDB의 제약사항으로 해결할 수 없었던 문제를 해결한 사례를 공유하려고 한다.
필요한 사전 지식

AWS DynamoDB

AWS Lambda

Serverless Framework (Lambda 설정을 위해 필요)

NodeJS


문제 상황
우리 팀에서는 개발한 한의원 비대면진료 플랫폼은 DynamoDB를 주 데이터베이스로 사용하고 있었다. 그러나 최근에 한의원의 경영...]]></description><link>https://blog.aqudi.me/dynamodb-streams</link><guid isPermaLink="true">https://blog.aqudi.me/dynamodb-streams</guid><category><![CDATA[DynamoDB]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 19 Jan 2025 14:59:10 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-65ok7ja06rca66mw">들어가며</h2>
<p>이번 글에서는 DynamoDB Streams를 사용하여 DynamoDB의 제약사항으로 해결할 수 없었던 문제를 해결한 사례를 공유하려고 한다.</p>
<p>필요한 사전 지식</p>
<ul>
<li><p>AWS DynamoDB</p>
</li>
<li><p>AWS Lambda</p>
</li>
<li><p>Serverless Framework (Lambda 설정을 위해 필요)</p>
</li>
<li><p>NodeJS</p>
</li>
</ul>
<h2 id="heading-66y47kccioydge2zqq">문제 상황</h2>
<p>우리 팀에서는 개발한 한의원 비대면진료 플랫폼은 DynamoDB를 주 데이터베이스로 사용하고 있었다. 그러나 최근에 한의원의 경영 분석 요구사항이 증가하면서, 다양한 관점에서 매출 분석이 필요해졌다. 이때 DynamoDB의 쿼리, 인덱스 제약으로 인해 요구사항을 만족시킬 수 없다는 문제가 발생했다.</p>
<h3 id="heading-7ioi66gc7jq0ioyaloq1roycro2vrq">새로운 요구사항</h3>
<p>한의원의 매출을 분석하여 인사이트를 주기 위해서는 생각보다 다양한 기준에서 분석해야 했다. 초기에 나온 매출 분석 기준만해도 다음과 같다:</p>
<ul>
<li><p>일별/월별/연별 매출 추이</p>
</li>
<li><p>이벤트/비이벤트 기간별 매출 추이</p>
</li>
<li><p>진료 항목별 매출 추이</p>
</li>
<li><p>환자 유입 경로별 매출 현황</p>
</li>
</ul>
<h3 id="heading-dynamodb">DynamoDB의 구조적 한계</h3>
<h4 id="heading-1">1. 제한된 쿼리 패턴</h4>
<p>DynamoDB 특성상 처음 설계된 쿼리 패턴 외에는 쿼리가 불가능하다는 문제가 있다.</p>
<ul>
<li><p>테이블당 하나의 파티션 키와 정렬 키만 사용 가능</p>
</li>
<li><p>미리 정의된 접근 패턴으로만 효율적인 쿼리 가능</p>
</li>
<li><p>유연한 데이터 조인이나 그룹핑에 제한</p>
</li>
</ul>
<h4 id="heading-2">2. 분석 워크로드 부적합</h4>
<p>매출 분석을 위한 다양한 집계 연산이 필요했지만, DynamoDB는 이러한 작업에 최적화되어 있지 않다:</p>
<ul>
<li><p>복잡한 집계 쿼리 직접 수행 불가</p>
</li>
<li><p>다차원 분석을 위한 유연한 쿼리 제한</p>
</li>
<li><p>대규모 데이터 스캔 시 성능 저하</p>
</li>
</ul>
<h4 id="heading-3">3. 확장의 어려움</h4>
<p>분석 요구사항은 지속적으로 변화하고 증가하는데 반해, DynamoDB는 이러한 변화에 유연하게 대응하기 어렵다:</p>
<ul>
<li><p>새로운 분석 관점마다 데이터 모델 수정 필요</p>
</li>
<li><p>미리 정의되지 않은 새로운 조회 패턴 수용 어려움</p>
</li>
</ul>
<p>이러한 새로운 요구사항과 DynamoDB의 한계로 인해 단순히 인덱스를 추가하는 방식으로는 근본적인 해결이 어려웠다. 따라서 우리는 비대면진료와 매출 분석 서비스를 분리하여, 각각 특성에 맞는 데이터베이스를 사용하는 방향으로 해결책을 모색하게 되었다.</p>
<h2 id="heading-642w7j207yswioupmeq4so2zlcdslyttgqtthy3sspgg6rws7zie">데이터 동기화 아키텍처 구현</h2>
<p>분석 서비스로의 데이터 동기화를 위해 DynamoDB Streams와 Lambda를 활용했다. 이 방식은 진료 서비스의 코어 로직을 수정하지 않고도 데이터를 실시간으로 동기화할 수 있다는 장점과 다른 AWS 서비스와 쉽게 연동할 수 있다는 장점이 있다.</p>
<h3 id="heading-7j2067kk7yq4io2dkoumhoupha">이벤트 흐름도</h3>
<pre><code class="lang-mermaid">sequenceDiagram
    participant D as DynamoDB
    participant S as Streams
    participant L as Lambda
    participant P as PostgreSQL

    D-&gt;&gt;S: 데이터 변경 발생
    S-&gt;&gt;L: 이벤트 전달 (batch)

    L-&gt;&gt;P: SQL 변환 및 저장
</code></pre>
<h3 id="heading-dynamodb-streams">DynamoDB Streams 설정</h3>
<ol>
<li><p>AWS Console 접속</p>
</li>
<li><p>DynamoDB 테이블 선택</p>
</li>
<li><p>내보내기 및 스트림 탭 &gt; DynamoDB 스트림 세부 정보 켜기</p>
</li>
<li><p>새 이미지와 이전 이미지 선택 후 스트림 켜기</p>
</li>
<li><p>DynamoDB Stream ARN 복사</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737298717260/0b0ac7c0-2990-4f39-9cbc-fe2969b75bd5.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-lambda">Lambda 동기화 로직 구현</h3>
<p>만약 DynamoDB 테이블 하나에 여러 키 구조가 혼재되어 있다면 원치 않는 데이터가 추가/변경됐을 때도 Lambda가 호출되는 일이 발생할 수 있다.</p>
<p>이를 방지하기 위해 이벤트 필터링을 사용하면 특정 데이터 변경사항만 선택적으로 처리할 수 있다. 예를 들어 진료 기록 중 결제가 완료된 건만 매출 분석 시스템으로 전송하고 싶다면 다음과 같이 설정할 수 있다:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">functions:</span>
  <span class="hljs-attr">syncCompletedPayments:</span>
    <span class="hljs-attr">handler:</span> <span class="hljs-string">src/handlers/syncToAnalytics.handler</span>
    <span class="hljs-attr">events:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">stream:</span>
          <span class="hljs-attr">type:</span> <span class="hljs-string">dynamodb</span>
          <span class="hljs-attr">arn:</span> <span class="hljs-string">&lt;복사한</span> <span class="hljs-string">DynamoDB</span> <span class="hljs-string">Stream</span> <span class="hljs-string">Arn&gt;</span>
          <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
          <span class="hljs-attr">filterPatterns:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">eventName:</span> [<span class="hljs-string">"INSERT"</span>, <span class="hljs-string">"MODIFY"</span>]
              <span class="hljs-attr">dynamodb:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">Keys:</span>
                    <span class="hljs-attr">pk:</span>
                      <span class="hljs-attr">S:</span>
                        <span class="hljs-bullet">-</span> <span class="hljs-attr">prefix:</span> <span class="hljs-string">"CLINIC#"</span>    <span class="hljs-comment"># 병원 ID</span>
                    <span class="hljs-attr">sk:</span>
                      <span class="hljs-attr">S:</span>
                        <span class="hljs-bullet">-</span> <span class="hljs-attr">prefix:</span> <span class="hljs-string">"PAYMENT#"</span>   <span class="hljs-comment"># 결제 기록</span>
                <span class="hljs-attr">NewImage:</span>
                  <span class="hljs-attr">status:</span>
                    <span class="hljs-attr">S:</span> [<span class="hljs-string">"COMPLETED"</span>]           <span class="hljs-comment"># 완료된 결제만</span>
                  <span class="hljs-attr">amount:</span>
                    <span class="hljs-attr">N:</span> [{<span class="hljs-attr">"numeric":</span> [<span class="hljs-string">"&gt;"</span>, <span class="hljs-number">0</span>]}] <span class="hljs-comment"># 금액이 있는 경우만</span>
</code></pre>
<ul>
<li><p>필터 패턴 (filterPatterns 속성)</p>
<ul>
<li><p><code>eventName</code>: 어떤 DB 작업을 감지할지 지정</p>
</li>
<li><p><code>Keys</code>: DynamoDB의 파티션키/정렬키 기준 필터링</p>
</li>
<li><p><code>NewImage</code>: 변경된 데이터의 특정 필드값으로 필터링</p>
</li>
</ul>
</li>
</ul>
<p>Lambda 함수는 DynamoDB 변경 사항을 PostgreSQL에 적절히 매핑하여 저장한다:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> DynamoDBStreamEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-lambda'</span>;
<span class="hljs-keyword">import</span> { unmarshall } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/util-dynamodb"</span>; <span class="hljs-comment">// 역직렬화 유틸</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event: DynamoDBStreamEvent) =&gt; {
  <span class="hljs-keyword">const</span> records = event.Records.map(<span class="hljs-function"><span class="hljs-params">record</span> =&gt;</span> ({
    eventName: record.eventName,
    oldImage: unmarshall(record.dynamodb.OldImage || {}),
    newImage: unmarshall(record.dynamodb.NewImage || {})
  }));

  <span class="hljs-comment">// 배치 처리를 위한 트랜잭션 시작</span>
  <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">await</span> pool.connect();
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">'BEGIN'</span>);

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> record <span class="hljs-keyword">of</span> records) {
      <span class="hljs-keyword">switch</span> (record.eventName) {
        <span class="hljs-keyword">case</span> <span class="hljs-string">'INSERT'</span>:
        <span class="hljs-keyword">case</span> <span class="hljs-string">'MODIFY'</span>:
          <span class="hljs-keyword">await</span> upsertToPostgres(client, record.newImage);
          <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> <span class="hljs-string">'REMOVE'</span>:
          <span class="hljs-keyword">await</span> deleteFromPostgres(client, record.oldImage);
          <span class="hljs-keyword">break</span>;
      }
    }

    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">'COMMIT'</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">'ROLLBACK'</span>);
    <span class="hljs-keyword">throw</span> error;
  } <span class="hljs-keyword">finally</span> {
    client.release();
  }
};
</code></pre>
<h2 id="heading-6rkw66gg">결론</h2>
<p>이번 포스팅에서는 DynamoDB의 쿼리 패턴 제약으로 인해 만족시키지 못하는 요구사항들을 해결하기 위해 DynamoDB Streams와 Lambda를 활용해 PostgreSQL로 데이터를 동기화하는 방식을 소개했다.</p>
<h3 id="heading-7lau7zueioqwnoyeocdsgqztla0">추후 개선 사항</h3>
<ol>
<li><p>재시도 정책 설정</p>
</li>
<li><p>배치 처리 최적화</p>
</li>
<li><p>모니터링 알림 설정</p>
</li>
</ol>
<h2 id="heading-7lc46rog7j6q66om">참고자료</h2>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/GSI.html">DynamoDB Local Seconday Index (LSI) - AWS DynamoDB</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/GSI.html">DynamoDB Global Seconday Index (GSI) - AWS DynamoDB</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/Streams.html">DynamoDB Streams에 대한 변경 데이터 캡처 - AWS DynamoDB</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/Streams.Lambda.html">DynamoDB Streams 및 AWS Lambda 트리거 - AWS DynamoDB</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[2024년, 다사다난 했던 해]]></title><description><![CDATA[2024년을 한 마디로 정리하자면 "다사다난했던 해"라고 할 수 있을 것 같다. 환경 변화로만 봐도 프리랜서, 직장인, 훈련소 이 세가지가 한 해에 일어났으니 1년이 정말 길었다고 느껴진다. 1년 회고하는 지금도 이게 한 해에 다 이뤄졌다는 게 믿기지 않는다.
정말 길었던 한 해였기 때문에 연간 회고에 적고 싶은 이야기가 굉장히 많지만 그 중에서 커리어, 기록, 여행, 관계 이렇게 4가지 키워드를 중심으로 2024년을 기록해보려고 한다.
커리어...]]></description><link>https://blog.aqudi.me/2024-retrospective</link><guid isPermaLink="true">https://blog.aqudi.me/2024-retrospective</guid><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 05 Jan 2025 14:58:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736168496540/090ebdb2-fc4f-4dd5-8887-3f578e472947.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>2024년을 한 마디로 정리하자면 "다사다난했던 해"라고 할 수 있을 것 같다. 환경 변화로만 봐도 프리랜서, 직장인, 훈련소 이 세가지가 한 해에 일어났으니 1년이 정말 길었다고 느껴진다. 1년 회고하는 지금도 이게 한 해에 다 이뤄졌다는 게 믿기지 않는다.</p>
<p>정말 길었던 한 해였기 때문에 연간 회고에 적고 싶은 이야기가 굉장히 많지만 그 중에서 커리어, 기록, 여행, 관계 이렇게 4가지 키워드를 중심으로 2024년을 기록해보려고 한다.</p>
<h3 id="heading-7luk66as7ja0ioq3uoumroqzocdrs5hsl63si5zsnpeh">커리어 그리고 병역시작!</h3>
<p>먼저 가장 축하하고 싶은 일이다! 4월에 입사하면서 <strong>전문연구요원을 시작해서 벌써 9개월차</strong>가 됐다. 지금까지 계속 스트레스 받던 일인데 시작하고 나니까 속이 후련하다. 3년을 할 바에는 1년 반 육군을 갔다와야 하나 고민을 수십번을 했고 대학원을 괜히 갔나 후회도 많이 했는데 지금에와서 생각하니 배부른 소리였다.</p>
<p>10월에 훈련소를 갔다오고 나니까 1년 반 육군은 말이 안되는 소리라는 걸 알게 됐다. 휴가 갔다온다고 생각하고 갔는데 전혀 아니었다. 앞으로는 군대가 낫다는 소리를 절대 안하려고 한다. <strong>뭐가 됐든 사회가 좋다.</strong></p>
<p>취업 준비부터 시작해서 취업까지 약 3개월이 걸렸는데 다시 돌아보니 정말 운이 좋게 착착 진행됐던 것 같다. 이력서 준비하는 과정부터 시작해서 내가 고민이 있을 때 경청해주고 조언을 해준 부모님과 주변 동료, 선배들에게 너무 감사하다. 지금같이 어려운 시기에 내가 목표했던 조건의 회사에 적절한 시기에 들어갈 수 있었던 건 정말 운이 많이 도와준 것 같다.</p>
<p>취직 준비를 하면서 가장 크게 배운 점은 <strong>어떤 일을 하던 기준을 내 안에 두는 게 가장 중요하다</strong>는 것이다. 아무리 맛있는 음식이라고 주변에서 칭찬을 하더라도 내 입맛에 맞지 않으면 나에게 그 음식은 좋은 음식이 아니다. 커리어도 마찬가지다. 나와 비슷한 상황인 사람을 참고할 수는 있어도 결국은 판단은 내 몫이고 내 기준에 따라 해야 한다. 취업 준비를 하던 시기의 나는 처음 해보는 이 일이 두려워 자꾸 다른 사람에게 미루고 남들과 비교만 하며 나를 옥죄고 있었다. 주변 사람들의 조언 덕에 <strong>결국 내 커리어는 내가 원하는 커리어의 모습</strong>에 달려 있다는 걸 깨닫고 두렵지만 차근차근 준비해서 지금의 내가 있을 수 있다.</p>
<h4 id="heading-2025">⭐2025년에는!⭐</h4>
<p>커리어 영역에서 내 기준을 만들기 위해 계속해서 노력해야겠다.</p>
<ul>
<li><p>어떤 기술과 업무를 맡았을 때 내가 가장 몰입하고 성장하는지 관찰하기</p>
</li>
<li><p>업계의 다양한 선배들을 만나 그들의 커리어 기준과 철학 배우기</p>
</li>
<li><p>내가 생각하는 "좋은 개발자"의 모습 정의하기</p>
</li>
</ul>
<h3 id="heading-6riw66gd7j2yio2emce">기록의 힘!</h3>
<p>주변에 자랑하고 다니는 것 중 하나는 1년 동안 약 49개의 주간 회고를 작성했다는 것이다. 메모어 14기부터 시작해서 17기를 하고 있는 지금까지 약간의 텀을 제외하고는 짧더라도 계속 회고를 지속해왔다. 기록을 하면서 나에 대해서 더 잘 알게 되고 지금에 와서는 아예 기억도 안나는 입사 초의 감정들이 고스란히 회고에 남아있다. 앞으로도 회고는 꾸준히 가져가야 할 내 습관 중 하나가 됐다. 일요일 10시만 되면 놀고 있던 쉬고 있던 일어나 회고를 적는 내 모습 너무 뿌듯하다.</p>
<p>특히, <strong>감정에 대한 회고는 그 당시에 이유없이 느껴졌던 불안감이나 우울함을 객관적으로 바라보게 해주기 떄문에 너무 소중한 기록</strong>이다. 2022년 말부터 학교에서 심리상담을 받으면서 '나'를 알기 위해 노력했던 것들이 지금에 와서 회고로 완성된 것 같다. <strong>어떤 상황에서 내가 어떤 감정을 느끼는지 파악</strong>하고 다음에 비슷한 상황이 왔을 때 더 빠르게 인지하고 조치를 취할 수도 있게 됐다는 게 자랑스럽다. 조만간 그때 심리상담사 선생님께 연락드려 감사함과 성장한 내 모습을 전해야겠다.</p>
<p>처음에는 회고 쓰는 것도 어려웠는데 어느 순간부터 아래처럼 내가 기록하고 싶은 내용, 관점을 중심으로 회고 포맷이 계속 변해왔다. 아래는 2024년 18주 때 썼던 신입 사원의 고충과 52주 때 썼던 여전히 고민하는 내 모습이 담긴 회고다. 까맣게 잊고 있었는데 그때의 감정이 다시금 새록새록 떠오른다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736088846684/dd76c33b-d243-4ea6-b63d-86eba2cf22f8.png" alt="2024년 18주차 회고" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736088878571/2064227d-ff1e-4ef8-807e-20070acd7f17.png" alt="2024년 52주차 회고" class="image--center mx-auto" /></p>
<h4 id="heading-2025-1">⭐2025년에는!⭐</h4>
<ul>
<li><p>지금처럼 주간 회고 작성하는 습관을 유지하기</p>
</li>
<li><p>월간회고, 분기별 회고를 추가하여 생각을 더 정돈하기</p>
</li>
<li><p>기록을 통해 나에 대해서 더 알기 위해 노력하기</p>
</li>
</ul>
<h3 id="heading-7jes7zajlcdstptslrug66em65ok6riw">여행, 추억 만들기</h3>
<p>이번 년도에 잘했던 일 중 하나로 여행도 소개하고 싶다. 2024년 전에는 해외 여행을 일본 오키나와와 규슈 지방 한 바퀴 돈 것 제외하고는 없었다. 그런데 2024년에만 대만, 필리핀, 홍콩, 일본 4개국에 총 5번이나 방문했다. 회고 하기 전까지는 이게 전부 1년 안에 일어난 거라고 생각도 못했다. 모든 여행이 내게 의미 깊은 날들이었어서 공유해보려고 한다.</p>
<ul>
<li><p>1월 대만, 나홀로 첫 해외여행</p>
</li>
<li><p>3월 필리핀, 입사 전 후회없는 선택</p>
</li>
<li><p>9월 홍콩, 훈련소 입소 전 후회없는 선택, 여자친구와 첫 해외 여행</p>
</li>
<li><p>12월 일본 후쿠오카, 대학 친구들과 첫 해외 여행</p>
</li>
<li><p>12월 도쿄, 생일, 크리스마스, 여자친구와 500일 기념 여행!</p>
</li>
</ul>
<p>가장 헤프닝이 많았던 여행이 1월 대만여행이다. 고등학교 친구와 같이 가기로 했었는데 예약까지 다 했더니 이 친구가 여권이 2년 전에 만료됐는데 이미 여권 신청을 해서 긴급여권 발급도 안되는 상황이 돼버렸다. 그래서 결국 나 혼자 떠나게 됐다.</p>
<p>의도치 않게 홀로 떠나는 첫 해외여행이 됐지만 너무 뜻깊은 시간이었다. 우연히 인도인 친구를 사귀어서 같이 하루를 보내고 친구의 친구 소개로 알게 된 홍콩, 대만 친구들과 대만식 훠궈도 먹어봤다. 버스투어 갔을 때는 혼자 온 한국인 관광객 2명과 파티를 맺어서 신나게 돌아다녔고 마지막 날에는 5년 만에 만나는 고등학교 친구가 우연히 대만에 왔다고 해서 같이 야시장을 돌았다.</p>
<p>우연으로 시작했지만 내게는 너무 좋은 기회가 됐고 이후 좋은 일도 있었고 나쁜 일도 있었지만 결국 다양한 사람을 경험해볼 수 있었고 나의 취향에 대해서 충분히 고민하고 행동할 수 있는 시간이었다.</p>
<p>이 여행으로 가장 크게 배운 점은 두가지다.</p>
<ul>
<li><p><strong>이동 동선 만큼은 꼭 미리 짜두자.</strong> 미리 고려하지 않은 만큼 시간 손해를 본다.</p>
</li>
<li><p><strong>그 나라 언어 공부는 얕게라도 꼭 하고 가자.</strong> 외국에 나가면 우리는 그냥 아시아인 1이다. 영어 안 통하는 곳이 많다.</p>
</li>
</ul>
<h4 id="heading-2025-2">⭐2025년에는!⭐</h4>
<ul>
<li><p>일본어로 자기소개할 수 있도록 연습하기</p>
</li>
<li><p>가족들과 여행 가보기</p>
</li>
</ul>
<h3 id="heading-6rsa6roe">관계</h3>
<p>2024년은 관계를 통해 내가 바라고자 하는 모습을 실현하고자 노력하는 해였다. <a target="_blank" href="https://www.youtube.com/watch?v=lyZx72FwBmY&amp;t=506s">4차 산업혁명의 시대, 자기를 혁신하는 방법</a>이라는 영상에서 본 "당신을 설명하는 사람은 최근에 만난 5명"이라는 말이 큰 영향을 주었다. 결국 내 주변에는 내 생활 패턴과 비슷한 사람들이 주로 남게 된다는 것이었다. 반대로 생각하면 내가 어떤 사람이 되고 싶은가에 따라서 주변에 어떤 사람을 둬야 하는지가 결정되는 것이다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736088912716/c8a0186d-1854-40ab-8781-de6c0ead79c6.png" alt="출처: 4차 산업혁명의 시대, 자기를 혁신하는 방법" class="image--center mx-auto" /></p>
<p>2024년의 내가 바라는 모습은 성장, 일 잘하기, 글쓰기라는 키워드로 설명할 수 있을 것 같다.</p>
<ul>
<li><p><strong>성장</strong>: 메모어에서 성장과 회고에 관심이 많은 사람들을 만났다.</p>
</li>
<li><p><strong>일 잘하기</strong>: 셀프그로쓰 스터디를 통해 회사 생활을 잘하는 법에 관심있는 동료들을 만났다.</p>
</li>
<li><p><strong>글쓰기</strong>: 글또에서 글쓰기를 잘하고 싶어하는 사람들을 만났다.</p>
</li>
</ul>
<p>성장, 일 잘하기, 글쓰기에 관심이 많은 사람들을 가까이 하면서 1년이 지난 지금은 기록과는 거리가 먼 내가 매주 회고를 하는 습관을 가진 사람이 됐고, 죽어있던 내 블로그에도 매달 2편의 글이 올라오고 있다. 그리고 처음 회사에서 혼자 끙끙 앓던 내가 이제는 회사에서 고민이 있을 때면 동료들과 함께 논의할 수 있는 내가 됐다!</p>
<p>다들 너무 고마운 존재들이고 앞으로 이런 커뮤니티에 나는 어떤 기여를 할 수 있을까? 어떻게 하면 좋은 영향을 주는 사람이 될 수 있을까를 계속해서 고민해서 받은 것의 배로 돌려주고 싶다.</p>
<h4 id="heading-2025-3">⭐2025년에는!⭐</h4>
<ul>
<li><p>각 영역별로 롤모델이 되는 멘토 찾기</p>
</li>
<li><p>일회성 만남이 아닌 지속적인 관계 맺기에 집중하기</p>
</li>
<li><p>받기만 하는 것이 아닌 서로 도움이 되는 관계 만들기</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[서버 부담 DOWN, 업로드 속도 UP: S3 Presigned Post로 이미지 업로드하기]]></title><description><![CDATA[배경
이번에 회사에서 후기 서비스를 개발하고 있다. 요구사항 중에 후기에는 최대 5개의 사진이 포함된다는 항목이 있었다. API 서버를 Aws Lambda로 만들고 있어서 Api Gateway의 payload 크기 제한인 10MB, Lambda의 payload 크기 제한이 6MB로 이미지 업로드 크기에 제한이 생기는 문제가 있다. 참고: Amazon API Gateway 할당량 및 중요 정보, AWS Lambda 할당량


또한 Lambda로 ...]]></description><link>https://blog.aqudi.me/overcome-lambda-payload-limit-through-s3-presigned-post</link><guid isPermaLink="true">https://blog.aqudi.me/overcome-lambda-payload-limit-through-s3-presigned-post</guid><category><![CDATA[AWS]]></category><category><![CDATA[lambda]]></category><category><![CDATA[image upload]]></category><category><![CDATA[s3 presigned URL]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 22 Dec 2024 11:46:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734867149328/d923117b-1237-48c7-8544-d7ace3231f75.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-67cw6rk9">배경</h2>
<p>이번에 회사에서 후기 서비스를 개발하고 있다. 요구사항 중에 후기에는 최대 5개의 사진이 포함된다는 항목이 있었다. API 서버를 Aws Lambda로 만들고 있어서 Api Gateway의 payload 크기 제한인 10MB, Lambda의 payload 크기 제한이 6MB로 이미지 업로드 크기에 제한이 생기는 문제가 있다. <a target="_blank" href="https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/limits.html">참고: Amazon API Gateway 할당량 및 중요 정보</a>, <a target="_blank" href="https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/gettingstarted-limits.html">AWS Lambda 할당량</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734855187358/92d26026-959b-495d-b03a-0643c3be31dd.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734858214557/ca235369-6c2d-4e3a-a16d-fe5cb394c8b7.jpeg" alt class="image--center mx-auto" /></p>
<p>또한 Lambda로 이미지를 업로드하는 경우에는 이미지 업로드 후 S3로 업로드라는 과정까지 추가되므로 이미지를 올리는 데 더 많은 시간이 소요되고 소요되는 만큼 lambda 비용이 추가적으로 나온다는 문제도 있다.</p>
<p>오늘은 이 문제를 해결하기 위해서 클라이언트에서 직접 s3로 파일을 업로드하는 방법들 최종적으로 선택했던 <strong>s3-presigned-post</strong> 방식에 대해서 포스팅하려고 한다.</p>
<h2 id="heading-1-api-gateway-s3-proxy">해결책 1: API Gateway S3 Proxy</h2>
<p>우선 API Gateway를 통해서 S3에 접근하는 방법이 있다. 이 방법은 S3를 public으로 만들지 않아도 S3에 접근할 수 있도록 할 수 있고 API Gateway의 보안 기능들을 활용할 수 있다. 그리고 Lambda를 통하지 않아도 되니 추가적인 비용이 들지도 않고 Lambda의 payload 크기 제한이 6MB가 넘는 파일도 다룰 수 있게된다.</p>
<p>그런데 이 방법은 여전히 10MB의 payload 제한이 있다는 단점이 있어서 이번에는 선택하지 않았다.</p>
<h2 id="heading-2-s3-presigned-url">해결책 2: S3 presigned url</h2>
<p>다음으로 고려했던 방법은 presigned url이었다. S3에 저장된 파일에 접근할 수 있는 권한 정보를 url에 인코딩하여 서버에서 클라이언트로 보내주면 그 url을 이용해서 파일을 조회하거나 업로드할 수 있는 기능이다.</p>
<p>구현이 매우 간단하고 url의 유효시간을 조절할 수 있어 유용하다.</p>
<p>서버에서 업로드를 관리하지 않으니 업로드 속도도 더 빠르고 파일이 커서 lambda payload limit에 걸리거나 lambda timeout이 생기는 일도 발생하지 않는다.</p>
<h3 id="heading-s3-presigned-url">s3 presigned url 방식 서버 코드 예시</h3>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> { S3Client, PutObjectCommand } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"@aws-sdk/client-s3"</span>);
<span class="hljs-keyword">const</span> { getSignedUrl } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"@aws-sdk/s3-request-presigner"</span>);

<span class="hljs-keyword">const</span> s3 = <span class="hljs-keyword">new</span> S3Client({ <span class="hljs-attr">region</span>: process.env.AWS_REGION });

<span class="hljs-built_in">module</span>.exports.generateSignedUrl = <span class="hljs-keyword">async</span> ({
  key,
  contentType,
  expiresIn = <span class="hljs-number">3600</span>,
}) =&gt; {
  <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> PutObjectCommand({
    <span class="hljs-attr">Bucket</span>: process.env.BUCKET_NAME,
    <span class="hljs-attr">Key</span>: key,
    <span class="hljs-attr">ContentType</span>: contentType,
  });

  <span class="hljs-keyword">const</span> signedUrl = <span class="hljs-keyword">await</span> getSignedUrl(s3, command, { expiresIn });
  <span class="hljs-keyword">return</span> signedUrl;
};
</code></pre>
<h3 id="heading-s3-presigned-url-1">s3 presigned url 방식 클라이언트 코드 예시</h3>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> getUploadUrl = <span class="hljs-keyword">async</span> (params: {
  key: <span class="hljs-built_in">string</span>
  contentType: <span class="hljs-built_in">string</span>
}): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt; =&gt; {
  <span class="hljs-keyword">return</span> axios
    .post(<span class="hljs-string">'/generate-presigned-url'</span>, {
      ...params,
    })
    .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> res.data);
}

<span class="hljs-keyword">const</span> file = <span class="hljs-string">"&lt;form으로 업로드한 파일&gt;"</span>
<span class="hljs-keyword">const</span> uploadUrl = <span class="hljs-keyword">await</span> getUploadUrl({ key, contentType: file.type })요청

<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(uploadUrl, {
    method: <span class="hljs-string">'PUT'</span>,
    body: file,
    headers: {
      <span class="hljs-string">'Content-Type'</span>: file.type,
    },
})

<span class="hljs-keyword">if</span> (!response.ok) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'파일 업로드 실패'</span>)
}
</code></pre>
<h3 id="heading-s3-presigned-url-2">s3 presigned url 방식의 한계</h3>
<p>클라이언트 측에서 파일 업로드를 전부 제어하기 때문에 content type, 파일 사이즈 등을 서버에서 컨트롤 할 수 없다는 문제가 있다. IAM 정책을 통해서 일부 제약을 걸 수도 있지만 이 경우에는 S3 전체에 적용되기 때문에 상황에 따라 유연하게 사용하기가 어렵고 인프라에서 관리하게 되니 눈으로 확인하기 어렵다고 느껴졌다.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
  <span class="hljs-attr">"Statement"</span>: [
    {
      <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Deny"</span>,
      <span class="hljs-attr">"Action"</span>: [<span class="hljs-string">"s3:PutObject"</span>, <span class="hljs-string">"s3:PutObjectAcl"</span>],
      <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::your-bucket-name/*"</span>,
      <span class="hljs-attr">"Condition"</span>: {
        <span class="hljs-attr">"StringNotEquals"</span>: {
          <span class="hljs-attr">"s3:x-amz-meta-file-type"</span>: [<span class="hljs-string">"image/jpeg"</span>, <span class="hljs-string">"image/png"</span>]
        }
      }
    }
  ]
}
</code></pre>
<h2 id="heading-3-s3-presigned-post">해결책3: S3 presigned post</h2>
<p>마지막으로 찾아본 방법은 s3 presigned post를 활용하는 방식이다.</p>
<p>presigned post는 presigned url과 유사하게 s3의 특정 객체에 일시적으로 접근할 권한을 주는 방식이다. 여기에 conditions를 통해서 업로드하는 파일에 제약을 걸 수 있다. Conditions는 <a target="_blank" href="https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html">AWS S3 POST Policy</a> 문서를 참고하여 작성할 수 있다.</p>
<p>이번에는 파일 사이즈, 파일 타입, 업로드할 폴더 정도만 제약을 걸면 되서 다음과 같은 조건들을 사용했다.</p>
<pre><code class="lang-js">[
  [<span class="hljs-string">'content-length-range'</span>, <span class="hljs-number">0</span>, options.maxFileSize], <span class="hljs-comment">// 파일 크기 제한</span>
  [<span class="hljs-string">'starts-with'</span>, <span class="hljs-string">'$Content-Type'</span>, options.allowedMimeType], <span class="hljs-comment">// MIME 타입 제한</span>
  [<span class="hljs-string">'starts-with'</span>, <span class="hljs-string">'$key'</span>, prefix], <span class="hljs-comment">// 파일 키는 특정 prefix로 시작</span>
]
</code></pre>
<h3 id="heading-s3-presigned-post">S3 presigned post 서버 코드 예시</h3>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { S3Client } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/client-s3'</span>;
<span class="hljs-keyword">import</span> { createPresignedPost } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/s3-presigned-post'</span>;
<span class="hljs-keyword">import</span> { ulid } <span class="hljs-keyword">from</span> <span class="hljs-string">'ulid'</span>;

<span class="hljs-keyword">const</span> s3Client = <span class="hljs-keyword">new</span> S3Client({
  region: process.env.AWS_REGION,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generatePresignedPost</span>(<span class="hljs-params">
  bucketName: <span class="hljs-built_in">string</span>,
  prefix: <span class="hljs-built_in">string</span>,
  options: {
    maxFileSize: <span class="hljs-built_in">number</span>;
    expirationSeconds: <span class="hljs-built_in">number</span>;
    allowedMimeType: <span class="hljs-built_in">string</span>;
  },
</span>) </span>{
  <span class="hljs-keyword">const</span> fileKey = <span class="hljs-string">`<span class="hljs-subst">${prefix}</span><span class="hljs-subst">${ulid()}</span>`</span>;

  <span class="hljs-keyword">const</span> post = <span class="hljs-keyword">await</span> createPresignedPost(s3Client, {
    Bucket: bucketName,
    Key: fileKey,
    Expires: options.expirationSeconds,
    Conditions: [
      [<span class="hljs-string">'content-length-range'</span>, <span class="hljs-number">0</span>, options.maxFileSize], <span class="hljs-comment">// 파일 크기 제한</span>
      [<span class="hljs-string">'starts-with'</span>, <span class="hljs-string">'$Content-Type'</span>, options.allowedMimeType], <span class="hljs-comment">// MIME 타입 제한</span>
      [<span class="hljs-string">'starts-with'</span>, <span class="hljs-string">'$key'</span>, prefix], <span class="hljs-comment">// 파일 키는 특정 prefix로 시작</span>
    ],
  });

  <span class="hljs-keyword">return</span> post;
}
</code></pre>
<p>위 함수를 실행하면 resource에 접근할 수 있는 url과 옵션으로 준 값들을 바탕으로 생성된 fields가 포함된 응답이 나온다.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"https://s3.&lt;aws_region&gt;.amazonaws.com/&lt;bucket_name&gt;"</span>,
    <span class="hljs-attr">"fields"</span>: {
      <span class="hljs-attr">"bucket"</span>: <span class="hljs-string">"&lt;BUCKET_NAME&gt;"</span>,
      <span class="hljs-attr">"X-Amz-Algorithm"</span>: <span class="hljs-string">"AWS4-HMAC-SHA256"</span>,
      <span class="hljs-attr">"X-Amz-Credential"</span>: <span class="hljs-string">"..."</span>,
      <span class="hljs-attr">"X-Amz-Date"</span>: <span class="hljs-string">"20241220T073342Z"</span>,
      <span class="hljs-attr">"X-Amz-Security-Token"</span>: <span class="hljs-string">"base64로 인코딩된 token"</span>,
      <span class="hljs-attr">"key"</span>: <span class="hljs-string">"path/to/your/file"</span>,
      <span class="hljs-attr">"Policy"</span>: <span class="hljs-string">"jwt로 인코딩된 policy"</span>,
      <span class="hljs-attr">"X-Amz-Signature"</span>: <span class="hljs-string">"..."</span>
    }
}
</code></pre>
<h3 id="heading-s3-presigned-post-1">s3 presigned post 클라이언트 코드 예시</h3>
<p>위의 서버측 응답의 url로 업로드할 파일과 fields의 값들을 formData에 넣어서 POST 요청을 하면 업로드할 수 있다.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>S3 Presigned POST Upload<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Upload File to S3<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"fileInput"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"uploadButton"</span>&gt;</span>Upload<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"upload.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<pre><code class="lang-js"><span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'uploadButton'</span>).addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> fileInput = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'fileInput'</span>);
    <span class="hljs-keyword">const</span> file = fileInput.files[<span class="hljs-number">0</span>];

    <span class="hljs-keyword">if</span> (!file) {
        alert(<span class="hljs-string">'Please select a file to upload.'</span>);
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-comment">// Presigned POST 응답을 가져오는 API 호출 (예: Lambda 함수)</span>
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/getPresignedPost'</span>); <span class="hljs-comment">// 실제 API 엔드포인트로 변경</span>
    <span class="hljs-keyword">const</span> presignedPost = <span class="hljs-keyword">await</span> response.json();

    <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();

    <span class="hljs-comment">// Presigned POST의 필드 추가</span>
    <span class="hljs-built_in">Object</span>.entries(presignedPost.fields).forEach(<span class="hljs-function">(<span class="hljs-params">[key, value]</span>) =&gt;</span> {
        formData.append(key, value);
    });

    <span class="hljs-comment">// 파일 추가</span>
    formData.append(<span class="hljs-string">'file'</span>, file);

    <span class="hljs-comment">// S3로 파일 업로드</span>
    <span class="hljs-keyword">const</span> uploadResponse = <span class="hljs-keyword">await</span> fetch(presignedPost.url, {
        <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
        <span class="hljs-attr">body</span>: formData
    });

    <span class="hljs-keyword">if</span> (uploadResponse.ok) {
        alert(<span class="hljs-string">'File uploaded successfully!'</span>);
    } <span class="hljs-keyword">else</span> {
        alert(<span class="hljs-string">'File upload failed.'</span>);
    }
});
</code></pre>
<h2 id="heading-6rkw66gg">결론</h2>
<p>서버리스 환경에서 사진을 업로드하려면 Lambda와 API Gateway의 제한(예: 10MB, 6MB)과 Lambda 실행 비용·시간을 모두 고려해야 합니다. 이러한 제약을 해결하면서도, 후기 서비스나 이미지 업로드가 많은 환경에서 유연하고 효율적인 방안을 원한다면, <strong>“S3 Presigned Post”</strong> 방식이 탁월한 선택입니다.</p>
<ul>
<li><p>별도의 Lambda 처리 없이 직접 업로드가 가능해, Lambda 비용과 응답 지연을 줄일 수 있습니다.</p>
</li>
<li><p>파일 크기, MIME 타입, 파일 경로 등 세부 조건을 정책(conditions)으로 설정해 업로드를 유연하게 제어할 수 있습니다.</p>
</li>
<li><p>API Gateway의 페이로드 제한과 Lambda의 메모리·타임아웃 문제에서 자유로울 수 있어, 대규모 트래픽이나 대용량 파일 업로드에도 안정적으로 대응 가능합니다.</p>
</li>
</ul>
<p>결국, 서버리스 환경에서 필요 이상의 비용·시간을 소모하지 않으면서도 확장성과 보안성을 모두 담보할 수 있다는 점이 “S3 Presigned Post” 방식의 가장 큰 장점입니다. 앞으로 후기 서비스를 비롯한 다양한 이미지 업로드 시나리오에서 이 방식을 고려해보셔도 좋을 것 같습니다.</p>
<h2 id="heading-7lc46rog7j6q66om">참고자료</h2>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/limits.html">AWS API Gateway 할당량 및 중요 정보</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/gettingstarted-limits.html">AWS Lambda Limits</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/using-presigned-url.html">AWS S3 Presigned Url</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-s3-presigned-post/">AWS S3 Presigned Post</a></p>
</li>
<li><p><a target="_blank" href="https://dev.to/dilanka-rathnasiri/delivering-images-in-aws-s3-bucket-through-aws-api-gateway-49e">Delivering images in AWS S3 bucket through AWS API gateway</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[AWS Lambda와 Connection Pool 사용 시 발생한 응답 지연 문제 해결기]]></title><description><![CDATA[배경

개발자님! 화면이 너무 늦게 떠요!!

이번 주에 매출 통계 기능을 개발해서 개발 서버에 올렸는데 테스팅을 하다가 갑자기 긴급 호출이 들어왔다. API 속도가 너무 느리다는데 얼마나 느리길래 그런지 확인해봤더니 무려 10~16초가 걸렸다.
신규 레포지토리를 파서 개발한 거라 설정에 문제가 있었나? VPC 간 통신 때문에 지연되는 건가? 별의별 생각이 다 들어서 각 모듈별로 실행되는 시간, 실제 쿼리하는 시간을 전부 측정해봤는데 10~16...]]></description><link>https://blog.aqudi.me/aws-lambda-connection-pool-response-latency-issue-resolution</link><guid isPermaLink="true">https://blog.aqudi.me/aws-lambda-connection-pool-response-latency-issue-resolution</guid><category><![CDATA[AWS]]></category><category><![CDATA[lambda]]></category><category><![CDATA[connection pool]]></category><category><![CDATA[latency]]></category><category><![CDATA[troubleshooting]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 24 Nov 2024 11:25:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732447407525/c9792aa7-50f1-4ec6-b0bf-12d69a9f603f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-67cw6rk9">배경</h2>
<blockquote>
<p>개발자님! 화면이 너무 늦게 떠요!!</p>
</blockquote>
<p>이번 주에 매출 통계 기능을 개발해서 개발 서버에 올렸는데 테스팅을 하다가 갑자기 긴급 호출이 들어왔다. API 속도가 너무 느리다는데 얼마나 느리길래 그런지 확인해봤더니 무려 10~16초가 걸렸다.</p>
<p>신규 레포지토리를 파서 개발한 거라 설정에 문제가 있었나? VPC 간 통신 때문에 지연되는 건가? 별의별 생각이 다 들어서 각 모듈별로 실행되는 시간, 실제 쿼리하는 시간을 전부 측정해봤는데 <strong>10~16초나 걸릴 만한 이유를 찾지 못했다.</strong></p>
<p>실제 동작이 query string 받아서 <code>SELECT * From table WHERE a = ? AND b = ? ORDER BY c DESC</code> 정도만 수행하는 아주 간단한 API 였는데, 너무 이상한 수치였다.</p>
<p>CloudWatch 로그를 아무리 뒤져봐도 에러도 보이지 않았고,<br />코드의 각 부분이 실행되는 시간을 측정해봐도 <strong>2초 이내에 모든 실행이 완료</strong>되고 있었다.</p>
<p>이상한 점은,</p>
<ul>
<li><p>지정한 동작(쿼리 실행, 결과 가공 등)은 모두 끝났는데도</p>
</li>
<li><p>응답을 바로 반환하지 않고 6초 이상 대기한 뒤에야 응답을 반환하고 있다는 것</p>
</li>
</ul>
<p>이었다.</p>
<p>즉, <strong>“로직은 다 끝났는데, 어딘가에서 응답이 붙잡혀 있는 상태”</strong> 였다.</p>
<h2 id="heading-connection-pool">Connection Pool과 유휴 연결</h2>
<p>결론부터 말하면, 문제의 1차적인 원인은</p>
<blockquote>
<p><strong>connection pool의 유휴 연결 때문에 Node.js 이벤트 루프가 완전히 비지 않았고</strong>,<br /><strong>그로 인해 Lambda가 응답을 바로 반환하지 못하고 지연이 발생한 것</strong>이었다.</p>
</blockquote>
<p>사용하고 있던 라이브러리는 <a target="_blank" href="https://node-postgres.com/apis/pool">pg.Pool</a>이었고, idleTimeoutMillis 기본값은 10초다.<br />node-postgres 문서의 설명은 다음과 같다:</p>
<blockquote>
<p>number of milliseconds a client must sit idle in the pool and not be checked out<br />before it is disconnected from the backend and discarded<br /><strong>default is 10000 (10 seconds)</strong> – set to 0 to disable auto-disconnection of idle clients</p>
</blockquote>
<p>즉, 한 번 빌려갔다가 돌려준 커넥션이 <strong>최소 10초 동안 idle 상태로 pool 안에 남아 있고</strong>, 이 동안에는 소켓/타이머 레퍼런스가 살아 있기 때문에 <strong>Node.js 이벤트 루프 입장에서는 “아직 완전히 idle이 아니다” 라고 보는 것</strong>이다.</p>
<h2 id="heading-lambda-handler">Lambda handler 방식과 이벤트 루프의 관계</h2>
<p>여기서 자연스럽게 이어지는 질문은 이것이다.</p>
<blockquote>
<p>“idle connection이 이벤트 루프를 잡고 있는 건 알겠는데,<br />이벤트 루프가 안 비면 왜 응답이 늦게 나가는 걸까?”</p>
</blockquote>
<p>그 이유는 <strong>Lambda handler 방식</strong> 때문이다.</p>
<p>Node.js에서 Lambda handler는 크게 두 가지 방식이 있다.</p>
<ol>
<li><p><strong>async/await 기반</strong></p>
<pre><code class="lang-javascript"> <span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> (event, context) =&gt; {
   <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> doSomething(event);
   <span class="hljs-keyword">return</span> {
     <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
     <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(result),
   };
 };
</code></pre>
</li>
<li><p><strong>callback 기반</strong> <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html">(공식 문서 기준으로 Node.js 22.x까지만 지원된다)</a></p>
<pre><code class="lang-javascript"> <span class="hljs-built_in">exports</span>.handler = <span class="hljs-function">(<span class="hljs-params">event, context, callback</span>) =&gt;</span> {
   doSomething(event, <span class="hljs-function">(<span class="hljs-params">err, result</span>) =&gt;</span> {
     <span class="hljs-keyword">if</span> (err) {
       <span class="hljs-keyword">return</span> callback(err);
     }
     callback(<span class="hljs-literal">null</span>, {
       <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
       <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(result),
     });
   });
 };
</code></pre>
</li>
</ol>
<p>callback 방식에서는 기본적으로</p>
<ul>
<li><p>콜백을 호출한 뒤에도</p>
</li>
<li><p><strong>이벤트 루프에 남아 있는 작업(타이머, 소켓 등)이 모두 사라질 때까지</strong>,</p>
</li>
<li><p>또는 <strong>함수 타임아웃에 도달할 때까지</strong></p>
</li>
</ul>
<p>실행을 계속 유지하려는 동작을 한다.</p>
<p>그래서 callback 방식으로 구현한 상태에서 이벤트 루프를 붙잡고 있는 idle connection이 있으면,<br /><strong>로직은 이미 끝났는데도 응답이 바로 나가지 않고 지연되는 상황이 만들어질 수 있다.</strong></p>
<h2 id="heading-fastify-aws-lambda-callback"><strong>fastify-aws-lambda 내부는 callback 기반이었다</strong></h2>
<p>Fastify 앱을 Lambda로 올릴 때 우리는 아래와 같은 형태로 간단하게 래퍼만 호출하고 있었다.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> awsLambdaFastify = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@fastify/aws-lambda'</span>);
<span class="hljs-keyword">const</span> { createApp } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./createApp'</span>);

<span class="hljs-keyword">const</span> app = createApp();
<span class="hljs-keyword">const</span> proxy = awsLambdaFastify(app);

<span class="hljs-built_in">module</span>.exports.handler = proxy;
</code></pre>
<p>코드만 보면 이 핸들러가 <strong>async/await 기반인지, callback 기반인지 전혀 알 수 없다.</strong></p>
<p>그런데 내부 구현을 확인해보니, <a target="_blank" href="https://github.com/fastify/aws-lambda-fastify">aws-lambda-fastify</a>는 Fastify 앱을 감싸면서 <strong>최종적으로 Lambda에 등록되는 handler를 (event, context, callback) 형태의 callback 기반으로 생성하고 있었다.</strong></p>
<p>그 결과,</p>
<ul>
<li><p>callback 스타일의 **기본 동작(이벤트 루프가 완전히 빌 때까지 실행을 유지하려는 동작)**이 그대로 적용되고</p>
</li>
<li><p><code>pg.Pool</code>의 idle connection이 이벤트 루프를 비우지 못하는 상황과 맞물리면서</p>
</li>
<li><p><strong>로직은 이미 다 끝났는데도 응답이 수 초 동안 지연되는 현상</strong>이 발생했다.</p>
</li>
</ul>
<h2 id="heading-7zw06rkw67cp67kv">해결방법</h2>
<h3 id="heading-1-lambda-contextcallbackwaitsforemptyeventloop-false">1) Lambda context.callbackWaitsForEmptyEventLoop = false</h3>
<p>먼저, Node.js Lambda의 <a target="_blank" href="https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/nodejs-context.html">Context 객체</a>는</p>
<p>함수 이름, 버전, 메모리 등 실행 환경에 대한 정보와 몇 가지 설정 값을 제공하는데,<br />그중 하나가 <code>callbackWaitsForEmptyEventLoop</code>다. 이 값은 <strong>callback 기반 핸들러에서만 의미가 있는 옵션</strong>이다.</p>
<p>문서 설명을 보면:</p>
<blockquote>
<p>Node.js 이벤트 루프가 빌 때까지 대기하는 대신, 콜백이 실행될 때 즉시 응답을 보내려면 false로 설정합니다.<br />이것이 false인 경우, 대기 중인 이벤트는 다음 번 호출 중에 계속 실행됩니다.</p>
</blockquote>
<p>즉, 이 값을 <code>false</code>로 설정하면</p>
<ul>
<li><p><strong>이벤트 루프가 완전히 비기까지 기다리지 않고</strong></p>
</li>
<li><p><strong>이미 준비된 응답을 바로 반환</strong>하고</p>
</li>
<li><p>남아 있는 타이머/소켓 같은 것들은 <strong>다음 호출에서 재사용될 수 있도록 놔두는</strong></p>
</li>
</ul>
<p>동작을 하게 된다.</p>
<p>Fastify를 Lambda로 올릴 때는 <code>aws-lambda-fastify</code>를 통해 handler를 만들고 있었기 때문에,<br />이 옵션을 래퍼에 직접 넘겨서 동작을 바꿔줄 수 있었다.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> proxy = awsLambdaFastify(app, {
  <span class="hljs-attr">callbackWaitsForEmptyEventLoop</span>: <span class="hljs-literal">false</span>,
});

<span class="hljs-built_in">module</span>.exports.handler = proxy;
</code></pre>
<p>이렇게 설정한 이후에는 로직이 끝난 시점과 클라이언트가 응답 받는 시점이 거의 일치하게 바뀌었고,<br />10초 가까이 붙잡혀 있던 지연 현상도 없어졌다.</p>
<h3 id="heading-2">2) 유휴 연결시간 줄이기</h3>
<p>유휴 연결시간 자체를 줄여서 이벤트 루프에 idle 연결이 더 짧게 남도록 조정하는 방법도 시도했봤다.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> { Pool } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'pg'</span>);

<span class="hljs-keyword">const</span> pool = <span class="hljs-keyword">new</span> Pool({
  <span class="hljs-attr">idleTimeoutMillis</span>: <span class="hljs-number">1000</span>, <span class="hljs-comment">// 1초로 설정</span>
});
</code></pre>
<p>기본값(10초)으로 설정했을 때보다 확실히 응답 시간이 줄어들긴 했지만,<br />여전히 응답이 준비된 이후에 잠깐의 대기 시간이 생기는 문제가 있었다.</p>
<p>그렇다고 idleTimeoutMillis를 너무 짧게 설정하면,<br />비용이 큰 데이터베이스 연결을 Lambda가 실행될 때마다 계속 새로 시도해야 하는 문제가 생긴다.</p>
<p>결국,</p>
<ul>
<li><p><code>context.callbackWaitsForEmptyEventLoop = false</code>로 응답을 먼저 보내도록 만들고</p>
</li>
<li><p><code>idleTimeoutMillis</code>는 사용 패턴에 맞는 적절한 값으로 조정하는 것</p>
</li>
</ul>
<p>이 현실적인 타협점이라고 판단했다.</p>
<h2 id="heading-6rkw66gg">결론</h2>
<p>삽질했던 시간보다 훨씬 간단하게 해결한 문제였다.</p>
<p>이번에 겪은 일은, 결국 내가 사용하고 있는 플랫폼에 대한 이해 부족에서 비롯된 문제였다. 요즘은 AWS 서비스들이 워낙 잘 되어 있다 보니 “설마 인프라 쪽에 원인이 있겠어?”라는 생각으로, 코드만 붙잡고 있었던 것도 한몫했다. 앞으로는 코드에만 집중하기보다, 런타임·플랫폼·네트워크까지 포함해서 문제를 좀 더 다각도로 바라보려 한다.</p>
<p>이번에 크게 느낀 점은 두 가지다.</p>
<ul>
<li><p><strong>첫째, 라이브러리의 겉 API만 보지 말고, 중요한 경계(Lambda handler, DB 커넥션, 이벤트 루프 등)를 다루는 부분의 구현은 한 번쯤 직접 뜯어보자</strong>는 것이다. 이번에도 aws-lambda-fastify 내부가 callback 기반이라는 사실을 알고 나서야 퍼즐이 맞춰졌다.</p>
</li>
<li><p><strong>둘째, 서버리스를 사용할 때는 기본 동작과 내가 적용한 설정이 어떻게 맞물리는지 반드시 이해</strong>해야 한다. Lambda의 실행 모델, handler 방식(async/await vs callback), Connection Pool의 동작 방식, idleTimeoutMillis, callbackWaitsForEmptyEventLoop 같은 설정은 실제 성능과 응답 지연에 직접적인 영향을 준다. “서버리스니까 알아서 처리해주겠지”라는 기대보다는, 내 코드가 서버리스 환경에서 어떻게 실행되는지 명확히 이해하고 사용하는 것이 중요하다는 점을 다시 한 번 느꼈다.</p>
</li>
</ul>
<p>이런 것들을 한 번 겪어두면, 다음 비슷한 장애가 왔을 때 “코드만 들여다보는” 시간은 조금 줄어들 거라고 기대하고 있다.</p>
<h2 id="heading-7lc46rog7j6q66om">참고자료</h2>
<ul>
<li><p><a target="_blank" href="https://jojoldu.tistory.com/634">NodeJS 와 PostgreSQL Connection Pool</a></p>
</li>
<li><p><a target="_blank" href="https://stackoverflow.com/questions/42605093/aws-lambda-rds-connection-timeout">AWS Lambda RDS connection timeout</a></p>
</li>
<li><p><a target="_blank" href="https://gist.github.com/streamich/6175853840fb5209388405910c6cc04b">https://gist.github.com/streamich/6175853840fb5209388405910c6cc04b</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/nodejs-context.html">https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/nodejs-context.html</a></p>
</li>
<li><p><a target="_blank" href="https://dev.to/dvddpl/event-loops-and-idle-connections-why-is-my-lambda-not-returning-and-then-timing-out-2oo7">https://dev.to/dvddpl/event-loops-and-idle-connections-why-is-my-lambda-not-returning-and-then-timing-out-2oo7</a></p>
</li>
<li><p><a target="_blank" href="https://en.wikipedia.org/wiki/Connection_pool">https://en.wikipedia.org/wiki/Connection_pool</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[불안 속에서 성장하기: 주니어의 커리어 고민과 인사이트]]></title><description><![CDATA[주니어로서 회사 생활 중 느낀 불안감과 성장에 대한 고민을 바탕으로, 주변에서 얻은 조언과 인사이트를 정리하여 공유하고자 합니다. 이 글이 여러분의 고민을 조금이라도 덜어줬으면 좋겠습니다.
들어가며
회사에 입사한 지 8개월이 지나면서, 업무에 대한 익숙함과 함께 막연한 불안감이 찾아왔다. 특히, 반복되는 이슈 처리와 해결 과정에서 '이렇게 계속해도 내 커리어에 도움이 될까?'라는 의문이 들었다. 이러한 고민을 해소하고자 시니어 개발자이신 개발전...]]></description><link>https://blog.aqudi.me/growing-through-uncertainty-junior-career-insights</link><guid isPermaLink="true">https://blog.aqudi.me/growing-through-uncertainty-junior-career-insights</guid><category><![CDATA[커리어]]></category><category><![CDATA[고민]]></category><category><![CDATA[주니어]]></category><category><![CDATA[인사이트]]></category><category><![CDATA[개발자]]></category><category><![CDATA[주니어 개발자]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 10 Nov 2024 14:36:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731250033609/b31500a0-c835-4c97-b899-c5806b09391b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>주니어로서 회사 생활 중 느낀 불안감과 성장에 대한 고민을 바탕으로, 주변에서 얻은 조언과 인사이트를 정리하여 공유하고자 합니다. 이 글이 여러분의 고민을 조금이라도 덜어줬으면 좋겠습니다.</p>
<h2 id="heading-65ok7ja06rca66mw">들어가며</h2>
<p>회사에 입사한 지 8개월이 지나면서, 업무에 대한 익숙함과 함께 막연한 불안감이 찾아왔다. 특히, 반복되는 이슈 처리와 해결 과정에서 '이렇게 계속해도 내 커리어에 도움이 될까?'라는 의문이 들었다. 이러한 고민을 해소하고자 시니어 개발자이신 개발전략실 이사님과 개발자는 아니지만 함께 일하고 있는 팀장님께 개인적인 성장과 회사일의 관계, 회사 생활 잘하는 법 등 조언을 구했다.</p>
<p>두 분 모두 친절하게 답변해주셨고 어느 정도 실마리를 얻은 것 같은 기분이 들었다. 아직은 더 생각해봐야 할 것들이 많아서 글로 정리하면서 생각을 좀 정리하려고 한다.</p>
<h2 id="heading-3">회사가 기대하는 내 연봉의 3배 가치, 나는 충분히 하고 있을까?</h2>
<p>이사님과의 대화에서 가장 인상 깊었던 부분은 "회사가 굴러가기 위해서는 직원이 받는 연봉의 2배, 3배 이상의 가치를 만들어줘야 한다"는 말이었다. 지금까지 연봉에 대한 불만만 품었던 나는 이 말을 듣고 큰 충격을 받았다. 한 번도 회사가 나를 어떤 시선으로 바라보고 어떤 기대를 하고 있는지는 진지하게 생각해본 적이 없었던 것 같다. <strong>나는 과연 회사가 기대하는 만큼의 성과를 내고 있는 걸까?</strong> 스스로에게 묻게 된 순간이었다.</p>
<p>또한, 이사님은 생산성의 핵심은 <strong>짧은 시간 안에 팀이 추구하는 방향으로 결과를 만들어내는 것</strong>이라고 설명해주셨다. 단순히 오래 일한다고 해서 생산성이 높아지는 게 아니라는 것이다. 오히려, 팀의 방향성과 맞지 않는 일은 아무리 많이 해도 결국 의미가 없다고 강조하셨다.</p>
<p>이 대화를 통해 깨달은 점은 내가 회사와 고용주가 기대하는 가치를 제대로 이해하지 않았다는 것이 불안감의 원인 중 하나였다는 것이다. 앞으로는 불만을 느끼기만 할 것이 아니라, 내가 회사에 어떤 가치를 제공할 수 있을지, 그리고 그 가치를 더 높이기 위해 무엇을 할 수 있을지 먼저 고민해 보자고 마음먹었다.</p>
<h2 id="heading-7jwi7ko87zwy7keaiounkoqzocdsl4xrrlqg67ku7jye66w8iouemoucmoutpoupscdtmzxsnqxtlzjqula">안주하지 말고 업무 범위를 넘나들며 확장하기</h2>
<p>이사님과 대화를 통해서 깨달은 다른 교훈 중 하나는 <strong>업무 범위를 넘나드는 자세</strong>였다. 연봉대비 효율성을 높이기 위해서는 맡은 일에만 안주하지 말고 업무범위를 넘나들며 내 범위를 확장하는 것이 필요하다고 했다. 물론 무조건적으로 다른 영역의 일을 침범하라는 의미가 아니라, 기회가 왔을 때 적극적으로 참여하라는 것이었다.</p>
<p>팀장님과의 1대1 미팅에서도 비슷한 이야기가 나왔다. 내가 잘하고 있는지에 대해 고민을 털어놓자, 팀장님은 "잘하고 있는지의 기준은 자신이 정해야 한다"고 말씀하셨다. 하지만 덧붙여, <strong>자신의 업무 범위를 넘어서 팀의 공백을 메우고, 커뮤니케이션 비용을 줄이는 데 기여하는 사람이 일을 잘하는 사람</strong>이라고 하셨다. 예를 들어, 어드민 도구 기획에서 미리 사용성과 필요한 기능을 고려해 설계한 덕분에 팀의 커뮤니케이션 비용을 줄일 수 있었던 사례를 언급해주셨다. 기획자가 맡았어야 할 업무를 내가 먼저 해결했기 때문에 더 효율적인 작업이 가능했던 것이다.</p>
<p>또한, 업무 범위를 확장하는 것이 중요한 이유는 조직이 성장할수록 더 많은 빈 공간이 생기기 때문이다. 특히 스타트업에서는 인원이 적을 때 한 사람이 여러 역할을 맡지만, 회사가 커지면서 각자의 역할이 분명해질수록 아무도 맡지 않는 일이 생겨난다. <strong>바운더리를 넘나드는 사람</strong>은 이 빈 공간을 자발적으로 메우고, 회사가 기대하는 이상을 충족하는 중요한 역할을 한다. 이러한 노력은 단순한 성과 이상의 가치를 만들어낸다. 바운더리를 확장하며 노력하는 사람은 동료들에게 신뢰받고 함께 일하고 싶은 동료로 기억된다. <strong>결국, 맡은 일에만 머무르지 않고 확장하고자 할 때, 나의 성장과 함께 동료들의 신뢰를 얻을 수 있는 것이다.</strong></p>
<h2 id="heading-7j2466el7j2yioykkeyaloyeseqzvcdsnbtrpbwg7zqo6ro87kcb7jy866gcio2znoyaqe2vmouklcdrsknrspu">인맥의 중요성과 이를 효과적으로 활용하는 방법</h2>
<p>이사님과 팀장님과의 대화를 통해 얻은 또 하나의 중요한 교훈은 <strong>인맥의 힘</strong>이었다. 시간이 지나면서 커리어가 깊어질수록, 우리가 정말로 잘하는 분야는 제한적일 수밖에 없다. 이때 내가 쌓아온 인맥은 그 한계를 뛰어넘어 줄 중요한 자산이 된다. 인맥은 단순한 인간관계를 넘어, 나의 성장을 도와주는 중요한 정보와 기회의 창구가 될 수 있다.</p>
<p>예전에 회사의 주니어, 중니어, 시니어 분들에게 "처음 보는 문제를 맞닥뜨렸을 때 어떻게 해결하시나요?"라고 물어본 적이 있었다. 시니어 분의 대답은 예상 밖이었다. 그는 "트위터나 페이스북에 있는 제 지인들에게 질문을 던질 것 같다"고 했다. 책이나 인터넷에 있는 일반적인 정보보다, 그 분야에서 오랜 시간 경험을 쌓아온 전문가의 조언이 훨씬 더 유용할 수 있다는 것이다. 이 답변은 내게 <strong>인맥이 문제 해결의 비밀 무기가 될 수 있다</strong>는 사실을 깨닫게 해주었다.</p>
<p>하지만 인맥을 효과적으로 활용하기 위해서는 몇 가지 팁이 필요하다. 먼저, <strong>전문가에게 접근할 때는 구체적이고 명확한 질문을 준비하는 것이 중요</strong>하다. 예를 들어, "이 기능을 구현할 때 성능 최적화 측면에서 어떤 방법이 가장 효율적일까요?"와 같이, 문제 상황과 필요한 조언을 분명히 설명하는 것이다. 이렇게 하면 상대방이 더 쉽게 상황을 이해하고, 정확한 조언을 줄 수 있다.</p>
<p>또한, 전문가를 멀리서만 찾을 필요는 없다. <strong>회사 내에도 뛰어난 전문가들이 많다.</strong> 최근에 쿠팡에서 정산을 오랫동안 맡아온 분이 우리 회사에 합류했는데, 그분께 커피 한 잔을 제안하며 자연스럽게 정산에 관한 궁금증을 물어볼 수 있다. 회사 안에서도 용기를 내어 대화의 문을 열면, 생각보다 많은 인사이트를 얻을 수 있다.</p>
<p>지금은 주변에 주니어들이 많아 인맥의 중요성을 크게 실감하지 못할 수도 있다. 하지만 시간이 지나면서 각자의 커리어에서 중요한 자리에 오르게 될 것이다. <strong>나와 비슷한 위치에 있는 동료들이 시간이 흐르며 함께 성장하는 모습은 마치 복리효과가 쌓이는 것처럼, 그 가치가 점점 커질 것이다.</strong> 앞으로 이 복리효과를 제대로 누릴 수 있도록 더 많은 사람들과 의미 있는 관계를 맺어 나가야겠다고 생각했다.</p>
<h2 id="heading-66ei66y066as">마무리</h2>
<p>이 글을 쓰면서 회사 생활에서 느꼈던 불안감과 내 성장과 회사일의 관계에 대해 다시 한 번 고민해볼 수 있었다. 그리고 주변에 나에게 도움을 줄 수 있는 분들이 많고 언제든지 내가 용기내어 손을 내민다면 기꺼이 도움을 주실 분들이라는 것도 느낄 수 있었다.</p>
<p>또한, 내게 주어진 것에 불평, 불만만 할 게 아니라 내가 줄 수 있는 가치가 무엇인지에 대해서도 고민하고 어떻게 하면 더 큰 가치를 줄 수 있는지에 대해서도 고민을 하게 됐다. 그리고 이 가치를 높이는 과정이 결국에는 나의 성장과 직결되는 것이라는 것도 새삼 알게 된 것 같다.</p>
<p>그리고 나와 함께 일하고 있는 동료들도 생각보다 훨씬 훨씬 큰 자산이라는 것도 깨달았다. 동료들과 함께 일하게 된 게 너무나 기쁜 일이고 이미 나는 많은 도움을 받고 있다는 것을 새삼 느꼈다. 앞으로 내가 그들에게 도움이 되는 존재가 될 수 있도록 열심히 노력해야겠다!</p>
<p>아직은 조언들을 구체적으로 어떻게 실천해야 할지는 아직은 좀 막막한 것 같다. 그래서 일단은 아래와 같은 2가지 액션 아이템을 먼저 시도해보려고 한다. 아자 아자 화이팅!</p>
<ol>
<li><p><strong>월간 업무 돌아보기</strong>: 매달 말, 내가 맡았던 프로젝트나 업무를 돌아보며 내 업무 범위를 어떻게 확장했는지와 더 나아갈 수 있는 영역이 있는지 정리해보기</p>
</li>
<li><p><strong>월 1회 사내 커피챗</strong>: 회사 내에서 관심 분야의 시니어나 다른 부서의 전문가 분을 한 명 정해 월 1회 커피를 마시며 대화하는 시간 가져보기</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[2024년 4분기 나의 다짐]]></title><description><![CDATA[방향성 있는 성장을 위한 새로운 시작
2024년 마지막 분기를 맞이하며, 지난 시간을 돌아보고 앞으로의 성장을 위해 새로운 다짐을 하고자 한다. 지금까지 큰 목표없이 눈 앞에 놓인 일을 처리하기에 바빴는데 이렇게 살다보니 하루는 매우 알차게 보낸 것 같은데 성장을 하고 있다는 확신도 없고 앞으로의 커리어가 불안하게 느껴지는 것 같다. 그래서 이번 다짐글을 시작으로 달성 가능한 목표와 함께 방향성 있게 성장하기를 기대한다.
지금까지 가장 아쉬웠던...]]></description><link>https://blog.aqudi.me/2024-q4-growth-commitment</link><guid isPermaLink="true">https://blog.aqudi.me/2024-q4-growth-commitment</guid><category><![CDATA[개인성장]]></category><category><![CDATA[글쓰기습관]]></category><category><![CDATA[자기계발]]></category><category><![CDATA[2024]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Wed, 09 Oct 2024 13:54:21 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-67cp7zal7isxioyeiouklcdshlhsnqxsnyqg7jye7zwcioydiouhnoyatcdsi5zsnpe">방향성 있는 성장을 위한 새로운 시작</h2>
<p>2024년 마지막 분기를 맞이하며, 지난 시간을 돌아보고 앞으로의 성장을 위해 새로운 다짐을 하고자 한다. 지금까지 큰 목표없이 눈 앞에 놓인 일을 처리하기에 바빴는데 이렇게 살다보니 하루는 매우 알차게 보낸 것 같은데 성장을 하고 있다는 확신도 없고 앞으로의 커리어가 불안하게 느껴지는 것 같다. 그래서 이번 다짐글을 시작으로 달성 가능한 목표와 함께 방향성 있게 성장하기를 기대한다.</p>
<p>지금까지 가장 아쉬웠던 부분은 목표 외에도 기록과 공유의 부족함이다. 이번 년도만해도 외주 마무리, 취직 준비, 취직, 병역 특례 시작 등 다양한 일들이 있었고 취직 후 회사에서도 팀 이동, 신규 프로젝트 런칭 등 다양한 경험을 했다. 그런데 기록을 하지 않으니 이런 경험들이 스쳐지나갈 뿐이지 앞으로 두고 두고 활용할 수 있는 내 자산으로 남는다는 느낌이 없었다. 특히, 그 당시 했던 생각들은 경험과 다르게 구체적이지 않으니 나중에 생각하려고 하면 아예 생각이 안나고 '좋았다', '힘들었다' 정도로만 남아 아쉬웠다.</p>
<p>더불어, 최근 회사 동료들과 스터디를 하거나 내가 속해 있는 외부 커뮤니티의 사람들과 이야기를 나누면서 다들 비슷한 고민을 하고 있다는 것을 깨달았다. 이를 통해 왜 그동안 내 경험이나 고민을 공유하지 않았을까 하는 아쉬움이 들었다. 내 경험이나 고민을 공유했다면 비슷한 고민을 하고 있을 다른 사람들에게 도움이 됐을 수도, 내가 도움을 받았을 수도 있었을 텐데 그 기회를 놓친 것 같아 아쉬움이 크다.</p>
<p>이번 하반기에는 이런 아쉬웠던 점들을 반성하며, 나는 이제 의식적으로 나의 경험을 기록하고, 동료들과 적극적으로 소통하며 함께 성장해나가고자 한다. 어려움이 있더라도 이 다짐을 꾸준히 실천하여 남은 2024년을 뜻깊게 마무리하고 더 나은 2025년을 맞이할 수 있기를 바란다.</p>
<h2 id="heading-1">1. 기록의 힘을 믿고, 생활 속에서 실천하기</h2>
<p>기록을 습관화하자는 목표 달성을 돕기 위해 이번에 글또 10기에 지원해서 활동을 시작했다. 지금까지 챌린지나 커뮤니티 활동 같은 것들을 가볍게 여기고는 했는데 이번에는 이번 다짐을 시작으로 나 자신과의 약속을 만들어 지켜나갈 것이다.</p>
<p>글을 통해서 내 생각과 경험이 한 순간 스쳐지나가는 것들이 아니라 두고 두고 볼 수 있는 자산으로 소중히 대할 것이다. 그리고 이번 글쓰기 습관화 과정은 계속 회고하며 내게 맞는 방법으로 바꾸어 나갈 것이다. 다음은 그를 위한 구체적인 계획이다:</p>
<ol>
<li><strong>글쓰기 습관 형성</strong>: 최소 2주에 한 번 글 작성하여 글쓰기 습관 만들어 갈 것이다. 이를 위해:<ul>
<li>격주마다 월요일에 글 주제를 정하고 대략적인 초안을 작성한다.</li>
<li>평일 동안에는 매일 해당 주제에 대한 자료를 찾거나 생각을 정리한다.</li>
<li>주말에는 최소 2시간은 할애해서 자료들을 정리하고 글쓰는 시간을 가진다.</li>
</ul>
</li>
<li><strong>좋은 글, 영상 보기</strong>: 좋은 글을 쓰기 위해서는 좋은 인풋을 많이 만들어야 한다. 이를 위해:<ul>
<li>매주 1개씩 2024 토스 컨퍼런스 백엔드 파트 영상을 보고 정리한다.</li>
</ul>
</li>
<li><strong>글쓰기 스킬 향상</strong>: 성장을 위해서는 주기적인 회고가 필요하다. 이를 위해:<ul>
<li>매월 말일, 한 달 동안 작성한 글을 돌아보며 개선점을 찾아 기록한다.</li>
<li>개선점 중 하나를 고칠 액션 아이템을 선정하고 다음 달에 적용한다.</li>
</ul>
</li>
</ol>
<p>위의 노력을 통해 꾸준히 글을 써서 이번 년도가 끝날 때까지는 6개 이상의 글을 작성할 것이고 나만의 글쓰기 프레임워크를 만들 것이다.</p>
<h2 id="heading-2">2. 동료들과 소통하며 함께 성장하기</h2>
<p>회사 동료들이나 글또, 메모어 등 내가 속해있는 여러 커뮤니티에서 나와 비슷한 경험을 가진 사람들이 많을 것이다. 적극적으로 내 고민이나 경험을 공유하여 서로 좋은 영향을 주고 받을 수 있는 사람이 되기 위해 노력할 것이다. 이를 위한 구체적인 계획은 다음과 같다:</p>
<ol>
<li><strong>적극적인 커뮤니티 참여</strong><ul>
<li>한 달에 2회 이상 커뮤니티, 회사 동료들과 커피챗, 스터디 등 네트워킹 활동에 참여한다.</li>
</ul>
</li>
<li><strong>경험과 고민의 적극적 공유</strong><ul>
<li>회사 TIL 채널과 글또 글읽었또 채널에 고민한 내용, 배운 내용을 주 1회 이상 공유한다.</li>
<li>주 1회 다른 사람의 고민 글을 읽고 내 경험을 바탕으로 고민해본 후 댓글을 단다.</li>
</ul>
</li>
</ol>
<p>위와 같은 노력을 통해서 고민하고 배운 내용을 공유할 창구를 늘리고 같이 고민할 수 있는 사람을 늘려나갈 것이다. 개인적으로는 만나는 여러 사람들 사이에서 내가 믿고 존경할 수 있는 분을 만났으면 하는 바람도 있는데 꼭 그런 만남을 이뤘으면 좋겠다.</p>
<h2 id="heading-66ei66y066as">마무리</h2>
<p>이번 2024년 4분기 다짐을 통해 나는 두 가지 큰 목표를 세웠다. 첫째, 기록의 힘을 믿고 생활 속에서 실천하기, 둘째, 동료들과 소통하며 함께 성장하기이다. 이 목표들은 단순히 이번 분기만의 목표가 아닌, 앞으로의 내 삶과 커리어를 더욱 풍성하게 만들어줄 중요한 습관이 될 것이라고 믿는다.</p>
<p>이 다짐을 시작으로, 나는 더 이상 눈 앞의 일만 처리하는 데 급급한 삶이 아닌, 방향성을 가지고 꾸준히 성장해나가는 삶을 살고자 한다. 어려움이 있더라도 포기하지 않고, 매일 조금씩이라도 앞으로 나아가는 노력을 할 것이다. 이 여정을 함께할 커뮤니티와 회사 동료들에게 미리 감사의 마음을 전하며, 이 다짐을 시작을 더 나은 개발자, 더 나은 사람으로 성장할 것을 약속한다.</p>
]]></content:encoded></item><item><title><![CDATA[삶의 지도, 지금까지의 삶 돌아보기]]></title><description><![CDATA[글또 10기에 지원하면서 제 인생에 대해서 전체적으로 돌아보는 시간을 가져봤습니다. 처음에는 막막했는데 적다보니 생각보다 적을 이야기가 많고 제가 훨씬 입체적인 사람처럼 느껴지네요! 혹시나 이 글을 읽게 되신다면 한 번 자신의 삶의 지도를 작성해보셔도 좋을 것 같습니다!

[만들기를 좋아하던 성장기의 나]
잘 기억이 나지 않는 유년기의 모습 중 기억에 남는 것은 무언가를 만드는 걸 참 좋아한다는 것입니다. 블록이나 레고, 종이접기같이 뭔가 만들...]]></description><link>https://blog.aqudi.me/map-of-life</link><guid isPermaLink="true">https://blog.aqudi.me/map-of-life</guid><category><![CDATA[Retrospective]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sat, 21 Sep 2024 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742731361645/19f87f5f-c1a8-4d70-8527-bfb265756d49.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>글또 10기에 지원하면서 제 인생에 대해서 전체적으로 돌아보는 시간을 가져봤습니다. 처음에는 막막했는데 적다보니 생각보다 적을 이야기가 많고 제가 훨씬 입체적인 사람처럼 느껴지네요! 혹시나 이 글을 읽게 되신다면 한 번 자신의 삶의 지도를 작성해보셔도 좋을 것 같습니다!</p>
</blockquote>
<h3 id="heading-kipcwunjoutpoq4soulvcdsoovslyttlzjrjzgg7isx7j6l6riw7j2yioucmfxdkio"><strong>[만들기를 좋아하던 성장기의 나]</strong></h3>
<p>잘 기억이 나지 않는 유년기의 모습 중 기억에 남는 것은 <strong>무언가를 만드는 걸 참 좋아한다는 것</strong>입니다. 블록이나 레고, 종이접기같이 뭔가 만들어내는 것을 좋아했고 언젠가는 초록매실을 만들겠다고 집 근처 매실나무에서 매실을 따서 칼질을 하다가 손이 다쳤던 것도 생각이 나네요. 이후 초, 중, 고를 나오면서도 변하지 않았던 것 같습니다. 천연효모를 만들겠다고 포도를 옷장에 넣고 발효시키던 모습과 나무를 사서 톱질하다가 손바닥을 꼬맸던 적도 있었던 것 같습니다. 결과물의 퀄리티가 그렇게 좋지 못했던 것 같은데 그래도 나름 열심히 뭔가를 만들려고 노력을 많이 했던 것 같습니다. 그나마 이때 시도했던 여러가지 중에 아직까지 유용하게 쓰고 있는 기술은 요리인 것 같네요 ;D</p>
<h3 id="heading-kipcw2uhouhnoq3uouemouwjeqzvcdssqsg66em64koxf0qkg"><strong>[프로그래밍과 첫 만남]</strong></h3>
<p>제가 프로그래밍을 처음 접한 건 중학교 1학년때 과학선생님이 임베디드 소프트웨어 경진대회에 데리고 나가면서였습니다. 사실 그때는 너무 못해서 블록코딩으로 주어진 로봇을 앞, 뒤, 좌, 우로 움직여서 미로 통과하는 미션만 겨우 해내고 나머지 미션들은 거의 포기했었습니다. 🤣 그럼에도 이 경험이 프로그래밍하면 가장 먼저 생각나는 이유는 이때부터 컴퓨터가 스타크래프트, 마인크래프트 머신이 아니라 생산적인 도구가 될 수 있다는 걸 알려줬기 때문입니다.</p>
<p>이 경험 이후로 프로그래밍에 대한 관심은 빠르게 늘어가서 중학교 3학년때 아두이노로 도어락을 만들어 무려 미래창조과학부 장관님과 악수를 했습니다! (수상한 건 아니었고 박람회 같은 곳에 전시했던 걸로 기억납니다.)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727014507489/a4513e44-914d-4b11-90d5-adaa7660f6e1.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727014490613/d7ed46ce-f0e1-4a46-bc05-8845793506bf.png" alt class="image--center mx-auto" /></p>
<p>이런 일련의 사건들을 거치면서 프로그래밍에 대한 흥미는 점점 커져갔고 언제부터인가 저는 제가 경험한 프로그래밍의 즐거움과 가능성을 다른 사람들과 나누고 싶어졌습니다. 특히 처음에는 프로그래밍을 어렵게 생각하는 사람들에게, 이것이 얼마나 재미있고 유용한 도구인지 보여주고 싶었습니다.</p>
<h3 id="heading-kipcwucmoydmcdshlhsnqug6ro17iudoidsu6trrqtri4jti7drpbwg7ya17zwcioyngoylnsdqs7xsnkdsmyag7isx7j6lxf0qkg"><strong>[나의 성장 공식: 커뮤니티를 통한 지식 공유와 성장]</strong></h3>
<p>대학교에 진학한 후에는 프로그래밍 교육이 제 주된 관심사였습니다. 1학년때부터 지방의 중학교에 방문해서 5~6일 정도의 프로그래밍 교육을 기획해서 방학동안 캠프를 열거나 일산의 초등학교에서 주말마다 몇 주에 걸쳐서 교육하는 활동도 기획해서 운영했었고, 대학교 2학년때부터는 멋쟁이사자처럼이라는 비전공자를 위한 프로그래밍 교육 동아리에 들어가 회장, 운영진 등으로 활동하면서 웹 개발 교육 프로그램을 기획하고 실제로 교육도 진행하고 해커톤, 아이디어톤 등 다양한 행사를 개최했습니다.</p>
<p>이렇게 프로그래밍의 매력을 널리 알리고자 하는 열정으로 시작한 활동들은 예상치 못한 방식으로 제 자신의 성장으로 이어졌습니다. 저는 이 과정에서 중요한 깨달음을 얻었습니다. 바로 <strong>'나눔을 통한 성장'이라는 성장 공식</strong>을 발견한 것입니다.</p>
<p>처음에는 단순히 제가 알고 있는 것을 다른 사람들에게 전달하는 것이 목표였습니다. 하지만 교육 활동을 하면 할수록, 제가 오히려 더 많이 배우고 성장하고 있다는 것을 깨달았습니다. 질문에 답하기 위해 더 깊이 공부하게 되고, 복잡한 개념을 쉽게 설명하기 위해 노력하는 과정에서 제 지식이 더욱 단단해졌습니다. 그리고 이런 과정 중에 수백명 규모의 수강생이 있는 강의를 찍게 되는 등 예상치 못한 기회들이 들어오기도 했습니다.</p>
<p>이후에도 플러터라는 크로스 플랫폼 프레임워크에 관심이 생겨서 공부를 할 때도 이 성장 공식이 잘 들어맞았던 것 같습니다. 오픈 채팅방에서 열심히 활동하면서 올라오는 질문들은 공부해서 답변하고 모르는 건 같이 고민했고, 그 안에서 스터디 그룹을 만들어 같이 공부하기도 했습니다. 그리고 여기서도 좋은 기회를 만나 졸업 전에 서버, IOT기기, 애플리케이션까지 모두 외주로 개발해보는 경험도 쌓을 수 있었습니다.</p>
<h3 id="heading-kipcwyasoulue2dle2dlsdrjidtlznsm5bcxsoq"><strong>[우당탕탕 대학원]</strong></h3>
<p>처음 대학원 진학을 결심했던 것은 웹 개발만으로는 경쟁력을 얻기 어려울 것 같다는 생각때문이었습니다. 그 당시 우후죽순 생겨나는 부트캠프들에 이대로 가만히 있어서는 뒤쳐질 것 같다는 생각에 대학교 2학년 2학기 말부터 학부 연구생 생활을 하다가 한 학기 일찍 졸업하고 대학원으로 진학을 했었습니다.</p>
<p>처음에는 연구실 분위기가 너무 좋고 연구 주제도 재밌어서 좋았지만 중간에 현대자동차와 함께 하는 프로젝트를 메인으로 수행하게 되면서부터 연구실 내에서 비슷한 주제의 연구를 한 사람도 없어 도움받기가 어렵고 방향성도 계속 흔들리면서 연구에 대한 흥미가 많이 떨어졌었습니다. 그러면서 내가 진짜 하고 싶은게 무엇인지에 대한 고민을 많이 했던 것 같습니다. 다행히 졸업은 무사히 했지만 졸업 이후에도 앞으로의 진로에 대한 고민이 정말 많은 시기였습니다.</p>
<p>그래도 고민이 그렇게 오래가지는 않았던 것 같습니다. 졸업 이후, 플러터 커뮤니티에서 알게 된 인연들을 통해 여러 외주를 받아 일하고 주변 지인들과 이야기도 나눠보고 메모어라는 회고 모임에서 활동하면서 '나'에 대해서 계속 고민하다보니 결국 내가 원하던 건 연구원, 개발자 같은 직업이 아니라는 걸 깨닫게 됐습니다.</p>
<h3 id="heading-kipcw2yhoyercdrgpjsnzgg66qp7zgcxf0qkg"><strong>[현재 나의 목표]</strong></h3>
<p>'내 서비스 개발'이 현재 제가 목표로 삼고 있는 것입니다.</p>
<p>이 목표는 단순히 기술적인 도전을 넘어서, 의미 있는 가치를 창출할 수 있는 일을 하고 싶다는 생각에서 출발했습니다. 외주 프로젝트를 진행하면서 서비스 개발의 매력에 빠져들었고, 사용자들이 실제로 사용할 수 있는 제품을 만들고 그것을 사용하는 모습을 보며 느끼는 성취감이 무엇보다 컸습니다. 이를 통해 제 제품을 널리 퍼트리고 싶다는 열망을 갖게 되었습니다.</p>
<p>서비스 개발은 단순히 코드를 작성하는 것 이상의 과정입니다. 사용자 경험(UX) 개선, 문제 해결, 프로젝트 관리, 마케팅 등 다양한 요소들이 결합되어야 합니다. 이는 제가 지금까지 경험해보지 못했던 많은 것들을 요구합니다.</p>
<p>그래서 저는 다양한 관련 업무들을 경험할 수 있는 회사에서 백엔드 개발자로 일하며, 미래에 있을 내 서비스 개발을 위한 역량을 키우기 위해 노력하고 있습니다. 현재 회사에서 개발하고 운영하는 서비스도 매우 재미있지만, 내 서비스를 출시할 날을 고대하고 있습니다.</p>
<p>그리고 그 날이 더 빠르게 다가올 수 있도록 사이드 프로젝트를 시작했습니다. 작게나마 조금씩 개발을 진행하며, 길게 잡아도 이번년도에는 내 서비스를 출시하는 것을 목표로 하고 있습니다. 앞으로도 꾸준한 노력과 성장을 통해 이 목표를 달성하기 위해 최선을 다할 것입니다.</p>
]]></content:encoded></item><item><title><![CDATA[[회고] 2023년 뭐하고 살았나]]></title><description><![CDATA[좀 많이 늦었지만 2023년 회고를 간단하게 작성해봤다. 이전에 작성하던 내용을 마무리하지 못하고 있었는데 더 이상 미루다가는 내년에 올릴 것 같아서 후다닥 마무리했다. (지금은 달라진 내용들도 있지만 아카이브라고 생각하고 일단 그대로 작성해서 올린다.)
🎓대학원 졸업
2021년 9월부터 시작했던 대학원 생활을 2023년 8월에 공식적으로 마무리 지었다. 비공식적으로는 몇 가지 일이 남아서 이후에도 마무리를 지은 게 좀 있었지만 일단 정상적으...]]></description><link>https://blog.aqudi.me/2023</link><guid isPermaLink="true">https://blog.aqudi.me/2023</guid><category><![CDATA[Retrospective]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Thu, 29 Feb 2024 15:00:00 GMT</pubDate><content:encoded><![CDATA[<p>좀 많이 늦었지만 2023년 회고를 간단하게 작성해봤다. 이전에 작성하던 내용을 마무리하지 못하고 있었는데 더 이상 미루다가는 내년에 올릴 것 같아서 후다닥 마무리했다. (지금은 달라진 내용들도 있지만 아카이브라고 생각하고 일단 그대로 작성해서 올린다.)</p>
<h2 id="heading-8jokumgo2vmeybkcdsobjsl4u">🎓대학원 졸업</h2>
<p>2021년 9월부터 시작했던 대학원 생활을 2023년 8월에 공식적으로 마무리 지었다. 비공식적으로는 몇 가지 일이 남아서 이후에도 마무리를 지은 게 좀 있었지만 일단 정상적으로 졸업을 했다는 것에 안도감이 크게 몰려왔다. 나의 경우에는 산학협력 과제와 논문 작성을 동시에 수행해야 하는 상황이어서 할 일이 많기도 했고 연구에 제약이나 불가피한 상황도 꽤 있어서 힘들고 졸업에 대한 불안감이 있었던 것 같다.</p>
<p>그래도 연구실 동료들의 아낌없는 정서적 지원과 조언을 바탕으로 정신적으로 힘든 시기도 이겨낼 수 있었고 이를 바탕으로 연구도 성공적으로 마칠 수 있었던 것 같다. 연구실 동료들은 다시 돌아봐도 대학원 가기를 잘한 이유 1번으로 자신 있게 말할 수 있다. 졸업 이후에도 계속 연락을 주고 받고 정기적으로 모이기도 하고 있다. 다들 너무 고맙고 잘 됐으면 하는 바람이 크다.</p>
<p>또 다른 잘했다고 생각하는 이유는 연구하는 방법을 배운 것과 과제를 수행하면서 데이터 수집부터 시작한 일련의 과정을 경험해 봤다는 것이다. 특히 과제를 하면서 현업에 계신 분들과 같이 소통할 기회를 얻고 그분들의 피드백을 받을 수 있었기 때문에 더 좋은 경험이 된 것 같다.</p>
<p>만약 대학원을 고민하는 사람이 있다면 "좋아하는 주제가 있다면 좋은 교수님과 적절한 여건의 연구실을 가서 2년간 행복하게 보낼 수 있는 곳이야"라고 이야기를 해주고 싶다. 다시 말하면 좋아하는 주제가 없고 연구에 집중할 수 없는 환경이라면 조금 힘들 가능성이 높은 곳이다. 미리 연구실 인턴 등으로 경험해 보고 가는 것을 추천한다.</p>
<h2 id="heading-8jnre2uhoumrouenoyencdtmzzrj5nqs7wg7kee66gc7jeqioumgo2vncdqs6drr7w">🧭프리랜서 활동과 진로에 대한 고민</h2>
<p>졸업 이후로는 거의 곧바로 프리랜서로 일할 기회가 생겨서 3개의 프로젝트를 진행했다. 두 개는 이전에 외주를 받아서 일을 하던 회사에서 들어온 서비스 개발 프로젝트이고 서버, 앱, IOT 기기까지 다 하는 한 마디로 올인원 프로젝트다. 나머지 하나는 병원에서 들어온 논문 요약 AI 연구 프로젝트다.</p>
<p>이 두 가지 일을 하면서 취업 전선에 뛰어들기 전 나의 무기라고 할 수 있는 이력서를 정리하면서 어떤 직무로 지원을 할지 고민을 하게 됐었다. 주변 사람들의 반응이 전부 "대학원 나왔으니까 AI 연구원으로 가겠네?"였기 때문에 AI 직무와 백엔드나 프론트엔드와 같은 개발 직무 사이에서 고민을 좀 많이 했다.</p>
<p>결국은 내가 하는 일을 소개를 해야 할 때가 왔을 때 가장 즐겁게 소개를 할 수 있는 일이 무엇일까를 생각했을 때 AI 모델을 연구하는 일이 조금 덜 매력적으로 보인다는 생각이 들어서 백엔드 쪽으로 먼저 나아가보다는 결심을 했다. 연구원 경력을 못 살릴 수도 있다는 것이 좀 많이 아쉽지만 일단 마음먹은 쪽으로 쭉 밀고 나아가볼 생각이다.</p>
<p>남는 시간에는 사이드 프로젝트 팀을 모았다. 팀 이름은 "만나"로 지었다. 나이를 먹어갈수록 새로운 사람을 만나기 어려워진다는 생각이 들던차에 이 문제를 해결할 수 있는 방안 중 하나인 것 같아 소개팅 어플을 만들기로 결정했다. 8~9월에 직접 기획하고 팀원을 모아 총 6명의 인원으로 시작했다. 여러 번 만나면서 기획을 다듬는 과정을 거듭해가며 서로 많이 친해졌다. 3월 출시 목표로 열심히 달려나가야겠다!</p>
<h2 id="heading-4pyoiounioustoumra">✨ 마무리</h2>
<p>뭔가 속이 후련하기도 하고 살짝은 불안한 마음이 들기도 한다. 너무 감사한 점은 이런 마음이 들 때마다 내 주변에 도움을 줄 수 있는 사람들이 많다는 것이다. 부모님, 동생들부터 시작해서 고등학교, 대학교 동창, 연구실 동료들 그리고 8월부터 사귀기 시작한 여자친구까지 다들 너무 고맙고 앞으로 더 의지하게 될 것 같다. 내가 도움을 받은 만큼 더 멋진 사람이 돼서 그들이 나에게 의지할 수 있게 됐으면 좋겠다.</p>
<p>2023년은 익숙했던 환경에서 벗어나는 경험과 더불어 내 주변에 너무나 감사한 사람들이 많다는 것을 느낄 수 있었던 해였던 것 같다. 앞으로 경험하게 될 새로운 환경들과 그곳에서 만날 새로운 사람들이 너무 기대가 된다.</p>
]]></content:encoded></item><item><title><![CDATA[[Python] 정렬 마스터하기: 주요 함수와 모듈 소개]]></title><description><![CDATA[최근 프로그래머스에서 Python을 사용해 알고리즘 문제를 풀다가 정렬이 필요한 문제를 만났다. Python에서는 key함수를 이용해서 정렬을 하는데 다른 언어처럼 비교 함수를 구현해 정렬할 수 있는지 궁금해져서 공부를 하게 됐다. 이 글에서는 Python에서 정렬하는 방법을 알아본다.
프로그래머스 - 가장 큰 수
Key 함수로 리스트 정렬하기
Python에서 가장 간단한 정렬 방법 중 하나는 key 함수를 사용하는 것이다. key 함수는 인...]]></description><link>https://blog.aqudi.me/mastering-python-sorting</link><guid isPermaLink="true">https://blog.aqudi.me/mastering-python-sorting</guid><category><![CDATA[Python 3]]></category><category><![CDATA[Python]]></category><category><![CDATA[sorting]]></category><category><![CDATA[algorithms]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Fri, 16 Feb 2024 03:22:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708053698604/9d1cadf3-414f-4eb6-9242-b3a1095e338f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>최근 프로그래머스에서 Python을 사용해 알고리즘 문제를 풀다가 정렬이 필요한 문제를 만났다. Python에서는 key함수를 이용해서 정렬을 하는데 다른 언어처럼 비교 함수를 구현해 정렬할 수 있는지 궁금해져서 공부를 하게 됐다. 이 글에서는 Python에서 정렬하는 방법을 알아본다.</p>
<p><a target="_blank" href="https://school.programmers.co.kr/learn/courses/30/lessons/42746#">프로그래머스 - 가장 큰 수</a></p>
<h2 id="heading-key">Key 함수로 리스트 정렬하기</h2>
<p>Python에서 가장 간단한 정렬 방법 중 하나는 key 함수를 사용하는 것이다. key 함수는 인자 하나를 받아 정렬에 사용할 기준 값을 반환한다.</p>
<p>key 함수, 또는 collation 함수는 <code>min()</code>, <code>max()</code>, <code>sorted()</code>, <code>list.sort()</code>, <code>heapq.merge()</code>, <code>heapq.nlargest()</code>, <code>heapq.nsmallest()</code>, <code>itertools.groupby()</code> 등 정렬이 필요한 다양한 함수에서 활용된다.</p>
<h3 id="heading-7jii7kccoidrjidshozrrljsnpag6rws67aeioyxhuuklcdsojxrokw">예제: 대소문자 구분 없는 정렬</h3>
<pre><code class="lang-python">words = [<span class="hljs-string">"apple"</span>, <span class="hljs-string">"Banana"</span>, <span class="hljs-string">"cherry"</span>, <span class="hljs-string">"Date"</span>]
sorted_case_insensitive = sorted(words, key=str.lower)
sorted_default = sorted(words)

print(sorted_case_insensitive)
print(sorted_default)

<span class="hljs-comment"># 출력 결과:</span>
<span class="hljs-comment"># ['apple', 'Banana', 'cherry', 'Date']</span>
<span class="hljs-comment"># ['Banana', 'Date', 'apple', 'cherry']</span>
</code></pre>
<p><code>str.lower</code> 함수를 key로 사용하면 대소문자를 구분하지 않고 알파벳 순으로 정렬한다. 반면, 기본 정렬은 대소문자를 구분해 정렬한다.</p>
<h3 id="heading-7jii7kccoidtipztlizsnzgg7yq57kcvioqwkidquldspiag7kcv66cs">예제: 튜플의 특정 값 기준 정렬</h3>
<pre><code class="lang-python"><span class="hljs-comment"># 학생 정보 (이름, 성적, 나이) 튜플 리스트</span>
students = [
    (<span class="hljs-string">'John'</span>, <span class="hljs-string">'A'</span>, <span class="hljs-number">15</span>),
    (<span class="hljs-string">'Dave'</span>, <span class="hljs-string">'B'</span>, <span class="hljs-number">15</span>),
    (<span class="hljs-string">'Alice'</span>, <span class="hljs-string">'A'</span>, <span class="hljs-number">12</span>),
    (<span class="hljs-string">'Carol'</span>, <span class="hljs-string">'B'</span>, <span class="hljs-number">12</span>),
]
sorted_students_by_age = sorted(students, key=<span class="hljs-keyword">lambda</span> student: student[<span class="hljs-number">2</span>])

print(sorted_students_by_age)

<span class="hljs-comment"># 출력 결과:</span>
<span class="hljs-comment"># [('Alice', 'A', 12), ('Carol', 'B', 12), ('John', 'A', 15), ('Dave', 'B', 15)]</span>
</code></pre>
<p>나이를 기준으로 정렬하고 싶다면 key 함수에 나이에 해당하는 값을 반환하는 함수를 지정해 정렬하면 된다. 이 예제에서는 익명 함수를 사용해서 나이를 반환하는 함수를 구현했다.</p>
<h3 id="heading-7jii7kccoidsl6zrn6wg6riw7ksa7jy866gcioygleugro2vmoq4sa">예제: 여러 기준으로 정렬하기</h3>
<p>Python 정렬 알고리즘은 기본적으로 안정적(stable)이어서, 같은 키 값을 가진 요소들 사이의 원래 순서를 유지한다. 여러 조건을 사용해 정렬할 때는 <code>key</code> 함수를 통해 반환되는 값을 튜플로 구성해 이를 쉽게 달성할 수 있다.</p>
<p>다음 예제에서는 학생 정보를 담은 튜플 리스트를 성적(내림차순)과 나이(오름차순)를 기준으로 정렬한다. 이를 위해 <code>lambda</code> 함수를 사용해 정렬 기준을 정의한다.</p>
<pre><code class="lang-python">students = [(<span class="hljs-string">'Carol'</span>, <span class="hljs-string">'B'</span>, <span class="hljs-number">12</span>), (<span class="hljs-string">'Alice'</span>, <span class="hljs-string">'A'</span>, <span class="hljs-number">12</span>), (<span class="hljs-string">'Dave'</span>, <span class="hljs-string">'B'</span>, <span class="hljs-number">15</span>), (<span class="hljs-string">'John'</span>, <span class="hljs-string">'A'</span>, <span class="hljs-number">15</span>)]
sorted_students = sorted(students, key=<span class="hljs-keyword">lambda</span> student: (-ord(student[<span class="hljs-number">1</span>]), student[<span class="hljs-number">2</span>]))
print(sorted_students)
<span class="hljs-comment"># 출력 결과:</span>
<span class="hljs-comment"># [('Alice', 'A', 12), ('John', 'A', 15), ('Carol', 'B', 12), ('Dave', 'B', 15)]</span>
</code></pre>
<p>위 예제에서 <code>lambda</code> 함수는 각 학생(<code>student</code>)에 대해 성적의 알파벳(<code>student[1]</code>)을 ASCII 코드로 변환해 내림차순 정렬 기준으로 하고, 같은 성적을 가진 학생들 사이에서는 나이(<code>student[2]</code>)를 오름차순 정렬 기준으로 한다.</p>
<p>또 다른 방법으로 성적을 기준으로 내림차순 정렬한 후, 이후에 나이를 기준으로 오름차순 정렬하는 방식도 가능하다. 하지만, Python 정렬이 안정적이라는 점을 활용해 한 번에 여러 조건을 적용하는 것이 코드를 간결하게 유지하고 성능 측면에서도 유리하다.</p>
<pre><code class="lang-python"><span class="hljs-comment"># 성적 기준 내림차순 정렬 (단계 1)</span>
sorted_by_grade = sorted(students, key=<span class="hljs-keyword">lambda</span> student: -ord(student[<span class="hljs-number">1</span>]))
<span class="hljs-comment"># 나이 기준 오름차순으로 다시 정렬 (단계 2)</span>
sorted_by_grade_then_age = sorted(sorted_by_grade, key=<span class="hljs-keyword">lambda</span> student: student[<span class="hljs-number">2</span>])
print(sorted_by_grade_then_age)
<span class="hljs-comment"># 출력 결과:</span>
<span class="hljs-comment"># [('Alice', 'A', 12), ('John', 'A', 15), ('Carol', 'B', 12), ('Dave', 'B', 15)]</span>
</code></pre>
<h3 id="heading-operator">operator 모듈을 활용하여 리스트 정렬하기</h3>
<p>함수를 임의로 정의할 수 있지만, operator 모듈의 사전 정의된 기능을 활용할 수도 있다. 예를 들어, 리스트의 인덱스1에 위치한 아이템을 가져오는 lambda 함수 대신 <code>operator.itemgetter(1)</code>를 사용할 수 있고, dictionary의 key나 클래스 멤버는 <code>operator.attrgetter("firstname")</code>와 같이 접근할 수 있다.</p>
<p>여러 기준으로 정렬하기를 원한다면 <code>operator.itermgetter(1, 2)</code> 또는 <code>operator.attrgetter("firstname", "lastname")</code>과 같이 사용할 수 있다.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> operator

sorted_students = sorted(students, key=operator.itemgetter(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>))
print(sorted_students)
<span class="hljs-comment"># 출력 결과:</span>
<span class="hljs-comment"># [('Alice', 'A', 12), ('Carol', 'A', 15), ('Dave', 'B', 12), ('Bob', 'B', 15)]</span>
</code></pre>
<p><code>operator.itemgetter(1, 2)</code>를 사용해 성적 오름차순, 나이 오름차순으로 정렬한다.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> operator <span class="hljs-keyword">import</span> attrgetter

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, firstname, lastname</span>):</span>Ï
        self.firstname = firstname
        self.lastname = lastname

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__repr__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"Person(<span class="hljs-subst">{self.firstname}</span>, <span class="hljs-subst">{self.lastname}</span>)"</span>

persons = [
    Person(<span class="hljs-string">'John'</span>, <span class="hljs-string">'Doe'</span>),
    Person(<span class="hljs-string">'Jane'</span>, <span class="hljs-string">'Doe'</span>),
    Person(<span class="hljs-string">'Alice'</span>, <span class="hljs-string">'Cooper'</span>),
    Person(<span class="hljs-string">'Bob'</span>, <span class="hljs-string">'Barker'</span>)
]

sorted_persons = sorted(persons, key=attrgetter(<span class="hljs-string">'lastname'</span>, <span class="hljs-string">'firstname'</span>))
Ï
<span class="hljs-keyword">for</span> person <span class="hljs-keyword">in</span> sorted_persons:
    print(person)

<span class="hljs-comment"># 출력 결과:</span>
<span class="hljs-comment"># Person(Bob, Barker)</span>
<span class="hljs-comment"># Person(Alice, Cooper)</span>
<span class="hljs-comment"># Person(Jane, Doe)</span>
<span class="hljs-comment"># Person(John, Doe)</span>
</code></pre>
<p>클래스나 dictionary의 경우 <code>operator.attrgetter(...)</code>로 속성 기준 정렬을 한다.</p>
<h3 id="heading-functoolscmptokey"><code>functools.cmp_to_key</code>로 리스트 정렬하기</h3>
<p><code>functools.cmp_to_key</code> 함수는 사용자 정의 비교 함수를 키 함수로 변환해 사용한다. Python 3에서는 <code>cmp(a, b)</code> 형태의 직접 비교 방식 대신 키 함수 사용을 권장한다. 비교 함수를 사용해야 하는 상황이라면 이 함수로 비교 함수를 키 함수로 변환할 수 있다.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> functools <span class="hljs-keyword">import</span> cmp_to_key

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">compare_items</span>(<span class="hljs-params">x, y</span>):</span>
    <span class="hljs-keyword">return</span> len(x) - len(y)

key_function = cmp_to_key(compare_items)

str_list = [<span class="hljs-string">'banana'</span>, <span class="hljs-string">'apple'</span>, <span class="hljs-string">'cherry'</span>, <span class="hljs-string">'date'</span>]
sorted_str_list = sorted(str_list, key=key_function)

print(sorted_str_list)  

<span class="hljs-comment"># 출력 결과:</span>
<span class="hljs-comment"># ['date', 'apple', 'banana', 'cherry']</span>
</code></pre>
<p><code>compare_items</code> 함수는 문자열 길이를 비교한다. <code>cmp_to_key</code>로 이를 키 함수로 변환하고, <code>sorted</code>의 <code>key</code> 인자로 전달해 문자열 길이 기준으로 정렬한다.</p>
<h2 id="heading-7kcv66cs7j2eioyngoybko2vmouklcdtgbtrnpjsiqqg66em65ok6riw">정렬을 지원하는 클래스 만들기</h2>
<p>복잡한 정렬 로직이 필요한 경우 사용자 정의 클래스를 만들어 정렬할 수 있도록 하는 것이 유지보수와 가독성 측면에서 유리하다.</p>
<p>Python에서는 매직 메소드를 이용하여 클래스 인스턴스 간 비교 연산자를 직접 정의할 수 있고 이렇게 구현한 비교 연산자들은 <code>sorted</code>, <code>list.sort()</code>와 같은 내장 함수와도 함께 사용할 수 있어 더 깨끗하고 Pythonnic한 코드를 만들 수 있다.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, firstname, lastname</span>):</span>
        self.firstname = firstname
        self.lastname = lastname

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__repr__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"Person('<span class="hljs-subst">{self.firstname}</span>', '<span class="hljs-subst">{self.lastname}</span>')"</span>

    <span class="hljs-comment"># 비교 매직 메소드 추가</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__lt__</span>(<span class="hljs-params">self, other</span>):</span>
        <span class="hljs-keyword">return</span> (self.lastname, self.firstname) &lt; (other.lastname, other.firstname)

persons = [
    Person(<span class="hljs-string">'John'</span>, <span class="hljs-string">'Doe'</span>),
    Person(<span class="hljs-string">'Jane'</span>, <span class="hljs-string">'Doe'</span>),
    Person(<span class="hljs-string">'Alice'</span>, <span class="hljs-string">'Cooper'</span>),
    Person(<span class="hljs-string">'Bob'</span>, <span class="hljs-string">'Barker'</span>)
]

<span class="hljs-comment"># 성(lastname)과 이름(firstname) 순으로 정렬</span>
sorted_persons = sorted(persons)

<span class="hljs-keyword">for</span> person <span class="hljs-keyword">in</span> sorted_persons:
    print(person)

<span class="hljs-comment"># 출력 결과:</span>
<span class="hljs-comment"># Person('Bob', 'Barker')</span>
<span class="hljs-comment"># Person('Alice', 'Cooper')</span>
<span class="hljs-comment"># Person('Jane', 'Doe')</span>
<span class="hljs-comment"># Person('John', 'Doe')</span>
</code></pre>
<h2 id="heading-6rkw66gg">결론</h2>
<p>이번 포스팅을 작성하면서 python 정렬 방식을 확실하게 정리한 것 뿐만 아니라 operator 모듈을 활용하는 방법이나 여러 정렬 기준을 한 번에 적용하여 정렬하는 방법까지 알게 됐다. Python을 꽤 오래 사용했었는데 관성적으로 사용하는 것들만 사용하던 것을 반성하는 시간이 됐다. </p>
<p>오랜만에 어떤 기능들이 추가됐는지 궁금하여 <a target="_blank" href="https://docs.python.org/ko/3/whatsnew/3.12.html">Python 3.12 릴리즈 문서</a>를 열어보니 type statement가 추가되어 generic 타입과 type aliases를 좀 더 자연스럽게 할 수 있게 됐고, f-strings 사용 시 불편했던 <code>"{""}"</code>이 이제는 오류가 나지 않는다고 해서 반가웠다. 언어도 끊임없이 발전하니 언어에 대한 공부도 게을리하지 말아야겠다고 생각이 들었다.</p>
<p>다음에는 최근에 많이 쓰고 있는 Dart나 Python의 새로운 기능들이나 어떻게 업데이트가 되어왔는지 한 번 훑어보는 글을 작성해봐야겠다.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><a target="_blank" href="https://docs.python.org/ko/3/library/operator.html">Python3 Docs - operator module</a></li>
<li><a target="_blank" href="https://docs.python.org/3/glossary.html#term-key-function">Python3 Docs - key function</a></li>
<li><a target="_blank" href="https://docs.python.org/3/library/functools.html#functools.cmp_to_key">Python3 Docs - functools.cmp_to_key</a></li>
<li><a target="_blank" href="https://supermemi.tistory.com/entry/Python-3-Magic-Methods-%EB%8B%A4%EB%A3%A8%EA%B8%B0-2%ED%8E%B8-%EB%B9%84%EA%B5%90-%EC%97%B0%EC%82%B0%EC%9E%90-eq-ne-lt-gt-le-ge">Python Magic Methods 비교 연산자</a></li>
<li><a target="_blank" href="https://docs.python.org/ko/3/howto/sorting.html">Python3 Docs - 정렬 How To</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Value Object를 통한 안전한 데이터 사용]]></title><description><![CDATA[개요
최근 BLE 프로토콜을 통해서 IOT기기와 통신을 하는 애플리케이션을 개발할 일이 있었는데 펌웨어 업데이트 이후에 갑자기 통신이 안 되는 문제가 발생을 했다. 원인을 찾아보니 일부 기기의 unique key를 advertising 하는 로직이 바뀌어 포맷은 동일한데 일부 자릿수가 16진수로 표기될 때 앞의 0을 빼먹고 전달이 된 것과 대소문자의 차이로 이러한 문제가 발생을 했다는 것을 알게 됐다.
이때 단순하게 IoT device를 나타내...]]></description><link>https://blog.aqudi.me/why-value-object</link><guid isPermaLink="true">https://blog.aqudi.me/why-value-object</guid><category><![CDATA[ValueObject]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[#Domain-Driven-Design]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Tue, 06 Feb 2024 14:56:22 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-6rcc7jqu">개요</h2>
<p>최근 BLE 프로토콜을 통해서 IOT기기와 통신을 하는 애플리케이션을 개발할 일이 있었는데 펌웨어 업데이트 이후에 갑자기 통신이 안 되는 문제가 발생을 했다. 원인을 찾아보니 일부 기기의 unique key를 advertising 하는 로직이 바뀌어 포맷은 동일한데 일부 자릿수가 16진수로 표기될 때 앞의 0을 빼먹고 전달이 된 것과 대소문자의 차이로 이러한 문제가 발생을 했다는 것을 알게 됐다.</p>
<p>이때 단순하게 IoT device를 나타내는 클래스의 getter를 통해서 수정을 할까 하다가 코드가 너무 길어져서 가독성, 유지보수성이 떨어지기도 해서 value object로 분리하여 validation의 책임을 분리시키기로 했다. 오늘은 이때 공부했던 내용을 예시와 함께 정리를 해보려고 한다.</p>
<h2 id="heading-primitive-obsession">Primitive Obsession</h2>
<p>Primitive Obsession, 원시타입 집착은 복잡한 개념을 표현할 때 기본 자료형을 과도하게 사용하는 것을 말한다. 예를 들어, 2차원 좌표, 기간, 전화번호와 같은 복잡한 데이터 타입을 특정 클래스나 구조체를 만들지 않고 프리미티브로 표현하는 것이 여기에 포함된다.</p>
<p>전화번호는 문자열만을 이용해서 충분히 나타낼 수 있다. 하지만 문자열이 가지고 있는 모든 속성을 전화번호가 가지고 있지는 않다. 예를 들어, "문자열이 지원하는 더하기 연산을 전화번호가 지원을 해야 할까?"라고 생각한다면 그 대답은 "아니요"가 될 것이다.</p>
<p>Primitive Obsession는 이처럼 도메인적으로 해당 값이 나타내는 의미를 직관적으로 판단할 수 없게 만든다. 그뿐만 아니라 데이터와 관련된 검증 및 조작 로직을 코드 전체에 분산시켜 유지 관리를 어렵게 만들고 IDE의 도움을 받기 어렵기 때문에 실수가 일어나기도 쉽다.</p>
<p>Value Object가 바로 이러한 Primitive Obsession을 피하면서 값이 도메인 내에서 가지는 의미를 명확하게 전달할 수 있게 해주는 장치이다.</p>
<h2 id="heading-value-objectvo">Value Object(VO)란?</h2>
<p>Value Object는 도메인 주도 설계(domain-driven development, DDD)의 핵심적인 개념 중 하나로 도메인의 한 측면을 나타내는 객체이다. 보통 한 개 또는 그 이상의 값들을 묶어서 특정 값을 나타내는 객체로 대표적인 예시로는 x, y 좌표로 구성되는 2차원 좌표나 시작 날짜와 끝 날짜로 구성되는 기간 등이 있다.</p>
<p>Value object가 가져야 하는 주요 특징은 값 기반 동등성 검사와 불변성, 자기 유효성 검사가 있다. </p>
<h2 id="heading-value-object">Value Object의 특징</h2>
<h3 id="heading-6rcsioq4souwmcdrj5nrk7hshleg6rka7iks">값 기반 동등성 검사</h3>
<p><a target="_blank" href="https://hudi.blog/identity-vs-equality/">동일성(identity)과 동등성(equality)의 차이</a>를 간략하게 설명하면 동일성은 비교 대상인 두 객체의 메모리 주소가 같음을 의미하고 동등성은 두 객체가 논리적으로 동일한 값을 나타내고 있음을 의미한다.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> point1 = { <span class="hljs-attr">x</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">y</span>: <span class="hljs-number">10</span> };
<span class="hljs-keyword">const</span> point2 = { <span class="hljs-attr">x</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">y</span>: <span class="hljs-number">10</span> };
</code></pre>
<p>예를 들어, 2차원 좌표를 표현하는 값을 가지고 있는 두 변수 <code>point1</code>, <code>point2</code>가 있을때 두 값은 내부의 x, y 좌표값이 같기 때문에 동등하지만 서로 다른 변수이기 때문에 동일하지는 않다고 볼 수 있다.</p>
<p>동등성 검사는 언어마다 다르게 이루어진다. 예를 들어 Java에서는 <code>equals</code>, <code>hashcode</code>를 재정의해야 하는 반면에 functional programming 언어인 Haskell이나 Clojure에서는 기본적으로 속성을 기준으로 한 동등성 검사를 지원한다.</p>
<h3 id="heading-immutability">불변성 (Immutability)</h3>
<p>Value Object는 그 자체로 값인 객체이고 이 값이 불변이어야 한다는 원칙이 있다. 그렇기 때문에 수정자를 가지고 있지 않아야 하며 만약에 값을 바꾸고 싶다면 새로운 객체를 만들어서 값을 할당하는 방법 뿐이다.</p>
<p>이러한 특징 덕분에 side effect 발생을 방지할 수 있어 의도하지 않은 곳에서 이 값이 수정되어 발생하는 <a target="_blank" href="https://martinfowler.com/bliki/AliasingBug.html">Aliasing Bug</a> 걱정 없이 사용할 수 있다.</p>
<h3 id="heading-self-validation">자기 유효성 검사 (Self-validation)</h3>
<p>만약에 원시타입을 사용하여 복잡한 값을 표현한다면 값의 유효성을 사용하는 측에서 검사를 해야 한다. Value Object의 유효성 검사는 생성 시간에 이루어져서 항상 유효한 상태를 유지할 수 있도록 해야 하며 유효하지 않는 값이 들어왔을 때는 Value Object를 생성할 수 없어야한다.</p>
<p>Value Object의 불변성과 자기 유효성 검사를 통해 항상 유효한 상태 유지를 보장할 수 있어 클라이언트 쪽에서 도메인 규칙이 깨질 염려를 하지 않을 수 있다.</p>
<h2 id="heading-device-unique-key-value-object">Device의 unique key를 Value Object로 표현</h2>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DeviceUniqueKey</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> _uniqueKey;

  DeviceUniqueKey(<span class="hljs-built_in">String</span> uniqueKey)
      : <span class="hljs-keyword">assert</span>(uniqueKey.isNotEmpty),
        <span class="hljs-keyword">assert</span>(
          uniqueKey
              .split(<span class="hljs-string">":"</span>)
              .map((e) =&gt; <span class="hljs-built_in">int</span>.tryParse(e, radix: <span class="hljs-number">16</span>) != <span class="hljs-keyword">null</span>)
              .every((element) =&gt; element),
        ),
        _uniqueKey = uniqueKey.toUpperCase();

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> <span class="hljs-keyword">operator</span> ==(<span class="hljs-built_in">Object</span> other) {
    <span class="hljs-keyword">return</span> other.hashCode == hashCode;
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">int</span> <span class="hljs-keyword">get</span> hashCode =&gt; _uniqueKey.hashCode;
}
</code></pre>
<p>위의 dart로 작성된 unique key를 나타내는 value object을 살펴보면 앞서 본 Value Object의 특징처럼 생성 시점에 validation을 수행하여 특정한 포맷을 만족하도록 강제하며 자기 유효성 검사를 수행하는 것을 볼 수 있다. 그리고 불변성을 보장하기 위해 내부의 값을 수정할 수 없게 만들었으며 <code>==</code> 연산자와 <code>hashCode</code>를 재정의하여 값을 통한 동등성 검사를 지원하고 있다.</p>
<h2 id="heading-6rkw66gg">결론</h2>
<p>Value Object가 가지는 불변성, 자기 유효성 검사 등 클라이언트가 안심하고 사용할 수 있게 해주는 특성들은 협업 상황에서 더욱 빛을 발하는 것 같다. 특히 내가 만들어둔 객체가 의도한 상태대로 있게 해준다는 것은 커뮤니케이션 과정에서 발생할 수 있는 오류들을 줄일 수 있다는 것이 굉장히 큰 매력으로 느껴졌다. 도메인이 더 복잡해질수록 Value Object가 가져다주는 코드의 명확성, 유지보수성이 더 큰 장점으로 다가올 것 같다.</p>
<p>앞으로는 혼자서 개발을 할 때도 꽤 오래 전에 만들어 둔 로직들은 까먹을 수도 있기 때문에 앞으로 validation 이 필요한 값의 경우에는 Value Object를 통해서 처리를 하는 습관을 들여야겠다.</p>
<p>다음에는 Value Object를 공부하면서 정리를 하고 있었던 일급 컬렉션에 대한 포스팅을 할 것이다.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p>https://martinfowler.com/bliki/ValueObject.html</p>
</li>
<li><p>https://en.wikipedia.org/wiki/Value_object</p>
</li>
<li><p>https://hudi.blog/value-object/</p>
</li>
<li><p>https://hudi.blog/identity-vs-equality/</p>
</li>
<li><p>https://ksh-coding.tistory.com/83</p>
</li>
<li><p>https://velog.io/@livenow/Java-VOValue-Object%EB%9E%80</p>
</li>
<li><p>https://medium.com/@nicolopigna/value-objects-like-a-pro-f1bfc1548c72</p>
</li>
<li><p>https://medium.com/fistkim101/ddd-%EC%84%B8%EB%A0%88%EB%82%98%EB%8D%B0-3-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A3%BC%EB%8F%84-%EC%84%A4%EA%B3%84-%EA%B8%B0%EB%B3%B8-%EC%9A%94%EC%86%8C-99eead8e96f3</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[[Dart] Map 구현체: HashMap, SplayTreeMap, LinkedHashMap]]></title><description><![CDATA[Flutter 개발을 하다가 key를 기준으로 정렬되는 map 자료형이 필요하여 공부를 찾아보다가 생각보다 여러 가지 구현체가 있다는 것을 알게 됐다. Dart 언어를 접한 지 벌써 3년이 다 되어가는데 이런 요구사항이 있었던 적이 한 번도 없었어서 이제야 알게 됐다는 것이 놀라웠다.
그래서 오늘은 세 가지 Dart Map 구현체 HashMap, SplayTreeMap, LinkedHashMap에 대해서 살펴보고 각 구현체를 언제 사용해야 하는...]]></description><link>https://blog.aqudi.me/dart-hashmap-splaytreemap-linkedhashmap</link><guid isPermaLink="true">https://blog.aqudi.me/dart-hashmap-splaytreemap-linkedhashmap</guid><category><![CDATA[Dart]]></category><category><![CDATA[data structure]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[algorithms]]></category><category><![CDATA[sorting]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Thu, 18 Jan 2024 08:34:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1705634462417/d4e19977-81f0-4105-82f9-188266ebc9a1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Flutter 개발을 하다가 key를 기준으로 정렬되는 map 자료형이 필요하여 공부를 찾아보다가 생각보다 여러 가지 구현체가 있다는 것을 알게 됐다. Dart 언어를 접한 지 벌써 3년이 다 되어가는데 이런 요구사항이 있었던 적이 한 번도 없었어서 이제야 알게 됐다는 것이 놀라웠다.</p>
<p>그래서 오늘은 세 가지 Dart Map 구현체 <code>HashMap</code>, <code>SplayTreeMap</code>, <code>LinkedHashMap</code>에 대해서 살펴보고 각 구현체를 언제 사용해야 하는지에 대해서 정리를 해보려고 한다.</p>
<h2 id="heading-map">Map 자료 구조</h2>
<p>Map은 연관된 키를 사용하여 값을 검색하는 키/값 쌍의 컬렉션이다. 여러 프로그래밍 언어에서 기본적으로 제공하는 자료형으로 파이썬에서는 사전(dictionary)이라는 자료형으로 제공하고 있다.</p>
<p><code>Map.entries</code> 속성을 통해서 Map안의 키/값 쌍을 반복할 수 있는데 이때 반복의 순서는 맵의 세부 구현에 따라서 달라지게 된다. 현재 Dart 3.2.5 버전의 Map 구현체는 <code>HashMap</code>, <code>SplayTreeMap</code>, <code>LinkedHashMap</code> 세 가지가 있다.</p>
<h3 id="heading-1-hashmap"><strong>1. HashMap</strong></h3>
<p><code>HashMap</code>은 Dart의 기본 Map 구현체로 해시 테이블을 활용하여 조회, 추가, 삭제가 빠르지만 요소의 순서를 유지하지 않는다는 특징이 있다.</p>
<h4 id="heading-hashmap"><code>HashMap</code>이 적합한 상황</h4>
<ul>
<li><p><strong>성능</strong>: 빠른 조회, 추가, 삭제가 중요할 때</p>
</li>
<li><p><strong>순서가 없는 데이터:</strong> 요소들의 순서가 중요하지 않은 상황</p>
</li>
</ul>
<h4 id="heading-7jii7kccioylnoucmoumroyypdog7jwxioyepoyglsdsoidsnqusioqygoydiq">예제 시나리오: 앱 설정 저장, 검색</h4>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:collection'</span>;

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">final</span> userPreferences = HashMap&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt;();

  <span class="hljs-comment">// 사용자 선호도 설정</span>
  userPreferences[<span class="hljs-string">'theme'</span>] = <span class="hljs-string">'Dark'</span>;
  userPreferences[<span class="hljs-string">'fontSize'</span>] = <span class="hljs-number">14</span>;
  userPreferences[<span class="hljs-string">'notificationsEnabled'</span>] = <span class="hljs-keyword">true</span>;

  <span class="hljs-comment">// 특정 선호도 접근</span>
  <span class="hljs-keyword">final</span> theme = userPreferences[<span class="hljs-string">'theme'</span>];
  <span class="hljs-built_in">print</span>(<span class="hljs-string">'사용자가 선택한 테마: <span class="hljs-subst">$theme</span>'</span>);

  <span class="hljs-comment">// 더 많은 선호도 추가</span>
  userPreferences[<span class="hljs-string">'language'</span>] = <span class="hljs-string">'English'</span>;
}

<span class="hljs-comment">// Output:</span>
<span class="hljs-comment">// 사용자가 선택한 테마: Dart</span>
</code></pre>
<h3 id="heading-2-splaytreemap"><strong>2. SplayTreeMap</strong></h3>
<p><code>SplayTreeMap</code>은 <a target="_blank" href="https://en.wikipedia.org/wiki/Self-balancing_binary_search_tree">self-balancing binary search tree</a>를 기반으로 하는 Map 구현체로 키를 기준으로 값을 정렬한다. 자주 사용하는 값은 트리의 루트에 가까운 위치로 이동되어 이후에 접근할 때 더 빠르게 접근할 수 있게 만든다는 특징이 있다.</p>
<h4 id="heading-splaytreemap"><code>SplayTreeMap</code>이 적합한 상황</h4>
<ul>
<li><p><strong>정렬:</strong> 키를 기준으로 요소들을 정렬된 상태로 유지해야 할 때 (순서가 중요한 데이터)</p>
</li>
<li><p><strong>효율적인 접근:</strong> 데이터별로 접근 빈도가 크게 차이나는 상황</p>
</li>
</ul>
<h4 id="heading-7jii7iucioylnoucmoumroyypdog66as642u67o065oc">예시 시나리오: 리더보드</h4>
<p>점수 순으로 정렬된 상태를 유지하면서 Top3와 같이 자주 접근하는 데이터가 있을 경우가 있을 때 <code>SplayTreeMap</code>이 유용하게 사용될 수 있다.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:collection'</span>;

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">final</span> scores = &lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">int</span>&gt;{};
  <span class="hljs-keyword">final</span> leaderboard = SplayTreeMap&lt;<span class="hljs-built_in">int</span>, <span class="hljs-built_in">Set</span>&lt;<span class="hljs-built_in">String</span>&gt;&gt;();

  <span class="hljs-comment">// 플레이어 점수 추가/업데이트하는 함수</span>
  <span class="hljs-keyword">void</span> updateScore(<span class="hljs-built_in">String</span> playerId, <span class="hljs-built_in">int</span> score) {
    leaderboard[scores[playerId]]?.remove(playerId);
    leaderboard.putIfAbsent(score, () =&gt; {}).add(playerId);
    scores[playerId] = score;
  }

  <span class="hljs-comment">// 점수 추가/업데이트</span>
  updateScore(<span class="hljs-string">'player1'</span>, <span class="hljs-number">100</span>);
  updateScore(<span class="hljs-string">'player2'</span>, <span class="hljs-number">150</span>);
  updateScore(<span class="hljs-string">'player3'</span>, <span class="hljs-number">100</span>);

  <span class="hljs-comment">// 리더보드를 내림차순으로 표시 (가장 높은 점수가 먼저)</span>
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> score <span class="hljs-keyword">in</span> leaderboard.keys.toList().reversed) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'점수: <span class="hljs-subst">$score</span>, 플레이어들: <span class="hljs-subst">${leaderboard[score]}</span>'</span>);
  }

  <span class="hljs-comment">// 점수 추가/업데이트</span>
  updateScore(<span class="hljs-string">'player1'</span>, <span class="hljs-number">120</span>);
  updateScore(<span class="hljs-string">'player2'</span>, <span class="hljs-number">200</span>);
  updateScore(<span class="hljs-string">'player3'</span>, <span class="hljs-number">150</span>);

  <span class="hljs-comment">// 리더보드를 내림차순으로 표시 (가장 높은 점수가 먼저)</span>
  <span class="hljs-built_in">print</span>(<span class="hljs-string">"\n업데이트 후"</span>);
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> score <span class="hljs-keyword">in</span> leaderboard.keys.toList().reversed) {
    <span class="hljs-keyword">if</span>(leaderboard[score] != <span class="hljs-keyword">null</span> &amp;&amp; leaderboard[score]!.isNotEmpty) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'점수: <span class="hljs-subst">$score</span>, 플레이어들: <span class="hljs-subst">${leaderboard[score]}</span>'</span>);
    }
  }
}

<span class="hljs-comment">// Output:</span>
<span class="hljs-comment">// 점수: 150, 플레이어들: {player2}</span>
<span class="hljs-comment">// 점수: 100, 플레이어들: {player1, player3}</span>
<span class="hljs-comment">// </span>
<span class="hljs-comment">// 업데이트 후</span>
<span class="hljs-comment">// 점수: 200, 플레이어들: {player2}</span>
<span class="hljs-comment">// 점수: 150, 플레이어들: {player3}</span>
<span class="hljs-comment">// 점수: 120, 플레이어들: {player1}</span>
</code></pre>
<h3 id="heading-3-linkedhashmap"><strong>3. LinkedHashMap</strong></h3>
<p><code>LinkedHashMap</code>은 항목이 추가된 순서를 유지한다. 즉, 맵을 순회할 때 항목이 추가된 순서대로 반환된다. 항목의 삽입 순서가 중요할 때 특히 유용하다.</p>
<h4 id="heading-linkedhashmap"><code>LinkedHashMap</code>이 적합한 상황</h4>
<ul>
<li><strong>순서 유지:</strong> 데이터의 순서가 중요한 애플리케이션</li>
</ul>
<h4 id="heading-7jii7iucioylnoucmoumroyypdog7ie87zwrioy5to2kua">예시 시나리오: 쇼핑 카트</h4>
<p>쇼핑카트에는 담은 순서대로 표시가 되며 쇼핑카트의 수량이 변경되더라도 그 순서는 변경되지 않는다. 쇼핑카트는 <code>LinkedHashMap</code>이 동작하는 방식과 유사하다.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:collection'</span>;

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">var</span> shoppingCart = LinkedHashMap&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">int</span>&gt;();

  <span class="hljs-comment">// 상품을 쇼핑 카트에 추가</span>
  shoppingCart[<span class="hljs-string">'Apple'</span>] = <span class="hljs-number">1</span>;
  shoppingCart[<span class="hljs-string">'Banana'</span>] = <span class="hljs-number">2</span>;
  shoppingCart[<span class="hljs-string">'Orange'</span>] = <span class="hljs-number">2</span>;

  <span class="hljs-comment">// 카트 내용 확인</span>
  <span class="hljs-built_in">print</span>(<span class="hljs-string">"쇼핑 카트:"</span>);
  shoppingCart.forEach((itemNumber, itemName) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"<span class="hljs-subst">$itemNumber</span>: <span class="hljs-subst">$itemName</span>개"</span>);
  });

  <span class="hljs-comment">// 수량 변경</span>
  shoppingCart[<span class="hljs-string">'Apple'</span>] = <span class="hljs-number">6</span>;

  <span class="hljs-built_in">print</span>(<span class="hljs-string">"\n수량 변경 후 쇼핑 카트:"</span>);
  shoppingCart.forEach((itemNumber, itemName) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"<span class="hljs-subst">$itemNumber</span>: <span class="hljs-subst">$itemName</span>개"</span>);
  });
}

<span class="hljs-comment">// Output:</span>
<span class="hljs-comment">// 쇼핑 카트:</span>
<span class="hljs-comment">// Apple: 1개</span>
<span class="hljs-comment">// Banana: 2개</span>
<span class="hljs-comment">// Orange: 2개</span>
<span class="hljs-comment">// </span>
<span class="hljs-comment">// 수량 변경 후 쇼핑 카트:</span>
<span class="hljs-comment">// Apple: 6개</span>
<span class="hljs-comment">// Banana: 2개</span>
<span class="hljs-comment">// Orange: 2개</span>
</code></pre>
<h2 id="heading-6rkw66ggic0g66y07jeh7j2eioycroyaqe2vtoyvvcdtladquyw">결론 - 무엇을 사용해야 할까?</h2>
<ul>
<li><p>순서가 중요하지 않고 빠른 검색, 삽입, 삭제가 필요한 경우 <code>HashMap</code></p>
</li>
<li><p>정렬과 자주 접근하는 요소에 대한 효율적인 접근이 필요할 때 <code>SplayTreeMap</code></p>
</li>
<li><p>삽입 순서가 유지되어야 하는 경우 <code>LinkedHashMap</code></p>
</li>
</ul>
<h2 id="heading-7lau6rcaioyekoujjcdrsi8g7lc46rogioyekoujjdo">추가 자료 및 참고 자료:</h2>
<ul>
<li><p><a target="_blank" href="https://api.dart.dev/stable/3.2.5/dart-collection/HashMap-class.html">Dart Collection - HashMap</a></p>
</li>
<li><p><a target="_blank" href="https://api.dart.dev/stable/3.2.5/dart-collection/SplayTreeMap-class.html">Dart Collection - SplayTreeMap</a></p>
</li>
<li><p><a target="_blank" href="https://api.dart.dev/stable/3.2.5/dart-collection/LinkedHashMap-class.html">Dart Collection - LinkedHashMap</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[YesTakeout! - v1.0.0 소개 및 사용 방법]]></title><description><![CDATA[오늘은 YesTakeout v1.0.0 배포 기념으로 간단한 사용방법과 함께 소개글을 작성해보려고 한다. 그 사이에 갑작스럽게 여행을 가게 되어서 만들어두고 올리는 시기가 좀 늦어졌다.
먼저 이 프로젝트는 순전히 내가 필요했기 때문에 개인적인 용도로 만든 것으로 Yes24의 eBook 프로그램이 바뀌면 사용이 불가능할 수 있다. 지금은 Windows 버전만 만들었고 이후 MacOS 버전도 개발환경을 준비되면 배포해보도록 하겠다.
다운로드
아래 ...]]></description><link>https://blog.aqudi.me/yestakeout-v1-introduction</link><guid isPermaLink="true">https://blog.aqudi.me/yestakeout-v1-introduction</guid><category><![CDATA[viewer]]></category><category><![CDATA[highlights]]></category><category><![CDATA[메모]]></category><category><![CDATA[사용방법]]></category><category><![CDATA[ebook]]></category><category><![CDATA[annotations]]></category><category><![CDATA[yes24]]></category><category><![CDATA[독서노트]]></category><category><![CDATA[guide]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sat, 13 Jan 2024 13:25:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1705234392917/844f5638-a834-4c56-83c8-a135cd87620b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>오늘은 YesTakeout v1.0.0 배포 기념으로 간단한 사용방법과 함께 소개글을 작성해보려고 한다. 그 사이에 갑작스럽게 여행을 가게 되어서 만들어두고 올리는 시기가 좀 늦어졌다.</p>
<p>먼저 이 프로젝트는 순전히 내가 필요했기 때문에 개인적인 용도로 만든 것으로 Yes24의 eBook 프로그램이 바뀌면 사용이 불가능할 수 있다. 지금은 Windows 버전만 만들었고 이후 MacOS 버전도 개발환경을 준비되면 배포해보도록 하겠다.</p>
<h2 id="heading-64uk7jq066gc65oc">다운로드</h2>
<p>아래 링크로 들어가서 오른쪽 Release란에 최신 버전을 클릭하고 Release.zip 을 다운받고 압축을 풀면 최신 버전을 사용할 수 있다.<br /><a target="_blank" href="https://github.com/Aqudi/YesTakeout/tree/main"><strong>YesTakeout 설치 링크</strong></a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705235134415/a5974312-3d3c-420b-ac3a-089e0b80b84b.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705139757786/8543d18c-9754-41a8-8a84-027ec531bf3d.png" alt="YesTakeout Windows 버전 다운로드 방법 설명" class="image--center mx-auto" /></p>
<h2 id="heading-7iks7jqpiouwqeuylq">사용 방법</h2>
<p>이 프로젝트는 Yes24 Ebook에서 사용하는 로컬 데이터베이스에 담긴 정보가 필요하므로 우선 Yes24 Ebook을 설치와 데이터 다운로드 과정이 필수적이다.</p>
<h3 id="heading-yes24-ebook-pc">Yes24 eBook PC 뷰어 설치</h3>
<p>⚠️ 반드시 아무런 세팅없이 다음을 눌러서 설치를 하셔야지 사용하실 때 번거롭지 않습니다.</p>
<ol>
<li><p>아래 링크를 통해 Yes24 eBook PC 뷰어 설치</p>
<ul>
<li><a target="_blank" href="https://www.yes24.com/notice/eBookGuide/guide_pc.aspx">Yes24 eBook PC 뷰어 설치 링크</a> 접속 후 사진에 표시된 버튼 클릭하여 설치</li>
</ul>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705140071126/1d339943-0a8a-4f25-b2d9-b383fb46c205.png" alt="Yes24 eBook PC 뷰어 설치 방법" class="image--center mx-auto" /></p>
<p> 원하는 책의 <code>다운로드</code> 버튼을 눌러 책을 다운로드</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705140247728/fdfa10fd-8730-40ec-a323-96242d1461de.png" alt="Yes24 eBook PC 뷰어에서 책 다운로드 방법" class="image--center mx-auto" /></p>
<p> 다운로드 완료 후 2번 사진처럼 <code>열기</code> 버튼으로 바뀌었다면 <code>열기</code> 버튼 클릭</p>
</li>
</ol>
<h3 id="heading-yestakeout">YesTakeout을 사용하여 하이라이트, 메모 열람</h3>
<ol>
<li><p><code>yes_takeout.exe</code> 파일 실행</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705140429693/dc1a7ad4-33bf-4b55-9fae-2e64634a6e7f.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>아래와 같은 화면이 뜨면 <code>Takeout 하러 가기</code> 클릭</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705139809003/125b2556-0c5c-4ab5-9d4d-39b321e98beb.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><code>Open database</code> 를 클릭하여 Yes24 eBook 데이터베이스 선택</p>
<ul>
<li><p>기본 설정으로 설치했다면 <code>Open database</code> 클릭했을 때 바로 설정이 될 것이다.</p>
</li>
<li><p>기본 데이터베이스 주소: <code>%USERPROFILE%\AppData\Local\YES24eBook\databases</code></p>
</li>
</ul>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705145349981/894746de-fa4d-4513-93ab-902ff97b868a.png" alt class="image--center mx-auto" /></p>
<p> 원하는 책 선택</p>
</li>
<li><p>인덱스 보기, 메모 보기, 하이라이트색 등을 토글할 수 있다.</p>
<p> 이제 원하는 부분을 드래그하여 선택한 후 독서노트를 작성하면 된다.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705145719355/33f2f7d9-67c6-455e-b444-aee6f4d33a03.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-7jwe7jy866gc7j2yioqzho2ajq">앞으로의 계획</h2>
<p>앞으로는 다음과 같은 기능들을 만들어볼 예정입니다. 일단은 제가 필요하다고 생각하는 내용들을 위주로 넣어놨는데 지금도 충분한 것 같기는 해서 사용하시는 분들이 있는지 반응을 보고 필요하다면 업데이트를 해봐야겠습니다.</p>
<ul>
<li><p>MacOS 지원</p>
</li>
<li><p>템플릿 기능: 선택한 하이라이트를 템플릿에 맞춰서 내보내기</p>
</li>
<li><p>옵시디언으로 내보내기 기능: 옵시디언으로 하이라이트와 메모, 책 정보 내보내기</p>
</li>
<li><p>필사 기능: 그냥 복사하는 것이 아니라 직접 타이핑해서 복사</p>
</li>
</ul>
<h2 id="heading-7zwc6roe7kcq">한계점</h2>
<ul>
<li><p>우선 가장 큰 한계점은 Yes24 eBook PC 뷰어에 의존하고 있기 때문에 이 프로그램이 바뀌거나 했을 때 바로 대응이 안 될 수 있다. 개인적으로 필요해서 만들었기 때문에 그렇게 된다면 리*북스로 넘어가야할 것 같다.</p>
</li>
<li><p>PDF 기반의 eBook은 지원이 안된다. PDF 파일 자체에 주석이 저장되고 그 내용을 데이터베이스에 저장하는 방식인 것 같아서 그것까지는 어떻게 할 수가 없었다.</p>
</li>
<li><p>Web을 지원하려고 했으나 데이터베이스를 읽어오는 부분에서 문제가 생겨서 PC 버전으로 만들게 됐다. 귀찮을 수 있지만 다운받아서 사용해야 한다.</p>
</li>
</ul>
<h2 id="heading-7zue6riw">후기</h2>
<p>Windows 프로그램을 만들어본 것은 처음이었지만 Flutter를 사용하니까 그냥 크로스플랫폼 앱 개발하듯이 매우 편하게 개발할 수 있었다. 후기에서는 이번에 시도해본 것들과 개발하면서 느낀점들을 간략하게 정리해보겠다.</p>
<ol>
<li><p>SQLite3의 Flutter Web 버전 지원 문제</p>
<ul>
<li><p>Web 버전으로 만들어서 모두가 사용할 수 있도록 할 예정이었는데 큰 자칠이 생겼다. WASM을 이용해서 컴파일된 SQLite3를 지원하지만 로컬 데이터베이스를 읽어서 로드하는 기능을 지원하지 않아서 Web 지원을 포기하게 됐다.</p>
</li>
<li><p>BLE 기능을 도입하는 프로젝트에서도 공식적인 라이브러리가 없어서 flutter 사용이 굉장히 힘들었는데 이번에는 native에 대한 이해가 얼마나 중요한지와 flutter의 단점을 여실히 느낄 수 있었다.</p>
</li>
</ul>
</li>
<li><p>Clean architecture(?)</p>
<ul>
<li><p>이번에 clean architecture를 도입해보고 riverpod 이라는 상태관리 라이브러리의 2.0 버전을 사용해봤다. 일단 두 가지 다 좋은 방법이라고 생각이 되지만 익숙하지 않으니 시간이 꽤 오래 걸린 것 같다. 특히, 이게 맞는 방법인지가 확실하지 않으니까 더 더뎠던 것 같다.</p>
</li>
<li><p>그리고 지금 프로젝트 규모에 비해서 너무 복잡한 설계를 사용한 것 같다. 추상화를 통해서 이득을 볼 부분을 없는 것 같은데 소 잡는 칼을 닭 잡는데 사용한 것 같은 꼴이었다.</p>
</li>
<li><p>개발 속도 빼고 불편했던 점 중 하나는 바로 IDE의 지원이다. Interface에 의존을 하다보니 구현체로 이동하기 위해서는 하나의 스텝을 더 밟아야 해서 생각보다 거슬렸다.</p>
</li>
<li><p>그래도 한 번 해보니까 추상화에 의존하게 되므로 구현을 수정해도 활용하는 부분이 영향을 받지 않는 다는 것은 확인할 수 있었다. 그래도 프로젝트 규모가 작아서 앞에서 말한 것처럼 그렇게 큰 효과는 보지 못한 것 같다.</p>
</li>
</ul>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[YesTakeout! - Yes24 독서노트 자유롭게 가져가세요]]></title><description><![CDATA[프로젝트 동기
Yes24 Ebook의 독서노트 내보내기 미지원
현재 Yes24의 크레마 클럽을 구독 중이다. 정말 너무 좋은 서비스이지만 가장 큰 문제는 독서노트를 다른 플랫폼으로 이동시킬 수 없다는 것이다. 리디북스의 경우에는 웹 기반으로 되어 있어서 자연스럽게 독서노트를 내보낼 수 있는데 Yes24는 지원하지 않는다는 것이 굉장히 아쉬운 부분이었다.
심지어 PC 버전에서는 하이라이트를 할 수 없으며 하이라이트가 표시도 되지 않는다. 메모도 ...]]></description><link>https://blog.aqudi.me/about-yestakeout-project</link><guid isPermaLink="true">https://blog.aqudi.me/about-yestakeout-project</guid><category><![CDATA[독서노트]]></category><category><![CDATA[예스24]]></category><category><![CDATA[이북]]></category><category><![CDATA[프로젝트]]></category><category><![CDATA[yes24]]></category><category><![CDATA[Applications]]></category><category><![CDATA[ebook]]></category><category><![CDATA[memo]]></category><category><![CDATA[note]]></category><category><![CDATA[e-book]]></category><dc:creator><![CDATA[Taejung Heo]]></dc:creator><pubDate>Sun, 24 Dec 2023 09:30:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1705234376367/6e78876a-2c07-4497-b4ac-4b77fe268af8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-7zse66gc7kcd7yq4ioupmeq4sa">프로젝트 동기</h2>
<h3 id="heading-yes24-ebook">Yes24 Ebook의 독서노트 내보내기 미지원</h3>
<p>현재 Yes24의 크레마 클럽을 구독 중이다. 정말 너무 좋은 서비스이지만 가장 큰 문제는 독서노트를 다른 플랫폼으로 이동시킬 수 없다는 것이다. 리디북스의 경우에는 웹 기반으로 되어 있어서 자연스럽게 독서노트를 내보낼 수 있는데 Yes24는 지원하지 않는다는 것이 굉장히 아쉬운 부분이었다.</p>
<p>심지어 PC 버전에서는 하이라이트를 할 수 없으며 하이라이트가 표시도 되지 않는다. 메모도 작성이 안 된다. (이 정도면 PC 버전은 완전히 버린 것이 아닌가 싶은 생각이 든다). 이쯤되면 포기하고 리디북스로 가는 것이 맞겠지만... 우리 Yes24 나쁘지 않은 서비스니까 또 사용하시는 분이 꽤 되는 것 같으니까 내가 한 번 방법을 찾아보게 됐다.</p>
<h3 id="heading-7ja065a76rkmio2vtoqyso2vocdsijgg7jeg7j2e6rmmpw">어떻게 해결할 수 없을까?</h3>
<p>최근에는 Obsidian이라는 툴에 관심이 생겨서 독서노트를 Obsidian에서 작성하고 싶어서 책을 다 까져오는 게 아니라 내가 하이라이트한 부분이랑 메모만 가져올 수 있으면 좋겠다는 생각에 Yes24 Ebook PC 프로그램을 좀 분석했다.</p>
<p>결과는 대성공! PC 버전에서는 지원하지 않는 기능이지만 내부적으로는 해당 정보들을 가지고 있는 것을 발견할 수 있었다. 다행히 간단하게 해결할 수 있는 부분인 것 같아서 주변에 필요한 기능들을 좀 물어보기도 하고 개인적으로도 정리해서 이 프로젝트를 시작했다! (생각보다 필요로 하시는 분들이 꽤 계신 것 같아서 두근두근하다.)</p>
<h2 id="heading-7zwe7jqu7zwcioq4soukpsdrsi8g7lac7iucioydvoyglq">필요한 기능 및 출시 일정</h2>
<p>간단하게 필요한 기능들을 중심으로 출시 일정을 정리해봤다.</p>
<ul>
<li><p>1차 출시: 2024년 1월 3일</p>
<ul>
<li><p>Yes24 Ebook에 작성한 메모나 하이라이트 등의 주석 추출</p>
</li>
<li><p>추출한 주석들을 디스플레이</p>
</li>
<li><p>추출한 주석들을 텍스트로 내보내기 기능</p>
</li>
</ul>
</li>
<li><p>2차 출시: 2024년 1월 13일</p>
<ul>
<li><p>추출한 주석들을 템플릿에 맞춰 내보내기 기능</p>
</li>
<li><p>추출한 주석들을 Obsidian으로 내보내기 기능</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-7is467aaioycro2vrq">세부 사항</h2>
<p>애플리케이션의 형태나 간단하게 필요한 도구들과 경험해보고 싶은 것들을 정리해봤다. 기능이 비해서 좀 과한 느낌이 없잖아 있지만 평소에 관심이 있던거라 슬쩍 끼워넣어봤다.</p>
<ul>
<li><p>애플리케이션 형태: 웹 애플리케이션</p>
</li>
<li><p>프레임워크: Flutter</p>
</li>
<li><p>클린아키텍쳐 써보면서 장단점 느껴보기</p>
<ul>
<li><p><a target="_blank" href="https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/">Code with andrea 튜토리얼 참고</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/rodydavis/clean-architecture-todo-app/tree/main">Drift clean architecture 튜토리얼 참고</a></p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://drift.simonbinder.eu/">Dart 언어로 작성된 ORM drift</a> 사용해보기</p>
</li>
<li><p><a target="_blank" href="https://riverpod.dev/docs/concepts/about-code-generation/">Riverpod2.0에서 소개된 riverpod generator</a> 사용해보기</p>
</li>
</ul>
]]></content:encoded></item></channel></rss>