<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>POSTMATH Tech Blog | 포스트매스 기술 블로그</title><description>수학비서를 만드는 포스트매스 팀의 기술·디자인·프로덕트 이야기</description><link>https://tech.postmath.co.kr/</link><language>ko-kr</language><lastBuildDate>Mon, 28 Sep 2026 02:22:15 GMT</lastBuildDate><ttl>60</ttl><item><title>Next.js 앱을 GCS에 올리기까지: 디자이너의 배포 회고 #3</title><link>https://tech.postmath.co.kr/product/landing-claude-code-3/</link><guid isPermaLink="true">https://tech.postmath.co.kr/product/landing-claude-code-3/</guid><description>기존 파이프라인에서 막히고, 정적 export로 돌아오기까지</description><pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;이번 작업에서 의외로 시간을 많이 쓴 건 페이지를 만드는 것보다 실제로 서비스에 올리는 과정이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-1/&quot;&gt;1편&lt;/a&gt;과 &lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-2/&quot;&gt;2편&lt;/a&gt;에서 다룬 것처럼 1주일 동안 5개 페이지를 만들었습니다.
하지만 프로덕션에 올리고 안정화하는 과정은 또 다른 문제였습니다.&lt;/p&gt;
&lt;h2 id=&quot;작업-흐름-한눈에&quot;&gt;작업 흐름 한눈에&lt;/h2&gt;
&lt;p&gt;배포까지의 전체 흐름을 6단계로 압축하면 이렇습니다.&lt;/p&gt;
&lt;pre&gt;1. 피그마 시안 확정
     ↓
2. 로컬에서 섹션별 구현 + GitHub push
     ↓
3. Vercel 프리뷰로 팀 리뷰
     ↓
4. &amp;quot;프로덕션에 어떻게 올리나요?&amp;quot; — 첫 협업
     ↓
5. next build + 정적 export → GCS 업로드
     ↓
6. &lt;a href=&quot;https://www.mathsecr.com/landing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;a href=&quot;https://www.mathsecr.com/landing/&quot;&gt;https://www.mathsecr.com/landing/&lt;/a&gt;&lt;/a&gt; 프로덕션 반영&lt;/pre&gt;
&lt;p&gt;1~3단계(만들기 → 푸시 → 프리뷰)는 앞선 글에서 다뤘습니다.
이번 글의 진짜 시작은 4단계 배포부터였습니다.&lt;/p&gt;
&lt;h2 id=&quot;프로덕션-어떻게-올리나요&quot;&gt;프로덕션, 어떻게 올리나요?&lt;/h2&gt;
&lt;p&gt;Vercel 프리뷰가 잘 떴으니 그대로 프로덕션 도메인에 연결하면 끝날 줄 알았습니다. 참고로 기존 랜딩은 Figma Site에서 별도로 만들어 운영하고 있었어요. 그런데 프론트엔드 동료에게 물어보니 답이 의외였어요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“현행은 Figma Site를 크롤링해서 GCS에 올리는 구조예요. 기존 방식대로 정적 파일로 만들어 GCS에 올려주시면 돼요.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;문제는 단순했습니다. &lt;strong&gt;제가 만든 Next.js 구조와 회사의 기존 정적 배포 구조가 맞지 않았습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;table-callout&quot;&gt;






















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;빌드&lt;/th&gt;&lt;th&gt;출력&lt;/th&gt;&lt;th&gt;호스팅&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;기존 운영&lt;/td&gt;&lt;td&gt;Figma Site 크롤링&lt;/td&gt;&lt;td&gt;정적 파일&lt;/td&gt;&lt;td&gt;GCS + CDN&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;내가 만든 것&lt;/td&gt;&lt;td&gt;Next.js 빌드&lt;/td&gt;&lt;td&gt;???&lt;/td&gt;&lt;td&gt;GCS???&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Next.js 앱은 보통 Node.js 서버나 Vercel이 필요한데 회사는 GCS 정적 호스팅에 묶여 있었어요. 이 둘을 어떻게 연결할지가 새로운 문제였습니다.&lt;/p&gt;
&lt;h2 id=&quot;정적-export--gcs-직접-업로드&quot;&gt;정적 export + GCS 직접 업로드&lt;/h2&gt;
&lt;p&gt;처음엔 기존 크롤러 파이프라인에 끼워 넣을 수 있지 않을까 생각했지만 크롤러는 Figma Site 같은 단순 정적 페이지용이라 Next.js 앱은 처리할 수 없었습니다. 기술적으로 되는 것과 우리 회사 환경에서 되는 건 달랐습니다.&lt;/p&gt;
&lt;p&gt;결국 가장 단순한 방법을 택했습니다. Next.js를 정적 export로 빌드한 결과물(HTML, JS, CSS, 이미지)을 GCS에 직접 복사하는 방식입니다. 빌드 → 파일 복사 → 압축까지 한 번에 처리하는 스크립트도 함께 만들었습니다.&lt;/p&gt;
&lt;figure class=&quot;figure&quot; style=&quot;margin:48px 0&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/static-export-pipeline.C5xUTCwo_Z45Lew.webp&quot; alt=&quot;배포 파이프라인 다이어그램 — Next.js 코드(소스 코드 작성) → next build(정적 export) → 정적 파일 묶음(HTML·JS·CSS·이미지) → GCS 버킷 업로드(정적 호스팅 배포) 4단계가 빨간 화살표로 연결되어 있다.&quot; style=&quot;background:transparent;border-radius:16px&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1626&quot; height=&quot;539&quot;&gt;&lt;/figure&gt;
&lt;p&gt;한 가지 추가 허들이 있었습니다. FAQ 페이지는 Google Sheets에서 실시간으로 질문/답변을 관리하는 구조인데 정적 export로 바꾸면서 SSR이 사라져 시트 수정이 반영되지 않았습니다. SEO보다 실시간성이 더 중요했기에 서버 fetch를 클라이언트 useEffect로 옮기고 스켈레톤 UI를 넣어 해결했습니다.&lt;/p&gt;
&lt;h2 id=&quot;협업과-환경&quot;&gt;협업과 환경&lt;/h2&gt;
&lt;p&gt;배포까지 며칠 동안 동료분과 여러번 협업했어요. Vercel 프로젝트 확인, 프리뷰 검토, 배포 방법 문의, GCS 업로드 요청.&lt;/p&gt;
&lt;p&gt;특히 “프로덕션 배포는 어떻게 진행하면 될까요?” 라는 질문 한 번이 그 후 작업의 방향을 결정했습니다. 디자이너가 직접 만든다는 건 개발자와 협업하지 않는다는 뜻이 아니었어요. &lt;strong&gt;협업 횟수는 줄었지만, 협업하는 내용 자체가 달라진 거였습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;배포까지 거친 환경은 이렇습니다.&lt;/p&gt;
&lt;div class=&quot;table-callout&quot;&gt;
































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;단계&lt;/th&gt;&lt;th&gt;환경&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;개발&lt;/td&gt;&lt;td&gt;Next.js 개발 서버 (&lt;code&gt;npm run dev&lt;/code&gt;로 &lt;code&gt;localhost:3000&lt;/code&gt;에서 실행)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;프리뷰&lt;/td&gt;&lt;td&gt;Vercel 자동 배포&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;정적 export&lt;/td&gt;&lt;td&gt;next build + 자동화 스크립트&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;로컬 검증&lt;/td&gt;&lt;td&gt;정적 파일을 호스팅하는 서버 (&lt;code&gt;preview_server.py&lt;/code&gt;로 &lt;code&gt;localhost:8000&lt;/code&gt;에서 실행)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;최종 저장소&lt;/td&gt;&lt;td&gt;내부 GCS 버킷&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;프로덕션&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.mathsecr.com/landing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;a href=&quot;https://www.mathsecr.com/landing/&quot;&gt;https://www.mathsecr.com/landing/&lt;/a&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;이 모든 이름을 1주일 전엔 몰랐어요. 알고 나니 전체 흐름이 보이기 시작했습니다.&lt;/p&gt;
&lt;h3 id=&quot;git은-생각보다-성실했다&quot;&gt;Git은 생각보다 성실했다&lt;/h3&gt;
&lt;p&gt;한 번은 동료에게 슬랙이 왔습니다.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/slack-gitignore.Dlvs_xx5_ZyXcKf.webp&quot; alt=&quot;슬랙 대화 캡처. 동료가 &apos;landing-page-code.zip 파일과 스크린샷 파일을 작업 경로에서 제외해달라&apos;고 요청하고, &apos;깃은 경로에 있고 별도로 예외처리 안해주면 같이 올라간다&apos;고 친절히 설명해주는 내용.&quot; style=&quot;border-radius:14px&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1548&quot; height=&quot;656&quot;&gt;&lt;/figure&gt;
&lt;p&gt;작업 폴더에 있던 zip 파일과 스크린샷까지 함께 push되고 있었습니다.
그날 처음 알았습니다. 주의하지 않으면 AI가 &lt;code&gt;git add .&lt;/code&gt;처럼 전체 파일을 통째로 올리는 명령을 써서 같은 경로의 파일이 모두 Git에 올라간다는 걸요.&lt;/p&gt;
&lt;p&gt;사소한 해프닝이었지만, 시안만 넘겼으면 평생 몰랐을 걸 배운 순간이었어요.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/first-push.C713mKkY_lNctG.webp&quot; alt=&quot;GitHub Pull Request 화면. 제목은 &apos;feat: 랜딩페이지 반응형 리뉴얼 및 모바일 메뉴 추가&apos;, Summary와 주요 변경사항이 한국어로 자동 정리되어 있습니다.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;3452&quot; height=&quot;1920&quot;&gt;&lt;p class=&quot;caption&quot;&gt;디자이너가 처음으로 올린 PR. 요약은 Claude가 자동으로 정리해줬습니다.&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;이후 단계는 동료가 이어서 진행했습니다. 빌드 결과물을 GCS 버킷에 올리는 작업은 프로덕션 환경에 직접 접근해야 했고 보안상 service account 키가 필요했습니다.&lt;/p&gt;
&lt;p&gt;이어서 CDN 캐시를 비우는 작업도 필요했습니다. 새로 올린 파일이 즉시 반영되도록 기존 캐시를 무효화하는 과정인데 잘못 건드리면 서비스 전체에 영향을 줄 수 있는 작업이었습니다.&lt;/p&gt;
&lt;p&gt;두 작업 모두 프로덕션에 직접 영향을 주는 작업이라 이후 단계는 개발자가 맡아 진행했습니다.&lt;/p&gt;
&lt;h2 id=&quot;디자이너가-배포까지-해본다는-것&quot;&gt;디자이너가 배포까지 해본다는 것&lt;/h2&gt;
&lt;p&gt;배포까지 끝내고 나서 가장 크게 든 생각은 이거였어요.&lt;/p&gt;
&lt;h3 id=&quot;디자인이-끝나고-나서가-진짜-시작이다&quot;&gt;디자인이 끝나고 나서가 진짜 시작이다&lt;/h3&gt;
&lt;p&gt;운영까지 올리는 동안 매 순간 “이걸 굳이 디자이너가 해야 하나” 싶었어요. 옆에 있는 개발자가 30분이면 풀 일을 며칠씩 헤매고 있었으니까요.&lt;/p&gt;
&lt;p&gt;특히 막막했던 건 작업이 끝났는지조차 모를 때였습니다. 디자인은 “이 화면은 완성”이라는 명확한 종착점이 있는데 배포는 “여기까지 하면 끝”이라는 선이 모호해요. 빌드가 잘 되는지, 정적 파일이 다 옮겨졌는지, 캐시가 잘 무효화되는지 — 매 단계가 새로운 점검이었습니다.&lt;/p&gt;
&lt;p&gt;그런데 끝나고 보니 세 가지가 남았습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;첫째, 디자인 의도가 끝까지 유지됐습니다.&lt;/strong&gt;&lt;br/&gt;
시안 → 구현 → QA의 단계마다 의도가 깎이는 일반적인 흐름과 달리 처음부터 끝까지 제 머릿속의 디자인이 그대로 운영에 올라갔어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;둘째, 구현 관점에서 판단하는 힘이 생겼습니다.&lt;/strong&gt;&lt;br/&gt;
시안 단계에서 이미 구현 비용을 알면 구현 가능한 디자인을 더 빠르게 만들 수 있어요. 이건 다음 디자인 결정에 직접 영향을 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;셋째, 개발자와 대화하는 언어가 달라졌습니다.&lt;/strong&gt;&lt;br/&gt;
“이거 가능해요?”라고 묻기 전에 “이건 정적 export로 처리되니까 클라이언트 fetch로 가야겠죠?”라고 먼저 던질 수 있게 됐어요.&lt;/p&gt;
&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;p&gt;이번 경험을 통해 깨달은 것은 직접 배포를 한다고 해서 개발자와의 협업이 사라지는 게 아니라는 점입니다.&lt;/p&gt;
&lt;p&gt;오히려 대화의 깊이가 달라졌습니다.&lt;/p&gt;
&lt;p&gt;시안의 픽셀을 맞추는 대화 대신 인프라의 제약을 공유하고 최적의 경로를 결정하는 대화를 나눴으니까요. 배포는 단순히 파일을 올리는 게 아니라, 만든 걸 실제로 사용자한테 보여주기까지의 마지막 과정이었습니다.&lt;/p&gt;
&lt;p&gt;이 과정이 다른 디자이너분들에게도 작은 도움이 되었으면 좋겠습니다. 다음엔 새롭게 리뉴얼된 수학비서로 찾아뵐게요. 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;시리즈 전체 보기&lt;/strong&gt;&lt;/p&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;01&lt;/span&gt;
&lt;span style=&quot;color:rgb(121, 121, 136)&quot;&gt;  &lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-1/&quot;&gt;디자이너가 Claude Code로 랜딩 페이지 직접 배포하기 #1&lt;/a&gt;&lt;/span&gt;
&lt;br/&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;02&lt;/span&gt;
&lt;span style=&quot;color:rgb(121, 121, 136)&quot;&gt;  &lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-2/&quot;&gt;Figma → Code, 디자이너가 직접 옮길 때 알아야 할 것들 #2&lt;/a&gt;&lt;/span&gt;
&lt;br/&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;03&lt;/span&gt;
&lt;span style=&quot;color:rgb(25, 25, 28)&quot;&gt;  Next.js 앱을 GCS에 올리기까지: 디자이너의 배포 회고 #3&lt;/span&gt;</content:encoded><dc:creator>고유현</dc:creator><category>Next.js</category><category>GCS</category><category>Vercel</category><category>배포</category><category>static export</category><category>디자이너</category></item><item><title>Figma → Code, 디자이너가 직접 옮길 때 알아야 할 것들 #2</title><link>https://tech.postmath.co.kr/product/landing-claude-code-2/</link><guid isPermaLink="true">https://tech.postmath.co.kr/product/landing-claude-code-2/</guid><description>Figma MCP의 한계와 디자이너의 6가지 원칙</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-1/&quot;&gt;1편&lt;/a&gt;에서 실패를 정리했다면 이번 편은 그 실패에서 뽑아낸 패턴과 실전 노하우입니다.&lt;/p&gt;
&lt;h2 id=&quot;클로드가-자주-놓친-것들&quot;&gt;클로드가 자주 놓친 것들&lt;/h2&gt;
&lt;p&gt;Figma MCP는 값을 읽긴 했지만 간격·색상·반응형 기준을 자주 다르게 해석했습니다.&lt;/p&gt;
&lt;div class=&quot;figure-grid&quot; style=&quot;grid-template-columns:1fr 1fr;margin:32px 0&quot;&gt;&lt;div style=&quot;background:transparent;padding:0;flex-direction:column&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/example-figma._ja0C10u_Z1wktg4.webp&quot; alt=&quot;피그마 시안 — 카드 3개가 가지런히 배치되고 우측 상단 화살표 일러스트가 의도된 위치에 놓여 있다.&quot; style=&quot;background:transparent;border-radius:12px&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1749&quot; height=&quot;1552&quot;&gt;&lt;p class=&quot;caption&quot;&gt;피그마&lt;/p&gt;&lt;/div&gt;&lt;div style=&quot;background:transparent;padding:0;flex-direction:column&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/example-claude.CQJ2cPho_iQ89U.webp&quot; alt=&quot;Claude 구현 결과 — 카드가 2개만 렌더링되고 폭이 더 넓게 잡혔으며, 일러스트 위치도 단순화됐다.&quot; style=&quot;background:transparent;border-radius:12px&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1749&quot; height=&quot;1552&quot;&gt;&lt;p class=&quot;caption&quot;&gt;클로드가 읽은 결과&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;table-callout&quot;&gt;


































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;피그마&lt;/th&gt;&lt;th&gt;클로드가 읽은 결과&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;간격&lt;/td&gt;&lt;td&gt;&lt;code&gt;gap: 32px&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;gap: 24px&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;반응형 분기&lt;/td&gt;&lt;td&gt;모바일/태블릿/데스크탑 각각 다른 값&lt;/td&gt;&lt;td&gt;셋이 섞여서 적용&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;텍스트 크기&lt;/td&gt;&lt;td&gt;&lt;code&gt;40px&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;32px&lt;/code&gt; (정해진 토큰 무시)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;색상&lt;/td&gt;&lt;td&gt;디자인 토큰 사용&lt;/td&gt;&lt;td&gt;엉뚱한 hex 하드코딩&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;이미지 위치&lt;/td&gt;&lt;td&gt;&lt;code&gt;right: -29px&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;right: 0&lt;/code&gt;으로 단순화&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;특히 &lt;strong&gt;이미지 위치가 단순화&lt;/strong&gt;되면서 문제가 발생했습니다.
디자인 균형을 위해 의도적으로 이미지 위치를 오른쪽으로 29px 조정했는데 해당 설정이 사라지면서 디자인 의도가 사라졌습니다.&lt;/p&gt;
&lt;p&gt;같은 값을 세 번 다르게 가져오는 일도 잦았어요. 한 번 읽어서는 믿을 수 없었습니다. 그래서 &lt;strong&gt;노드 ID를 직접 찍어주는 방식&lt;/strong&gt;이 가장 안전했어요. “이 프레임 안의 오른쪽 섹션”처럼 말로 설명하면 엉뚱한 곳을 보지만 레이어 우클릭 후 Copy link로 얻은 &lt;code&gt;node-id&lt;/code&gt; 값을 전달하면 훨씬 정확하게 읽어옵니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 잘못된 프롬프트 (자주 틀림)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;quot;이 페이지 오른쪽 위 섹션의 카드 디자인 가져와&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 정확한 프롬프트&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;quot;node-id=66:34264 노드의 디자인 컨텍스트를 가져와&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;수정이 잦은 데는 몇 가지 패턴이 있었어요.&lt;/p&gt;
&lt;div class=&quot;checklist-callout&quot;&gt;&lt;h3&gt;수정이 잦았던 이유 4가지&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;AI의 ‘대충 비슷하게’ 한계&lt;/strong&gt;&lt;span&gt;90%는 맞아 보이는데 디자이너 눈에는 어색한 10%가 보임.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;반응형이 특히 어려움&lt;/strong&gt;&lt;span&gt;데스크탑은 멀쩡한데 태블릿에서 갑자기 튄다..&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Tailwind 기본 색상 쓰는 버릇&lt;/strong&gt;&lt;span&gt;bg-white 같은 기본 클래스를 자꾸 씁니다. 서비스 고유 토큰이 Tailwind 기본 수치와 미세하게 다를 때가 많아, 정밀도를 위해 bg-[#ffffff] 같은 Arbitrary value를 썼습니다.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;framer-motion의 함정&lt;/strong&gt;&lt;span&gt;모바일에서만 안 보이는 섹션이 있는데 AI가 알아서 못찾음.&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;
&lt;p&gt;그러다 보니 나름의 원칙이 6개 생겼습니다.&lt;/p&gt;
&lt;h2 id=&quot;6가지-원칙&quot;&gt;6가지 원칙&lt;/h2&gt;
&lt;h3 id=&quot;원칙-1-피그마를-먼저-정리하세요&quot;&gt;원칙 1. 피그마를 먼저 정리하세요&lt;/h3&gt;
&lt;p&gt;클로드에게 던지기 전에 피그마 파일부터 정돈이 필요합니다. 레이어 이름 제대로(group4 같은 이름 금지), 오토레이아웃 꼼꼼하게, 디자인 토큰(컬러/타이포) 일관되게, 브레이크포인트별 프레임 분리 등.&lt;/p&gt;
&lt;div style=&quot;background:var(--bg-quote);border-radius:14px;padding:24px;margin:32px 0&quot;&gt;&lt;div class=&quot;figure-grid&quot; style=&quot;grid-template-columns:1fr 1fr;margin:0;gap:20px&quot;&gt;&lt;div style=&quot;background:transparent;padding:0;flex-direction:column&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/layer-naming-messy.CtP-_obz_1uoFke.webp&quot; alt=&quot;피그마 레이어 트리 — 모든 프레임이 &apos;Frame 2147239268&apos;, &apos;Frame 2147239343&apos; 같은 자동 생성된 의미 없는 이름으로 남아 있다.&quot; style=&quot;background:transparent;border-radius:12px&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1206&quot; height=&quot;1368&quot;&gt;&lt;p class=&quot;caption&quot;&gt;정리 전&lt;/p&gt;&lt;/div&gt;&lt;div style=&quot;background:transparent;padding:0;flex-direction:column&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/layer-naming-clean.69qY3oih_Z18XxYN.webp&quot; alt=&quot;피그마 레이어 트리 — Section/Hero, Hero/Content-Wrapper, Hero/Visual-Group, Section/Features 처럼 역할 기반의 의미 있는 이름으로 정리되어 있다.&quot; style=&quot;background:transparent;border-radius:12px&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1206&quot; height=&quot;1368&quot;&gt;&lt;p class=&quot;caption&quot;&gt;정리 후&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;정리되지 않은 피그마를 던지면 AI는 추측에 기반하여 작업합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Figma 변수(Variables)는 CSS Variable로 export됐을 때 깨지는 경우가 있습니다. 색상 변수만 export하고 간격/타이포 변수는 직접 코드에 박는 방식이 안전했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;원칙-2-전체-말고-조각으로-만드세요&quot;&gt;원칙 2. 전체 말고 조각으로 만드세요&lt;/h3&gt;
&lt;p&gt;“전체 랜딩페이지 만들어줘”라고 하면 결과가 엉망입니다. 섹션 단위로 쪼개서 하나씩 눈으로 확인하며 진행해야 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;“섹션 단위”의 정의가 핵심입니다. 시각적 단위(히어로/카드/푸터)가 아니라 &lt;strong&gt;상태/로직 단위&lt;/strong&gt;로 쪼개야 합니다. 예: 리뷰 캐러셀은 시각적으론 한 섹션이지만 “자동 슬라이드 + 호버 일시정지 + 인디케이터 호버 전환” 3개 로직이 들어가서 따로 만들고 합치는 게 안전했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;원칙-3-감각-말고-숫자로-말하세요&quot;&gt;원칙 3. 감각 말고 숫자로 말하세요&lt;/h3&gt;
&lt;p&gt;“조금 더 왼쪽으로”보다 “20px 왼쪽으로”가 백 배 정확합니다. AI는 모호한 표현을 잘 해석하지 못합니다. 디자이너가 좋아하는 “여백이 좀 답답해”나 “톤이 애매해” 같은 표현은 클로드가 알아듣지 못합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;포지셔닝은 &lt;code&gt;left-[Xpx]&lt;/code&gt;가 아니라 &lt;code&gt;right-[Xpx]&lt;/code&gt;로 명시해야 합니다. 화면이 좁아질 때 우측 일러스트가 좌측으로 밀려들어가는 사고를 막을 수 있어요!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;원칙-4-디자이너의-눈으로-검증하기&quot;&gt;원칙 4. 디자이너의 눈으로 검증하기&lt;/h3&gt;
&lt;p&gt;코드가 정상적으로 동작한다고 해서 디자인이 정확하게 구현된 것은 아닙니다. 모든 브레이크포인트에서 실제 화면을 확인하고 피그마와 나란히 비교하며 디테일을 점검해야 합니다. 결국 마지막 디테일은 디자이너가 직접 눈으로 잡아야 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;viewport별 캡처를 매번 손으로 안 찍고 Playwright로 자동화하면 작업이 훨씬 빨라집니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;원칙-5-수정-전에-원인을-먼저-물어보세요&quot;&gt;원칙 5. 수정 전에 원인을 먼저 물어보세요&lt;/h3&gt;
&lt;p&gt;AI가 만든 코드가 이상하게 동작할 때 바로 “다시 만들어줘”라고 하지 말고 &lt;strong&gt;먼저 원인을 묻는 게 더 빠릅니다.&lt;/strong&gt; 모바일에서 요소가 안 보이면 “왜 안 보여?”라고 물어보면 AI가 원인을 분석해줍니다. 원인을 알아야 제대로 고칠 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;원칙-6-한글-폰트-확인하기&quot;&gt;원칙 6. 한글 폰트 확인하기&lt;/h3&gt;
&lt;p&gt;AI는 영문 타이포에 비해 한글 처리가 약합니다. 자간(&lt;code&gt;tracking&lt;/code&gt;), 행간(&lt;code&gt;leading&lt;/code&gt;), 폰트 패밀리 fallback 등을 디자이너가 직접 지정해줘야 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pretendard에서 &lt;strong&gt;한글 weight는 영문과 시각적 무게가 다릅니다.&lt;/strong&gt; font-medium(500)이 영문에서는 적당해 보여도 한글에서는 가늘게 보입니다. 데스크탑 헤딩은 semibold(600), 모바일은 medium(500) 식으로 브레이크포인트별로 weight를 다르게 잡아야 시각 균형이 맞았어요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;1-codex와-병행하기&quot;&gt;+1. Codex와 병행하기&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claude Code만 쓰면 토큰이 금방 바닥납니다.&lt;/strong&gt; 한 페이지 작업 중 컨텍스트가 누적되면 세션을 새로 열어야 하고 그때마다 프로젝트 컨텍스트를 처음부터 다시 알려줘야 했어요. 그래서 중간부터 &lt;strong&gt;Codex와 병행&lt;/strong&gt;해서 작업했습니다.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/codex-claude.Cbj46RrQ_ZOeDmb.webp&quot; alt=&quot;Codex와 Claude Code를 한 화면에 띄워두고 병행 작업하는 터미널 스크린샷&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;3456&quot; height=&quot;2087&quot;&gt;&lt;p class=&quot;caption&quot;&gt;제법 개발자 같죠? Claude Code 사용량이 금방 닳아서 Codex도 같이 띄워두고 작업했습니다.&lt;/p&gt;&lt;/figure&gt;
&lt;div class=&quot;table-callout&quot;&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;비교&lt;/th&gt;&lt;th&gt;Claude Code&lt;/th&gt;&lt;th&gt;Codex&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;강점&lt;/td&gt;&lt;td&gt;Figma MCP 연동, 긴 추론, 디자인 의도 잡기&lt;/td&gt;&lt;td&gt;빠른 응답, 토큰 효율, 단순 코드 작업&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;약점&lt;/td&gt;&lt;td&gt;토큰 누적 빠름, 비용 부담&lt;/td&gt;&lt;td&gt;Figma MCP 연동 약함, 긴 맥락 못 잡음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;실제 작업&lt;/td&gt;&lt;td&gt;새 섹션 만들기, 디자인 의사결정, 디버깅&lt;/td&gt;&lt;td&gt;반복 코드, 단순 수정, 타입 정리, 리팩터링&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;결국 하나만 고집하기보다 역할별로 나눠 쓰는 게 훨씬 빨랐습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;보너스-모션도-만들-수-있을까&quot;&gt;보너스: 모션도 만들 수 있을까?&lt;/h2&gt;
&lt;p&gt;원칙을 정리하다가 한 번 더 시험해보고 싶었어요. &lt;strong&gt;Claude Code가 CSS 모션도 잘 만들까?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;간단한 프롬프트 한 줄을 던져봤습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;공이 무한 계단을 통통 튀면서 올라가는 모션 만들어줘.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결과가 한 번에 깔끔하게 나왔습니다. 단일 HTML 파일에 CSS keyframes + SVG만 써서 의도한 모션이 그대로 구현됐어요.&lt;/p&gt;
&lt;iframe src=&quot;https://tech.postmath.co.kr/demo/landing-blog-02/stairs-bounce.html&quot; title=&quot;공이 계단을 오르는 CSS 모션&quot; loading=&quot;lazy&quot; style=&quot;width:100%;height:420px;border:0;border-radius:12px;display:block;margin:24px 0;background:#161616&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;머릿속으로 그려지는 움직임이면 AI한테 시켜도 되겠다는 감이 왔어요.&lt;/p&gt;
&lt;h2 id=&quot;자주-쓴-프롬프트-3가지&quot;&gt;자주 쓴 프롬프트 3가지&lt;/h2&gt;
&lt;p&gt;원칙은 외우기 어렵지만 실제로 매일 썼던 프롬프트는 몇 개 안 됐어요. 그중에서도 가장 자주 쓴 3개를 공유합니다. &lt;code&gt;[대괄호]&lt;/code&gt; 안만 본인 상황에 맞게 채우면 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;1-새-섹션-추가&quot;&gt;1. 새 섹션 추가&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;랜딩페이지 [/landing/web]에 새 섹션을 추가할게.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;섹션 이름: [이름]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;피그마 노드 ID: [ID]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;위치: 기존 [섹션명] 바로 아래&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Figma MCP로 3번 읽어서 값을 교차 검증한 뒤,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;반응형 3단계(모바일/태블릿/데스크탑)에 맞게 구현해줘.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;색상은 Tailwind 기본 클래스 쓰지 말고 hex 값으로 지정해줘.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;예: bg-white (X), bg-[#ffffff] (O)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-에러-디버깅-고쳐줘가-아니라-원인부터&quot;&gt;2. 에러 디버깅 (“고쳐줘”가 아니라 “원인부터”)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[증상 설명] 현상이 발생해.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;원인을 먼저 분석해서 알려줘. 수정은 원인 파악한 뒤에 할게.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예: “모바일에서 이 섹션이 아예 안 보여. 원인이 뭘까?”&lt;/p&gt;
&lt;p&gt;가장 자주 쓰던 디버깅 프롬프트입니다. “고쳐줘”라고 하면 같은 실수를 반복하지만 “왜 안 돼?”라고 물으면 원인을 분석해줘서 제대로 고쳐집니다.&lt;/p&gt;
&lt;h3 id=&quot;3-텍스트만-수정-스타일-안-건드리게&quot;&gt;3. 텍스트만 수정 (스타일 안 건드리게)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[/landing/web] 페이지의 히어로 카피를 수정할게.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;기존: &amp;quot;[기존 카피]&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;변경: &amp;quot;[새 카피]&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;해당 부분만 바꿔주고, 다른 스타일은 건들지 말아줘.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;“카피만 바꿔줘”라고만 하면 옆에 있는 패딩이나 색상까지 손대는 경우가 잦았어요. 범위를 명시하는 게 안전합니다.&lt;/p&gt;
&lt;h2 id=&quot;자주-만난-함정-2가지&quot;&gt;자주 만난 함정 2가지&lt;/h2&gt;
&lt;h3 id=&quot;함정-1-모바일에서-요소가-안-보일-때&quot;&gt;함정 1. 모바일에서 요소가 안 보일 때&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;증상:&lt;/strong&gt; framer-motion 애니메이션 섹션이 모바일에서 렌더링 안 됨&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;원인:&lt;/strong&gt; 99%는 &lt;code&gt;opacity: 0&lt;/code&gt; 초기값 + &lt;code&gt;whileInView&lt;/code&gt; 조합 문제. 모바일에서 viewport 감지가 한 박자 늦으면 &lt;code&gt;once: true&lt;/code&gt; 옵션 때문에 영영 활성화 안 돼서 opacity 0으로 멈춥니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;framer-motion의 initial 값에서 opacity를 제거해줘.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;whileInView의 viewport 옵션을 { once: true, amount: 0.1 }로 설정해줘.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;함정-2-nextjs-image가-우측-정렬-안-될-때&quot;&gt;함정 2. Next.js Image가 우측 정렬 안 될 때&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;증상:&lt;/strong&gt; &lt;code&gt;right-0&lt;/code&gt;, &lt;code&gt;position: absolute&lt;/code&gt; 설정했는데 왼쪽에 붙음&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;원인:&lt;/strong&gt; Next.js Image의 &lt;code&gt;fill&lt;/code&gt; prop이 &lt;code&gt;left: 0&lt;/code&gt;을 강제로 지정함&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Next.js Image 컴포넌트에 !left-auto 클래스를 추가해줘.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;p&gt;만드는 법을 익히는 데는 1주일,
운영에 올리는 법을 배우는 데는 며칠이 더 걸렸습니다.
다음 글에서는 만든 결과물을 실제 서비스에 올리기까지 겪은 배포 기록을 정리합니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;시리즈 전체 보기&lt;/strong&gt;&lt;/p&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;01&lt;/span&gt;
&lt;span style=&quot;color:rgb(121, 121, 136)&quot;&gt;  &lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-1/&quot;&gt;디자이너가 Claude Code로 랜딩 페이지 직접 배포하기 #1&lt;/a&gt;&lt;/span&gt;
&lt;br/&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;02&lt;/span&gt;
&lt;span style=&quot;color:rgb(25, 25, 28)&quot;&gt;  Figma → Code, 디자이너가 직접 옮길 때 알아야 할 것들 #2&lt;/span&gt;
&lt;br/&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;03&lt;/span&gt;
&lt;span style=&quot;color:rgb(121, 121, 136)&quot;&gt;  &lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-3/&quot;&gt;Next.js 앱을 GCS에 올리기까지: 디자이너의 배포 회고 #3&lt;/a&gt;&lt;/span&gt;</content:encoded><dc:creator>고유현</dc:creator><category>Claude Code</category><category>Codex</category><category>Figma MCP</category><category>Tailwind</category><category>framer-motion</category><category>AI 페어 프로그래밍</category></item><item><title>디자이너가 Claude Code로 랜딩 페이지 직접 배포하기 #1</title><link>https://tech.postmath.co.kr/product/landing-claude-code-1/</link><guid isPermaLink="true">https://tech.postmath.co.kr/product/landing-claude-code-1/</guid><description>수학비서 랜딩의 AI 전환기: iframe을 걷어내고 SSR을 입히다</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 수학비서 프로덕트 디자이너 고유현입니다.&lt;/p&gt;
&lt;p&gt;네이버 검색에 노출되지 않던 랜딩 페이지를 해결하기 위해, 개발 리소스 없이 Claude Code와 함께 2주 만에 새 랜딩 페이지를 구축하고 배포했습니다. 그 과정에서 AI가 디자이너의 작업 방식에 실제로 얼마나 도움이 되는지 직접 확인해봤습니다.&lt;/p&gt;
&lt;p&gt;잘된 점과 어려웠던 점, 그 과정에서 얻은 협업 원칙을 3편에 걸쳐 정리했습니다.&lt;/p&gt;
&lt;h2 id=&quot;배경&quot;&gt;배경&lt;/h2&gt;
&lt;p&gt;수학비서는 방대한 문제 데이터와 AI로 수학 선생님의 시간을 줄여주는 AI 기반 수학 교육 콘텐츠 플랫폼입니다.&lt;/p&gt;
&lt;p&gt;문제는 랜딩 페이지가 네이버 검색에 노출되지 않는다는 점이었습니다. 기존 랜딩 페이지는 Figma Site를 iframe으로 띄우는 구조였고, 검색 엔진이 iframe 내부 콘텐츠를 본문으로 충분히 인식하지 못하고 있었습니다. 구글에서는 일부 노출됐지만, 네이버에서는 사실상 미노출 상태였습니다.&lt;/p&gt;
&lt;p&gt;해결책은 명확했습니다. 랜딩 페이지를 Next.js 기반 SSR 구조로 다시 만드는 것.&lt;/p&gt;
&lt;p&gt;하지만 그 일을 맡길 개발 리소스는 없었습니다.&lt;/p&gt;
&lt;p&gt;그래서 제가 직접 Claude Code를 활용해 만들기로 했습니다.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/main-landing.Cvq1SZUV_mSGOa.webp&quot; alt=&quot;디자이너가 Claude Code와 함께 만든 수학비서 메인 랜딩페이지&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1680&quot; height=&quot;880&quot;&gt;&lt;p class=&quot;caption&quot;&gt;수학비서 메인 랜딩페이지&lt;/p&gt;&lt;/figure&gt;
&lt;h2 id=&quot;ai가-다-해줄-거라는-착각&quot;&gt;AI가 다 해줄 거라는 착각&lt;/h2&gt;
&lt;p&gt;작업을 시작하기 전, 머릿속 그림은 단순했습니다.&lt;/p&gt;
&lt;p&gt;“피그마 링크 던지면 알아서 코드 만들어주고, ‘여기 좀 다듬어줘’ 하면 알아서 다듬어주고, ‘반응형으로’ 하면 모바일·태블릿·데스크탑 다 맞춰주겠지?”&lt;/p&gt;
&lt;p&gt;이미 디자인은 완성되어 있었기 때문에 그대로 옮기기만 하면 끝날 것이라 생각했습니다.&lt;/p&gt;
&lt;p&gt;첫 프롬프트도 단순했습니다.&lt;/p&gt;
&lt;p&gt;“이 피그마 시안대로 메인 랜딩페이지 만들어줘. 똑같이 만들어봐.”&lt;/p&gt;
&lt;p&gt;결과물은 디자인 시안과 차이가 컸습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;색상이 피그마와 다름&lt;/li&gt;
&lt;li&gt;간격 기준이 일정하지 않음&lt;/li&gt;
&lt;li&gt;모바일 화면에서 요소가 튀어나감&lt;/li&gt;
&lt;li&gt;Pretendard가 적용되지 않고 기본 시스템 폰트로 표시됨&lt;/li&gt;
&lt;li&gt;일부 일러스트가 누락됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 다시 요청했습니다.&lt;/p&gt;
&lt;p&gt;“이거 피그마랑 다른데? 다시 똑같이 맞춰줘.”&lt;/p&gt;
&lt;figure class=&quot;figure image-callout&quot;&gt;&lt;div class=&quot;image-callout-inner&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/conversations.C84tTlVP_2dOA3b.webp&quot; alt=&quot;Claude와 나눈 텍스트 컬러·지그재그 이슈 대화 모음&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;2720&quot; height=&quot;1428&quot;&gt;&lt;/div&gt;&lt;p class=&quot;caption&quot;&gt;처음 며칠은 AI를 사람처럼 대하기도 했습니다. (생각보다 잘 만들 때도 있었습니다)&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;이 시기를 지나며 한 가지를 배웠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI는 자기가 만든 결과를 직접 보지 않습니다. 디자이너가 눈으로 확인하고 짚어주지 않으면, “맞게 만들었다”고 자신 있게 말합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;ai에게는-형용사가-아니라-숫자가-필요했습니다&quot;&gt;AI에게는 형용사가 아니라 숫자가 필요했습니다&lt;/h2&gt;
&lt;p&gt;초반엔 원하는 결과가 거의 나오지 않았습니다. 디자이너끼리는 자연스럽게 통하던 표현들이 AI 앞에서는 작동하지 않았습니다.&lt;/p&gt;
&lt;div class=&quot;table-callout&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;제가 시킨 말&lt;/th&gt;&lt;th&gt;클로드가 만든 결과물&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;”위계가 잘 안 보여, 다시 잡아줘”&lt;/td&gt;&lt;td&gt;폰트 사이즈를 무작정 키워버림&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”여백을 정리해줘”&lt;/td&gt;&lt;td&gt;모든 padding을 0으로&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”톤이 너무 무거워, 다운해줘”&lt;/td&gt;&lt;td&gt;색을 다 그레이 톤으로 변경함&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”리듬감이 없어, 자연스럽게”&lt;/td&gt;&lt;td&gt;무작위로 사이즈 변주&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;”공간감 좀 줘”&lt;/td&gt;&lt;td&gt;섀도우 값을 과하게 사용함&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;패턴은 명확했습니다.&lt;/p&gt;
&lt;p&gt;‘위계’, ‘톤’, ‘리듬감’, ‘공간감’은 디자이너끼리는 통하지만, AI에게는 어디를 어떻게 수정해야 하는지 불명확한 표현이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI는 추상적인 표현보다 수치와 기준에 더 정확하게 반응했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;예를 들어,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;카드 간격 24px로 통일&lt;/li&gt;
&lt;li&gt;타이틀 크기 32px → 40px 상향&lt;/li&gt;
&lt;li&gt;모바일 390px 기준 좌우 패딩 20px 적용&lt;/li&gt;
&lt;li&gt;본문 컬러 #111111로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 말했을 때 결과가 안정적으로 좋아졌습니다.&lt;/p&gt;
&lt;h2 id=&quot;10의-디테일&quot;&gt;10%의 디테일&lt;/h2&gt;
&lt;p&gt;결론부터 말하면, 기대한 자동화와 실제 작업 방식 사이에는 큰 차이가 있었습니다.&lt;/p&gt;
&lt;p&gt;Claude Code는 빠르게 만들고, 반복 작업과 수정에도 능합니다.&lt;/p&gt;
&lt;p&gt;하지만 무엇을 만들어야 할지, 결과가 맞는지 틀린지, 마지막 디테일을 어떻게 다듬어야 할지는 여전히 디자이너가 결정해야 했습니다.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;&lt;div class=&quot;figure-grid&quot; style=&quot;grid-template-columns:1fr 1fr 1fr&quot;&gt;&lt;div&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/viewport-desktop.B_dEQoTm_XAR2Y.webp&quot; alt=&quot;데스크탑 화면&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;2880&quot; height=&quot;12412&quot;&gt;&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/viewport-tablet.-P5X8BO8_kgXYz.webp&quot; alt=&quot;태블릿 화면&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;1640&quot; height=&quot;11836&quot;&gt;&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/viewport-mobile.BVYzbM6J_PcN09.webp&quot; alt=&quot;모바일 화면&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;750&quot; height=&quot;10816&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;p class=&quot;caption&quot;&gt;같은 페이지의 데스크탑·태블릿·모바일 화면&lt;/p&gt;&lt;/figure&gt;
&lt;figure class=&quot;figure image-callout&quot;&gt;&lt;div class=&quot;image-callout-inner&quot;&gt;&lt;img src=&quot;https://tech.postmath.co.kr/_astro/ignore-layout.ClRjcFuI_Z2gkr7V.webp&quot; alt=&quot;ignore auto layout 처리된 일러스트가 빈 회색 박스로 표시된 화면&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; fetchpriority=&quot;auto&quot; width=&quot;3446&quot; height=&quot;1930&quot;&gt;&lt;/div&gt;&lt;p class=&quot;caption&quot;&gt;피그마에서 ‘Ignore auto layout’으로 처리한 일러스트는 Claude가 아예 인식하지 못하는 이슈도 있었습니다.&lt;/p&gt;&lt;/figure&gt;
&lt;p&gt;제가 정확하게 요청한 만큼 정확한 결과가 나왔고, 놓친 부분은 그대로 남았습니다.&lt;/p&gt;
&lt;h2 id=&quot;그래서-어떻게-사용했나&quot;&gt;그래서 어떻게 사용했나&lt;/h2&gt;
&lt;p&gt;기대치를 조정한 뒤 정착한 작업 방식은 아래와 같았습니다.&lt;/p&gt;
&lt;div class=&quot;checklist-callout&quot;&gt;&lt;h3&gt;디자이너의 5단계 작업 패턴&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;피그마에서 섹션 단위로 나누기&lt;/strong&gt;&lt;span&gt;전체 페이지를 한 번에 요청하지 않고, 히어로 → 기능 카드 → 상세처럼 잘게 나누기.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Figma MCP로 디자인 값 추출&lt;/strong&gt;&lt;span&gt;색상, 간격, 폰트 값을 읽어와 코드에 반영.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;localhost에서 직접 검수&lt;/strong&gt;&lt;span&gt;피그마와 비교하며 모바일·태블릿·데스크탑 전부 확인.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;문제를 숫자로 피드백&lt;/strong&gt;&lt;span&gt;’이상해요’ 대신 ‘상단 여백 16px 늘려줘’&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;다음 섹션으로 이동&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;
&lt;p&gt;이 과정을 모든 페이지에 반복했습니다.&lt;/p&gt;
&lt;h2 id=&quot;2주-동안의-변화&quot;&gt;2주 동안의 변화&lt;/h2&gt;
&lt;p&gt;처음에는 거의 모든 프롬프트가 실패했습니다.&lt;/p&gt;
&lt;p&gt;며칠 뒤 첫 페이지가 완성됐고, 이후 작업 패턴이 잡히면서 수정 횟수가 빠르게 줄었습니다. 2주가 지나자 5개 페이지가 완성됐고, 다음 페이지는 반나절이면 만들 수 있겠다는 감각이 생겼습니다.&lt;/p&gt;
&lt;p&gt;기존에는 디자인 → 핸드오프 → 구현 → QA → 수정 → 배포까지 3~4주가 걸리던 흐름이었는데, 이번 작업은 약 2주 안에 마무리할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;가장 큰 변화는 코딩 실력이 아니었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI에게 정확히 지시하는 능력&lt;/strong&gt;, 즉 디자이너의 감각을 언어와 숫자로 번역하는 능력이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;ai의-한계는-나의-한계&quot;&gt;AI의 한계는 나의 한계&lt;/h2&gt;
&lt;p&gt;AI는 빠르게 결과를 만들고 반복 작업에 강했습니다. 하지만 무엇을 시킬지 모르면 헤매고, 마지막 완성도는 사람이 채워야 했습니다.&lt;/p&gt;
&lt;p&gt;결과물이 90% 맞아 보여도, 디자이너 눈에는 어색한 10%가 남습니다. 그 차이를 다듬느냐 지나치느냐에 따라 결과물 완성도가 달라졌습니다.&lt;/p&gt;
&lt;p&gt;2주 동안 써보며, 결국 중요한 건 기술보다 결과를 판단하는 눈이라는 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;그런데 더 흥미로운 이야기는 그 안에 있었습니다. 실패한 프롬프트들을 정리하다 보니, 반복해서 통하는 패턴과 협업 원칙이 보이기 시작했습니다.&lt;/p&gt;
&lt;p&gt;다음 글에서는 &lt;strong&gt;디자이너의 말을 AI가 이해하도록 바꾸는 프롬프트 원칙&lt;/strong&gt;을 다뤄보겠습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;시리즈 전체 보기&lt;/strong&gt;&lt;/p&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;01&lt;/span&gt;
&lt;span style=&quot;color:rgb(25, 25, 28)&quot;&gt;  디자이너가 Claude Code로 랜딩 페이지 직접 배포하기 #1&lt;/span&gt;
&lt;br/&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;02&lt;/span&gt;
&lt;span style=&quot;color:rgb(121, 121, 136)&quot;&gt;  &lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-2/&quot;&gt;Figma → Code, 디자이너가 직접 옮길 때 알아야 할 것들 #2&lt;/a&gt;&lt;/span&gt;
&lt;br/&gt;
&lt;span style=&quot;color:rgb(180, 180, 190);font-size:0.85em&quot;&gt;03&lt;/span&gt;
&lt;span style=&quot;color:rgb(121, 121, 136)&quot;&gt;  &lt;a href=&quot;https://tech.postmath.co.kr/product/landing-claude-code-3/&quot;&gt;Next.js 앱을 GCS에 올리기까지: 디자이너의 배포 회고 #3&lt;/a&gt;&lt;/span&gt;</content:encoded><dc:creator>고유현</dc:creator><category>Claude Code</category><category>Figma MCP</category><category>AI</category><category>디자이너</category><category>Next.js</category></item><item><title>트랜잭션을 걸었는데 왜 데이터가 꼬이지? — 싱글톤 Repository에서 UoW 패턴까지</title><link>https://tech.postmath.co.kr/tech/uow-vo/</link><guid isPermaLink="true">https://tech.postmath.co.kr/tech/uow-vo/</guid><description>결제시스템에서 발생한 트랜잭션 이슈를 해결하기까지의 여정을 소개합니다</description><pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. Order Payment Service를 담당하고 있는 Software Engineer 정현서입니다.
영광스럽게 포스트매스 기술 블로그에 첫 번째 글을 기고하게 되어 기쁩니다🙃&lt;/p&gt;
&lt;h2 id=&quot;배경&quot;&gt;배경&lt;/h2&gt;
&lt;p&gt;PostMath의 Order Payment Service는 Go + Gin + GORM 기반의 결제/포인트/주문 처리 서비스입니다. Clean Architecture 3계층 구조로 되어 있고, 트랜잭션도 적용하고 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[Client] → [Handler] → [Usecase] → [Repository] → [PostgreSQL]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                            ↕&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                      [gRPC Client] → [외부 서비스]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Handler&lt;/strong&gt;: HTTP/gRPC 요청을 받아서 Usecase에 전달하는 진입점&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Usecase&lt;/strong&gt;: 비즈니스 로직을 수행하고 트랜잭션을 관리하는 핵심 계층&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repository&lt;/strong&gt;: GORM을 통해 PostgreSQL에 접근하는 데이터 계층&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결제가 발생하면 주문 정보 업데이트, 결제 정보 저장, 포인트 차감, 회원 등급 갱신 등 여러 도메인에 걸친 작업이 하나의 요청 안에서 이루어집니다. 이 작업들은 전부 성공하거나 전부 실패해야 하는 원자적 단위입니다.&lt;/p&gt;
&lt;p&gt;그런데 운영 중에 포인트 잔액이 맞지 않는 케이스가 발견됐습니다. 두 결제 요청이 동시에 들어왔을 때, 둘 다 같은 잔액을 읽고 각자 차감하면서 한쪽의 결과가 사라지는 현상이었습니다. 트랜잭션을 걸고 있었는데도 발생한 이 문제의 원인과 해결 과정을 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;초기-구조-싱글톤-repository와-withtransaction&quot;&gt;초기 구조: 싱글톤 Repository와 WithTransaction&lt;/h2&gt;
&lt;h3 id=&quot;di-구조&quot;&gt;DI 구조&lt;/h3&gt;
&lt;p&gt;앱 시작 시 Repository를 &lt;strong&gt;한 번만 생성&lt;/strong&gt;해서 모든 Usecase에 주입하고 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setupHandlers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;router&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Engine&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;db&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;database&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Database&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Repository 계층 초기화 — 앱 전체에서 하나의 인스턴스&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    orderRepo  &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; repository.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewOrderRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(db)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    pointRepo  &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; repository.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(db)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    policyRepo &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; repository.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPolicyRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(db)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // gRPC Client 세팅&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mimgRPCClient &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; mimgRPCStart&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Usecase 계층 초기화 — 싱글톤 Repository를 주입받음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    orderUc  &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewOrderUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(orderRepo, mimgRPCClient, pointRepo, policyRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    pointUc  &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPointUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(pointRepo, mimgRPCClient, orderRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    policyUc &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPolicyUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(policyRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go의 Gin 프레임워크는 HTTP 요청이 들어올 때마다 고루틴을 생성합니다. 이 고루틴들이 전부 같은 Repository 인스턴스를 공유합니다. 각 Repository는 내부에 &lt;code&gt;*gorm.DB&lt;/code&gt;를 하나 들고 있었고, 이 DB 커넥션도 역시 공유되는 상태였습니다.&lt;/p&gt;
&lt;h3 id=&quot;트랜잭션-처리-방식&quot;&gt;트랜잭션 처리 방식&lt;/h3&gt;
&lt;p&gt;트랜잭션은 Repository 내부의 &lt;code&gt;WithTransaction&lt;/code&gt; 콜백 방식으로 처리하고 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;r &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithTransaction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    fn&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;tx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; IPointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointHistoryModel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointHistoryModel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    db &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; r.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getDB&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointHistoryModel&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; db.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Transaction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;tx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gorm&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DB&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // tx로 새 Repository를 만들어서 콜백에 전달&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        txRepo &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewPointRepositoryFromTx&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(tx)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        res, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fn&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(txRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        result &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result, err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GORM의 &lt;code&gt;Transaction()&lt;/code&gt; 안에서 &lt;code&gt;tx&lt;/code&gt;를 이용해 &lt;code&gt;txRepo&lt;/code&gt;를 생성하고, 콜백 함수에 넘겨줍니다. 콜백 안에서 &lt;code&gt;txRepo&lt;/code&gt;를 통해 쿼리를 실행하면 진짜 트랜잭션 안에서 동작합니다.&lt;/p&gt;
&lt;p&gt;Usecase에서는 이렇게 사용했습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;u &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pointUsecaseImpl&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithdrawPoint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;opsv1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithdrawPointRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;emptypb&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Empty&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    _, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.pointRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithTransaction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;tx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; repository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;IPointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointHistoryModel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // tx를 통해 쿼리 실행 — 진짜 트랜잭션 안에서 동작&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        holdingHistories, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GetHoldingHistoriesByMemberId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(req.MemberId))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;유저 보유 포인트 이력 가져오기 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%v&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; _, history &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; range&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; holdingHistories {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DeductedHoldingCurrentPoint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, history.Id, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;유저 포인트 보유이력 남은 잔액 수정 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%v&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        currentPoint, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GetMemberPointBalance&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(req.MemberId))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;현재 잔액 가져오기 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%v&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;MemberCurrentPointChange&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(req.MemberId), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, currentPoint.CashedPoint)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;현금성 포인트 잔액 변경 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%v&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;emptypb&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Empty&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{}, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WithTransaction&lt;/code&gt; 안에서 &lt;code&gt;tx&lt;/code&gt;를 통해 모든 쿼리를 실행하므로, &lt;strong&gt;단일 Repository 범위에서는&lt;/strong&gt; 트랜잭션이 정상적으로 동작했습니다. 포인트 조회 → 차감 → 잔액 변경이 하나의 트랜잭션으로 묶였습니다.&lt;/p&gt;
&lt;p&gt;여기까지는 문제가 없어 보입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;문제-1-repository별-독립-트랜잭션--도메인-간-불일치&quot;&gt;문제 1: Repository별 독립 트랜잭션 — 도메인 간 불일치&lt;/h2&gt;
&lt;p&gt;문제는 &lt;strong&gt;결제 처리&lt;/strong&gt;에서 드러났습니다. 결제는 단일 도메인이 아니라 여러 도메인에 걸친 작업입니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;결제 요청 → 주문 정보 조회 (OrderRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         → 결제 승인 API 호출 (Toss Payments)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         → 결제 정보 저장 (OrderRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         → 포인트 차감 (PointRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         → 포인트 이력 저장 (PointRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         → 누적 사용량 업데이트 (PointRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         → 회원 등급 확인/변경 (gRPC → MIM)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         → 주문 상태 업데이트 (OrderRepo)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 모든 작업이 &lt;strong&gt;하나의 원자적 단위&lt;/strong&gt;여야 합니다. 결제 정보는 저장됐는데 포인트가 안 빠지면 안 됩니다.&lt;/p&gt;
&lt;p&gt;그런데 &lt;code&gt;WithTransaction&lt;/code&gt;은 PointRepository 하나의 범위에서만 트랜잭션을 보장했습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;u &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;orderUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Payment&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;memberId&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 주문 정보 조회 — tx 없음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    order, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.repo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GetOrderByOrderId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, req.OrderId)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 결제 정보 저장 — tx 없음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.repo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;SavePayment&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, memberId, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;paymentResponse); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;결제 정보 저장 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 카드 정보 저장 — tx 없음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; paymentResponse.Card &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.repo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;SavePaymentCard&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;paymentResponse, memberId, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;paymentResponse.Card); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;카드 결제 정보 저장 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 포인트 사용 — PointUsecase 내부의 별도 tx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    _, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.pointUsease.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;UseCard&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, memberId, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;float64&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(paymentResponse.TotalAmount), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 주문 정보 수정 — tx 없음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.repo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;UpdateOrder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, memberId, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;paymentResponse); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;주문 정보 저장 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OrderRepository의 작업(결제 저장, 주문 수정)은 트랜잭션 없이 실행되고, PointUsecase의 작업(포인트 차감)은 별도 트랜잭션에서 실행됩니다. 두 트랜잭션은 완전히 분리되어 있습니다.&lt;/p&gt;
&lt;p&gt;이 구조에는 &lt;code&gt;OrderUsecase&lt;/code&gt;가 &lt;code&gt;PointUsecase&lt;/code&gt;를 &lt;strong&gt;직접 생성&lt;/strong&gt;하는 문제도 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewOrderUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;repo&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; OrderRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;gRPCClient&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Client&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                     pointRepo&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; IPointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;policyRepo&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; PolicyRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;OrderUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;orderUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        repo:        repo,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        gRPCClient:  gRPCClient,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        pointUsease: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPointUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(pointRepo, gRPCClient, repo),  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 내부에서 직접 생성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        policyRepo:  policyRepo,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;OrderUsecase&lt;/code&gt;와 &lt;code&gt;PointUsecase&lt;/code&gt;가 각자 다른 Repository 인스턴스(또는 같은 싱글톤이지만 다른 트랜잭션)를 사용하기 때문에, 둘을 같은 트랜잭션으로 묶을 방법이 없었습니다.&lt;/p&gt;
&lt;p&gt;주문은 저장됐는데 포인트 차감이 실패하면? 별도로 주문을 취소하는 보상 트랜잭션(Compensating Transaction)을 구현해야 합니다. 실제로 코드에 이런 보상 로직이 있었고, 복잡도를 높이는 주요 원인이었습니다.&lt;/p&gt;
&lt;p&gt;또한 새로운 도메인(배송, 환불 등)이 추가될 때마다 생성자 파라미터가 늘어나는 확장성 문제도 있었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;문제-2-동시-select--lost-update&quot;&gt;문제 2: 동시 SELECT — Lost Update&lt;/h2&gt;
&lt;p&gt;트랜잭션 범위를 넓혀서 문제 1을 해결한다 하더라도, 근본적인 동시성 문제가 하나 더 있습니다.&lt;/p&gt;
&lt;p&gt;PostgreSQL의 기본 격리 수준은 &lt;strong&gt;Read Committed&lt;/strong&gt;입니다. 이 격리 수준에서는 다른 트랜잭션이 커밋한 결과를 읽을 수 있고, &lt;strong&gt;같은 행을 동시에 SELECT하는 것을 막지 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;포인트 차감 시나리오로 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;시간 →&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TX-A:  BEGIN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       SELECT balance WHERE member_id = 1  →  잔액: 10,000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                                     ↑ 같은 값을 읽음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       TX-B:  BEGIN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;              SELECT balance WHERE member_id = 1  →  잔액: 10,000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       UPDATE balance = 5,000  (10,000 - 5,000)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       COMMIT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                                     ↓ A의 업데이트를 모르고 덮어씀&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;              UPDATE balance = 7,000  (10,000 - 3,000)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;              COMMIT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;기대 결과: 잔액 2,000  (10,000 - 5,000 - 3,000)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;실제 결과: 잔액 7,000  (마지막 쓰기가 이김)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이것이 &lt;strong&gt;Lost Update&lt;/strong&gt; 문제입니다. 8,000원이 차감되어야 하는데 3,000원만 빠집니다. 두 트랜잭션이 동시에 같은 잔액을 읽고, 각자 계산한 결과를 쓰면, 나중에 쓴 값이 먼저 쓴 값을 덮어씁니다.&lt;/p&gt;
&lt;p&gt;트랜잭션은 “내 안의 쿼리들이 원자적으로 커밋/롤백된다”를 보장하지, “다른 트랜잭션이 같은 데이터를 동시에 읽지 못한다”를 보장하지 않습니다. 이 차이를 이해하는 것이 중요합니다.&lt;/p&gt;
&lt;p&gt;운영 환경에서 동시 결제 요청이 들어올 때 실제로 이 현상이 발생했고, 포인트 잔액 불일치의 직접적인 원인이었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;해결-1-uowunit-of-work-패턴--여러-repository를-하나의-트랜잭션으로&quot;&gt;해결 1: UoW(Unit of Work) 패턴 — 여러 Repository를 하나의 트랜잭션으로&lt;/h2&gt;
&lt;h3 id=&quot;개념&quot;&gt;개념&lt;/h3&gt;
&lt;p&gt;마트에서 물건을 하나 집을 때마다 계산하는 사람은 없습니다. 카트에 다 담고 마지막에 계산대에서 한 번에 결제합니다. 카드가 거부되면 전체 취소합니다.&lt;/p&gt;
&lt;p&gt;UoW(Unit of Work)도 같은 개념입니다. Martin Fowler가 정의한 엔터프라이즈 애플리케이션 아키텍처 패턴 중 하나로, 여러 Repository의 작업을 하나의 트랜잭션으로 묶어서 전체 성공 또는 전체 실패를 보장합니다.&lt;/p&gt;
&lt;p&gt;기존 &lt;code&gt;WithTransaction&lt;/code&gt;과의 차이는 &lt;strong&gt;범위&lt;/strong&gt;입니다.&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;WithTransaction&lt;/th&gt;&lt;th&gt;UoW&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;트랜잭션 범위&lt;/td&gt;&lt;td&gt;단일 Repository&lt;/td&gt;&lt;td&gt;여러 Repository&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;사용 방식&lt;/td&gt;&lt;td&gt;콜백 안에서 txRepo 사용&lt;/td&gt;&lt;td&gt;Begin/Commit/Rollback 직접 호출&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;도메인 간 일관성&lt;/td&gt;&lt;td&gt;보장 안 됨&lt;/td&gt;&lt;td&gt;보장됨&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;구현&quot;&gt;구현&lt;/h3&gt;
&lt;p&gt;5월 26일, UoW 인터페이스와 구현체를 작성했습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// repository/UoW.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Begin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Commit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Rollback&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Repository 팩토리 — 같은 tx를 자동으로 공유&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    OrderRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;OrderRepository&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    PointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointRepository&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    PolicyRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PolicyRepository&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; gormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; struct&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    db &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gorm&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DB&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 주 DB 연결&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gorm&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DB&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 활성 트랜잭션 (Begin 후 생성)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewGormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;db&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gorm&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DB&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;UnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{db: db}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;트랜잭션 관리 메서드:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;uow &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Begin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    uow.tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.db.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Begin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.tx.Error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;uow &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Commit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 트랜잭션이 시작되지 않았거나 이미 종료됨&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.tx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Commit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;().Error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    uow.tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // 트랜잭션 종료 후 초기화&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;uow &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Rollback&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.tx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Rollback&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;().Error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    uow.tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 핵심인 &lt;strong&gt;Repository 팩토리 메서드&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;uow &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;OrderRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;OrderRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewOrderRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow.tx)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 트랜잭션 안에서&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewOrderRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow.db)       &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 트랜잭션 없이&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;uow &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewPointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow.tx)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 같은 tx!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewPointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow.db)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;uow &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PolicyRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PolicyRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.tx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewPolicyRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow.tx)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewPolicyRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow.db)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Begin()&lt;/code&gt;을 호출하면 &lt;code&gt;uow.tx&lt;/code&gt;가 생기고, 이후 &lt;code&gt;OrderRepository()&lt;/code&gt;나 &lt;code&gt;PointRepository()&lt;/code&gt;를 호출하면 이 &lt;code&gt;tx&lt;/code&gt;가 주입된 Repository를 반환합니다. &lt;strong&gt;모든 Repository가 같은 트랜잭션을 공유하는 것이 핵심입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;트랜잭션이 필요 없는 조회 API에서는 &lt;code&gt;Begin()&lt;/code&gt;을 호출하지 않으면 &lt;code&gt;uow.tx&lt;/code&gt;가 &lt;code&gt;nil&lt;/code&gt;이므로, 일반 &lt;code&gt;db&lt;/code&gt; 커넥션으로 Repository가 생성됩니다. 같은 UoW 인터페이스로 트랜잭션이 필요한 경우와 불필요한 경우를 모두 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;di-구조-변경&quot;&gt;DI 구조 변경&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Before: 개별 Repository 4개를 주입&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;orderUc &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewOrderUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(orderRepo, mimgRPCClient, pointRepo, policyRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;pointUc &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPointUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(pointRepo, mimgRPCClient, orderRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;policyUc &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPolicyUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(policyRepo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// After: UoW 하나만 주입&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;uow &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; repository.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewGormUnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(db.PostgreSQL)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;orderUc &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewOrderUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow, mimgRPCClient)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;pointUc &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPointUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow, mimgRPCClient)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;policyUc &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewPolicyUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uow)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;생성자 파라미터가 크게 줄어들었습니다. 새 도메인(배송, 환불 등)이 추가되면 UoW 인터페이스에 팩토리 메서드만 추가하면 됩니다.&lt;/p&gt;
&lt;p&gt;Usecase 구조체도 단순해졌습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Before&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; orderUsecase&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; struct&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    repo        &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;repository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;OrderRepository&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    gRPCClient  &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;mimv1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;MimServiceClient&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    pointUsease &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointUsecase&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    policyRepo  &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;repository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PolicyRepository&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// After&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; orderUsecase&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; struct&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    uow        &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;repository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;UnitOfWork&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    gRPCClient &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;mimv1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;MimServiceClient&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;usecase-사용-패턴&quot;&gt;Usecase 사용 패턴&lt;/h3&gt;
&lt;p&gt;UoW를 사용하는 결제 처리 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;u &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;orderUsecase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Payment&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;memberId&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; PaymentConfirmRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PaymentResponse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 1. 트랜잭션 시작&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.uow.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Begin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 2. 같은 tx를 공유하는 Repository 획득&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    orderRepo &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.uow.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;OrderRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    pointRepo &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.uow.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 3. 실패 시 자동 롤백 보장&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    committed &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    defer&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;committed {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            _ &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.uow.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Rollback&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 4. 비즈니스 로직 — 전부 같은 트랜잭션&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    order, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orderRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GetOrderByOrderId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, req.OrderId)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;주문 정보 불러오기 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // ... 결제 승인 API 호출 ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orderRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;SavePayment&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, memberId, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;paymentResponse); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;결제 정보 저장 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 포인트 차감 — OrderRepo와 같은 tx에서 실행!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    cumulative, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pointRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GetCumulativeMemberPoint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, memberId)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusBadRequest, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;누적 포인트 조회 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    cumulative.UsedCard &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; req.PurchaseAmount&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pointRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;UpsertCumulativeMemberPoint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, memberId, cumulative); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusBadRequest, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;누적 포인트 업데이트 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 주문 정보 수정 — 여전히 같은 tx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orderRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;UpdateOrder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, memberId, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;paymentResponse); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;주문 정보 저장 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 5. 모든 작업 성공 시 한 번에 커밋&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; u.uow.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Commit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusInternalServerError, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, fmt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Errorf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;트랜잭션 커밋 실패: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;%w&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, err)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    committed &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; http.StatusOK, resp, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;orderRepo&lt;/code&gt;와 &lt;code&gt;pointRepo&lt;/code&gt;가 같은 트랜잭션 안에서 동작합니다. 어디서든 에러가 발생하면 &lt;code&gt;defer&lt;/code&gt;에 의해 전체가 롤백되므로, “결제는 저장됐는데 포인트는 안 빠졌다”는 불일치가 구조적으로 불가능해집니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PointUsecase&lt;/code&gt;를 직접 생성할 필요도 없어졌습니다. &lt;code&gt;uow.PointRepository()&lt;/code&gt;로 같은 트랜잭션의 PointRepo를 바로 꺼내 쓰면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;해결-2-select-for-update--동시-읽기-차단&quot;&gt;해결 2: SELECT FOR UPDATE — 동시 읽기 차단&lt;/h2&gt;
&lt;p&gt;UoW로 트랜잭션 범위 문제(문제 1)는 해결했지만, 동시 SELECT로 인한 Lost Update(문제 2)는 여전히 남아있었습니다.&lt;/p&gt;
&lt;p&gt;6월 13일, 포인트 관련 SELECT 쿼리에 &lt;code&gt;FOR UPDATE&lt;/code&gt;를 추가했습니다.&lt;/p&gt;
&lt;h3 id=&quot;변경-내용&quot;&gt;변경 내용&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Before — 일반 SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;r &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GetMemberPointBalance&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;memberId&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;CurrentMemberPoints&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; entity &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;CurrentMemberPoints&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; r.db.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        Where&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;member_id = ?&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, memberId).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        First&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;entity).Error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; entity, err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// After — SELECT FOR UPDATE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;r &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pointRepository&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GetMemberPointBalance&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;memberId&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;CurrentMemberPoints&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; entity &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;CurrentMemberPoints&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; r.db.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        Where&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;member_id = ?&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, memberId).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        Clauses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;clause&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Locking&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{Strength: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;UPDATE&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}).  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 이 한 줄 추가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        First&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;entity).Error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; entity, err&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GORM에서는 &lt;code&gt;clause.Locking{Strength: &quot;UPDATE&quot;}&lt;/code&gt; 한 줄로 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;를 적용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;포인트 잔액 조회뿐 아니라, 포인트 보유 이력, 누적 포인트, 포인트 히스토리 등 금전 데이터를 다루는 &lt;strong&gt;모든 SELECT 쿼리&lt;/strong&gt;에 FOR UPDATE를 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// GetHoldingHistoryByMemberId — 포인트 보유 이력 (FIFO 차감 시 사용)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;query &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; r.db.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Table&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointHoldingHistories&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{}.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;TableName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Where&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;member_id = ?&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, memberId).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Where&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;current_point &gt; 0&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Where&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(gorm.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Expr&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;expiry_date &gt; NOW() OR expiry_date IS NULL&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Order&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;created_at ASC&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Order&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;id ASC&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Clauses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;clause&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Locking&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{Strength: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;UPDATE&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// FOR UPDATE 추가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// GetCumulativeMemberPoint — 누적 사용량&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; r.db.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Where&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;member_id = ?&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, memberId).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Clauses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;clause&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Locking&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{Strength: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;UPDATE&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}).  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// FOR UPDATE 추가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    First&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;entity).Error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// GetPointHistoriesByOrderId — 주문별 포인트 이력&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; r.db.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Table&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;domain&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PointHistories&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{}.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;TableName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Where&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;purchase_order_id = ?&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, orderId).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Clauses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;clause&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Locking&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{Strength: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;UPDATE&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}).  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// FOR UPDATE 추가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    Find&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;entities).Error&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;for-update가-하는-일&quot;&gt;FOR UPDATE가 하는 일&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;는 SELECT 시점에 해당 행에 &lt;strong&gt;잠금(Lock)&lt;/strong&gt; 을 겁니다. 다른 트랜잭션이 같은 행을 &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt;하려 하면, 첫 번째 트랜잭션이 끝날 때까지 &lt;strong&gt;대기&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TX-A:  BEGIN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       SELECT balance WHERE member_id = 1 FOR UPDATE  →  10,000 (잠금 획득)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TX-B:  BEGIN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       SELECT balance WHERE member_id = 1 FOR UPDATE  →  대기... (A가 잠금 보유 중)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TX-A:  UPDATE balance = 5,000 (10,000 - 5,000)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       COMMIT  →  잠금 해제&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TX-B:  →  이제 읽힘! 잔액: 5,000 (A가 업데이트한 최신 값)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       UPDATE balance = 2,000 (5,000 - 3,000)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       COMMIT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;결과: 정확히 8,000 차감! (Lost Update 방지)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TX-B는 TX-A가 커밋할 때까지 기다린 후 &lt;strong&gt;최신 값&lt;/strong&gt;을 읽습니다. TX-A가 5,000으로 업데이트한 결과를 바탕으로 TX-B가 차감하므로, 최종 잔액은 정확히 2,000원이 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;비관적-잠금-vs-낙관적-잠금&quot;&gt;비관적 잠금 vs 낙관적 잠금&lt;/h3&gt;
&lt;p&gt;이 방식을 &lt;strong&gt;비관적 잠금(Pessimistic Locking)&lt;/strong&gt; 이라 합니다. “충돌이 발생할 것이다”라고 비관적으로 가정하고, 미리 잠금을 걸어서 충돌을 원천 차단합니다.&lt;/p&gt;
&lt;p&gt;반대로 &lt;strong&gt;낙관적 잠금(Optimistic Locking)&lt;/strong&gt; 은 version 컬럼을 두고, 업데이트 시 version이 변경되었는지 확인합니다. 변경되었으면 충돌이 발생한 것이므로 재시도합니다. 읽기 위주이고 충돌이 드문 경우에 적합합니다.&lt;/p&gt;
&lt;p&gt;결제 시스템에서 비관적 잠금을 선택한 이유는 두 가지입니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;충돌 확률이 높음&lt;/strong&gt;: 같은 회원이 동시에 결제할 가능성이 있음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;재시도 비용이 큼&lt;/strong&gt;: 결제 승인 API 호출, 포인트 차감 등 사이드 이펙트가 있는 작업을 재시도하기 어려움&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;FOR UPDATE를 추가한 후에는 트랜잭션 타임아웃이 발생하는 경우가 있었습니다. 대기 시간이 길어질 수 있기 때문에 타임아웃을 조정하는 작업도 함께 진행했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;해결-3-uow-미들웨어--요청-격리와-안전망&quot;&gt;해결 3: UoW 미들웨어 — 요청 격리와 안전망&lt;/h2&gt;
&lt;p&gt;6월 19일, UoW 생명주기를 Gin 미들웨어로 옮겼습니다. 두 가지 목적이 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;목적-1-요청마다-새-uow-인스턴스-생성&quot;&gt;목적 1: 요청마다 새 UoW 인스턴스 생성&lt;/h3&gt;
&lt;p&gt;초기 UoW는 &lt;code&gt;setupHandlers&lt;/code&gt;에서 하나만 만들어 Usecase에 주입했습니다. 이렇게 하면 &lt;code&gt;gormUnitOfWork&lt;/code&gt; 안의 &lt;code&gt;tx&lt;/code&gt; 필드가 요청 간에 공유될 수 있습니다. 미들웨어에서 &lt;strong&gt;요청마다 새 UoW를 생성&lt;/strong&gt;하면 이 문제가 해결됩니다.&lt;/p&gt;
&lt;h3 id=&quot;목적-2-커밋롤백-누락-방지&quot;&gt;목적 2: 커밋/롤백 누락 방지&lt;/h3&gt;
&lt;p&gt;개발자가 실수로 &lt;code&gt;Commit()&lt;/code&gt;이나 &lt;code&gt;Rollback()&lt;/code&gt;을 빠뜨리면 트랜잭션이 열린 채로 남아서 커넥션 풀을 소모합니다. 미들웨어가 핸들러 실행 후 트랜잭션 상태를 확인해서 &lt;strong&gt;자동 롤백&lt;/strong&gt;하면 이런 실수를 방지할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;구현-1&quot;&gt;구현&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// middleware/handler.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UowMiddleware&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;db&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gorm&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DB&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;uowFactory&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gorm&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DB&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;types&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;UnitOfWork&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;HandlerFunc&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;c&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // OPTIONS, health check 요청은 UoW 불필요&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c.Request.Method &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;OPTIONS&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            c.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Next&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c.Request.URL.Path &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;/healthz&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c.Request.URL.Path &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;/readyz&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c.Request.URL.Path &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;/livez&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            c.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Next&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // 1. 요청마다 새 UoW 생성 — 요청 간 격리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        uow &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; uowFactory&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(db)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // 2. 트랜잭션 상태를 추적할 포인터 변수&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        closedTransaction &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // 3. Context에 UoW와 트랜잭션 상태 저장&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ctx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; context.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithValue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(c.Request.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(), types.UoW, uow)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ctx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; context.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithValue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx, types.ClosedTransaction, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;closedTransaction)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        c.Request &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c.Request.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WithContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // 4. 패닉 발생 시 롤백 보장&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        defer&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; r &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; recover&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(); r &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                _ &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Rollback&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;                panic&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(r)  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 원래 패닉은 계속 전파&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // 5. 핸들러 실행&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        c.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Next&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // 6. 안전망: 트랜잭션이 닫히지 않았으면 롤백&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;closedTransaction {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            _ &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; uow.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Rollback&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;closedtransaction-포인터-트릭&quot;&gt;closedTransaction 포인터 트릭&lt;/h3&gt;
&lt;p&gt;Go의 &lt;code&gt;context.Context&lt;/code&gt;는 &lt;strong&gt;불변(immutable)&lt;/strong&gt; 입니다. &lt;code&gt;context.WithValue&lt;/code&gt;로 값을 저장하면 새 context가 만들어지고, 원래 context는 변하지 않습니다. 그래서 context에 &lt;code&gt;bool&lt;/code&gt; 값을 직접 저장하면, Usecase에서 값을 변경해도 미들웨어의 context에는 반영되지 않습니다.&lt;/p&gt;
&lt;p&gt;이 문제를 &lt;code&gt;*bool&lt;/code&gt; 포인터로 해결했습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// types/context.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ContextKey&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    UoW&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;               ContextKey&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;uow&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    ClosedTransaction&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ContextKey&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;closedTransaction&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 포인터를 통해 값을 변경하는 헬퍼&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; SetTransactionClosed&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;closed&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; bool&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ptr, ok &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Value&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ClosedTransaction).(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*bool&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;); ok &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ptr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        *&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ptr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; closed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;미들웨어에서 &lt;code&gt;closedTransaction&lt;/code&gt; 변수의 포인터(&lt;code&gt;&amp;#x26;closedTransaction&lt;/code&gt;)를 context에 저장합니다. UoW의 &lt;code&gt;Begin()&lt;/code&gt;은 &lt;code&gt;*ptr = false&lt;/code&gt;로, &lt;code&gt;Commit()&lt;/code&gt;/&lt;code&gt;Rollback()&lt;/code&gt;은 &lt;code&gt;*ptr = true&lt;/code&gt;로 변경합니다. 미들웨어와 UoW가 같은 메모리 주소를 참조하므로 상태 동기화가 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;closedTransaction&lt;/code&gt;의 초기값이 &lt;code&gt;true&lt;/code&gt;인 이유는, GET 요청처럼 트랜잭션이 필요 없는 경우 &lt;code&gt;Begin()&lt;/code&gt;을 호출하지 않기 때문입니다. 초기값이 &lt;code&gt;false&lt;/code&gt;면 미들웨어가 불필요한 롤백을 시도합니다. &lt;code&gt;Begin()&lt;/code&gt; 호출 시 &lt;code&gt;false&lt;/code&gt;로 변경되고, &lt;code&gt;Commit()&lt;/code&gt; 또는 &lt;code&gt;Rollback()&lt;/code&gt; 호출 시 &lt;code&gt;true&lt;/code&gt;로 돌아갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;해결-4-vovalue-object--트랜잭션-전의-방어선&quot;&gt;해결 4: VO(Value Object) — 트랜잭션 전의 방어선&lt;/h2&gt;
&lt;h3 id=&quot;왜-필요한가&quot;&gt;왜 필요한가&lt;/h3&gt;
&lt;p&gt;잘못된 입력이 Usecase까지 내려오면 어떻게 될까요? 트랜잭션이 시작되고, FOR UPDATE로 포인트 행이 잠깁니다. 그 후 검증 로직에서 “음수 포인트는 허용되지 않습니다”와 같은 에러가 발생하고, 롤백됩니다.&lt;/p&gt;
&lt;p&gt;이 과정에서 다른 정상적인 결제 요청은 FOR UPDATE 잠금 때문에 &lt;strong&gt;대기&lt;/strong&gt;합니다. 잘못된 입력 하나가 정상 트래픽의 지연을 유발하는 것입니다.&lt;/p&gt;
&lt;p&gt;VO(Value Object)는 트랜잭션 시작 &lt;strong&gt;전에&lt;/strong&gt; 입력을 검증합니다. 유효하지 않은 요청은 Handler 레벨에서 즉시 반환되므로, 불필요한 트랜잭션 시작과 잠금 획득을 방지합니다.&lt;/p&gt;
&lt;h3 id=&quot;구현-2&quot;&gt;구현&lt;/h3&gt;
&lt;p&gt;VO는 &lt;strong&gt;팩토리 함수에서 생성 시점에 모든 비즈니스 규칙을 검증&lt;/strong&gt;하고, 하나라도 실패하면 &lt;code&gt;nil&lt;/code&gt;을 반환하는 패턴입니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// domain/pointGiftVO.go&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; GiftPointRequestVO&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; struct&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    FromMemberId   &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;int&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ToMemberId     &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;int&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Point          &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;float64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    RequestMessage &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdentityId     &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewGiftPointRequestVo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;fromMemberId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;toMemberId&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;point&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; float64&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                           requestMessage&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;identityId&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GiftPointRequestVO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Member ID는 양수여야 함&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; fromMemberId &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; toMemberId &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 자기 자신에게 선물 불가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; fromMemberId &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; toMemberId {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 포인트는 양수여야 함&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; point &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 실명인증 ID는 필수&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; identityId &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GiftPointRequestVO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        FromMemberId:   fromMemberId,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ToMemberId:     toMemberId,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Point:          point,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        RequestMessage: requestMessage,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        IdentityId:     identityId,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nil&lt;/code&gt;이 아닌 VO가 반환됐다면, 그 데이터는 모든 비즈니스 규칙을 통과한 유효한 상태입니다.&lt;/p&gt;
&lt;p&gt;Handler에서는 이렇게 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;h &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pointHandler&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GiftPoint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;c&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; req &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GiftPointRequest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ShouldBindJSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;req); err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        c.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(http.StatusBadRequest, &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;H&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;잘못된 요청 형식&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // VO 생성 — 비즈니스 규칙 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    vo &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; domain.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;NewGiftPointRequestVo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        req.FromMemberId, req.ToMemberId, req.Point, req.RequestMessage, req.IdentityId,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; vo &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // 트랜잭션 시작 전에 차단!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        c.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(http.StatusBadRequest, &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;H&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;유효하지 않은 요청&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Usecase는 VO를 받으면 데이터가 유효하다고 신뢰&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    statusCode, resp, err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; h.usecase.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GiftPoint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(c.Request.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(), vo)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        c.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(statusCode, &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;gin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;H&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: err.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    c.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(statusCode, resp)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usecase는 VO를 받으면 &lt;code&gt;fromMemberId &gt; 0&lt;/code&gt;, &lt;code&gt;point &gt; 0&lt;/code&gt;, &lt;code&gt;fromMemberId != toMemberId&lt;/code&gt; 등을 &lt;strong&gt;다시 검증할 필요가 없습니다.&lt;/strong&gt; 방어 코드 없이 비즈니스 로직에 집중할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이 패턴은 포인트 선물 외에도 정책(Policy) 관련 요청, 알림(NMS) 메시지 등 다양한 도메인에 적용했습니다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;go&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// domain/policy_vo.go — 정책 VO&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewPolicyRequestVo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;plan&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;action&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;discountRate&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;PolicyRequestVO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; plan &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; action &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; periodEnd.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Before&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(periodStart) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; discountRate &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; discountRate &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// domain/nmsVO.go — 알림 메시지 VO&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; NewMessageContentVo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;content&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;retentionType&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; RetentionType&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;MessageContentVO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; title &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; content &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; retentionType &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;([]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;rune&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(title)) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 256&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nil&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;문제&lt;/th&gt;&lt;th&gt;원인&lt;/th&gt;&lt;th&gt;해결&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;도메인 간 트랜잭션 분리&lt;/td&gt;&lt;td&gt;Repository별 독립 트랜잭션, Usecase 직접 생성&lt;/td&gt;&lt;td&gt;UoW — 여러 Repository를 하나의 tx로 통합&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Lost Update&lt;/td&gt;&lt;td&gt;Read Committed에서 동시 SELECT 허용&lt;/td&gt;&lt;td&gt;SELECT FOR UPDATE — 비관적 잠금&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;요청 간 UoW 상태 공유&lt;/td&gt;&lt;td&gt;싱글톤 인스턴스&lt;/td&gt;&lt;td&gt;미들웨어 — 요청당 UoW 생성 + 자동 롤백&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;잘못된 입력이 tx까지 도달&lt;/td&gt;&lt;td&gt;검증 없이 Usecase 진입&lt;/td&gt;&lt;td&gt;VO — 팩토리 함수에서 생성 시점 검증&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;진화 타임라인:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;4/30  WithTransaction — 단일 Repository 범위의 트랜잭션 (콜백 방식)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;5/26  UoW 구현 — 여러 Repository를 하나의 tx로 통합 + DI 단순화&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;6/13  SELECT FOR UPDATE — 동시 읽기 직렬화, Lost Update 방지&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;6/19  UoW 미들웨어 — 요청당 인스턴스 생성 + closedTransaction 포인터로 자동 롤백&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가장 큰 교훈은 &lt;strong&gt;“트랜잭션을 걸었다” ≠ “보호된다”&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;트랜잭션이 존재한다는 것만으로는 부족합니다. 그 트랜잭션의 범위가 비즈니스 단위와 일치하는지, 동시 읽기에 대한 잠금이 있는지, 요청 간 격리가 되는지까지 확인해야 진짜 보호됩니다.&lt;/p&gt;
&lt;p&gt;읽어주셔서 감사합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;*정현서 | Software Engineer at Order Payment Service @PostMath Co., Ltd.&lt;/p&gt;</content:encoded><dc:creator>정현서</dc:creator><category>Go</category><category>GORM</category><category>PostgreSQL</category><category>트랜잭션</category><category>동시성</category><category>Unit of Work</category><category>Clean Architecture</category><category>백엔드</category></item></channel></rss>