<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 개맛집</title>
    <link>https://study-easy-coding.tistory.com/</link>
    <description>이것저것 개발과 이것저것 리뷰 합니다.</description>
    <language>ko</language>
    <pubDate>Sat, 18 Jul 2026 23:55:13 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>foodev</managingEditor>
    <image>
      <title>개발 개맛집</title>
      <url>https://tistory1.daumcdn.net/tistory/4498861/attach/af27a3f3434b42c18d5fb4684afaeedb</url>
      <link>https://study-easy-coding.tistory.com</link>
    </image>
    <item>
      <title>비용 최적화를 위한 내부 로깅 시스템 설계와 시행착오 &amp;ndash; Azure 로그 비용 80% 절감한 경험</title>
      <link>https://study-easy-coding.tistory.com/193</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;770&quot; data-end=&quot;775&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;777&quot; data-end=&quot;869&quot; data-ke-size=&quot;size16&quot;&gt;스타트업 환경에서 인프라 비용은 곧 생존과 직결됩니다.&lt;br /&gt;당시 회사에서는 AWS와 Azure 크레딧을 제공받아 멀티 클라우드 환경으로 인프라를 운영하고 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;871&quot; data-end=&quot;1002&quot; data-ke-size=&quot;size16&quot;&gt;초기에는 AWS 크레딧을 중심으로 전체 인프라를 구성했지만, 크레딧 소진 이후 데이터베이스를 포함한 일부 워크로드를 Azure로 이전하게 되었습니다. 이 과정에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클라우드 로그 비용이 급격히 증가하는 문제&lt;/b&gt;를 마주하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  목차&lt;br /&gt;✅ 문제상황&lt;br /&gt;✅ 1차 접근: 로그를 저렴하게 저장하고, 필요할 때 분석하자&lt;br /&gt;✅ 2차 접근:&amp;nbsp; 비용과 실시간성의 균형을 다시 설계하자&lt;br /&gt;✅ 결과 및 성과&lt;br /&gt;✅&amp;nbsp;기술 회고&amp;nbsp;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;문제상황&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure로 일부 인프라를 이전한 뒤 비용을 모니터링하던 중,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1019&quot; data-end=&quot;1111&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Azure 로그 저장 비용이 전체 인프라 비용의 약 30%를 차지하고 있다는 점&lt;/b&gt;을 발견했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1113&quot; data-end=&quot;1140&quot; data-ke-size=&quot;size16&quot;&gt;원인을 분석해보니 다음과 같은 문제가 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;1142&quot; data-end=&quot;1258&quot;&gt;
&lt;li data-start=&quot;1142&quot; data-end=&quot;1185&quot;&gt;Azure Log Analytics는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;최소 30일 보관 정책&lt;/b&gt;을 강제&lt;/li&gt;
&lt;li data-start=&quot;1186&quot; data-end=&quot;1227&quot;&gt;개발 단계에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;info/debug 레벨의 상세 로그를 대량 수집&lt;/b&gt;&lt;/li&gt;
&lt;li data-start=&quot;1228&quot; data-end=&quot;1258&quot;&gt;로그 볼륨 증가 &amp;rarr; 보관 기간 &amp;times; 저장 비용이 누적&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1260&quot; data-end=&quot;1346&quot; data-ke-size=&quot;size16&quot;&gt;AWS CloudWatch Logs와 달리 Azure에서는 짧은 보관 기간으로 로그를 운영하기 어려웠고, 이 차이가 예상보다 큰 비용 문제로 이어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;1차 접근: 로그를 저렴하게 저장하고, 필요할 때 분석하자&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;아키텍처&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-01-11_14-02-27.png&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vkkNV/dJMcacohGY1/09qTkKYYeZwNj1ZPt1nz70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vkkNV/dJMcacohGY1/09qTkKYYeZwNj1ZPt1nz70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vkkNV/dJMcacohGY1/09qTkKYYeZwNj1ZPt1nz70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvkkNV%2FdJMcacohGY1%2F09qTkKYYeZwNj1ZPt1nz70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;970&quot; height=&quot;316&quot; data-filename=&quot;2026-01-11_14-02-27.png&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1502&quot; data-end=&quot;1511&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;접근 전략&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Winston&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그 레벨 분리&lt;/li&gt;
&lt;li&gt;JSON 기반 구조화 로그 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 포맷 개선&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NestJS Middleware에서 JWT 디코딩&lt;/li&gt;
&lt;li&gt;사용자 식별 정보(email)를 로그에 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;S3 기반 로그 저장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 로그를 1분 단위 파일로 변환&lt;/li&gt;
&lt;li&gt;Parquet 포맷으로 적재하여 저장 비용 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분석 환경 구축&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS Glue Crawler를 통해 스키마 자동 추론&lt;/li&gt;
&lt;li&gt;Data Lake에서 누락 없이 로그 분석 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1798&quot; data-end=&quot;1804&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;성과&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;1806&quot; data-end=&quot;1835&quot;&gt;
&lt;li data-start=&quot;1806&quot; data-end=&quot;1835&quot;&gt;Azure 로그 저장 비용&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;약 70% 절감&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1837&quot; data-end=&quot;1844&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;1846&quot; data-end=&quot;1938&quot;&gt;
&lt;li data-start=&quot;1846&quot; data-end=&quot;1874&quot;&gt;Glue Crawler 실행이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;배치 기반&lt;/b&gt;&lt;/li&gt;
&lt;li data-start=&quot;1875&quot; data-end=&quot;1903&quot;&gt;스키마 반영 및 적재까지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;5~10분 지연&lt;/b&gt;&lt;/li&gt;
&lt;li data-start=&quot;1904&quot; data-end=&quot;1938&quot;&gt;실시간 디버깅, 장애 대응에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;치명적인 사용성 문제&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1940&quot; data-end=&quot;2016&quot; data-ke-size=&quot;size16&quot;&gt;기술적으로는 비용 최적화에 성공했지만,&lt;br /&gt;운영 관점에서는 &amp;ldquo;로그는 즉시 조회할 수 있어야 한다&amp;rdquo;는 기본 요구사항을 충족하지 못했습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;2차 접근:&amp;nbsp; 비용과 실시간성의 균형을 다시 설계하자&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;아키텍처&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-01-11_14-06-06.png&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tVNYH/dJMcafL43EH/KOPrfPlQinu8EFVKlaRlz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tVNYH/dJMcafL43EH/KOPrfPlQinu8EFVKlaRlz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tVNYH/dJMcafL43EH/KOPrfPlQinu8EFVKlaRlz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtVNYH%2FdJMcafL43EH%2FKOPrfPlQinu8EFVKlaRlz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;922&quot; height=&quot;304&quot; data-filename=&quot;2026-01-11_14-06-06.png&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;2137&quot; data-end=&quot;2146&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개선 전략&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 접근에서 추가 개선을 진행했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;2148&quot; data-end=&quot;2409&quot;&gt;
&lt;li data-start=&quot;2148&quot; data-end=&quot;2237&quot;&gt;&lt;b&gt;선별적 로그 보관&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;2166&quot; data-end=&quot;2237&quot;&gt;
&lt;li data-start=&quot;2166&quot; data-end=&quot;2205&quot;&gt;error / warning 로그만 Azure Monitor에 저장&lt;/li&gt;
&lt;li data-start=&quot;2208&quot; data-end=&quot;2237&quot;&gt;info / debug 로그는 별도 저장소로 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;2238&quot; data-end=&quot;2300&quot;&gt;&lt;b&gt;실시간 분석 중심 설계&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;2259&quot; data-end=&quot;2300&quot;&gt;
&lt;li data-start=&quot;2259&quot; data-end=&quot;2277&quot;&gt;KQL 기반 실시간 쿼리 가능&lt;/li&gt;
&lt;li data-start=&quot;2280&quot; data-end=&quot;2300&quot;&gt;장애 발생 시 즉각적인 원인 파악&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;2301&quot; data-end=&quot;2343&quot;&gt;&lt;b&gt;Azure RBAC 적용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;2323&quot; data-end=&quot;2343&quot;&gt;
&lt;li data-start=&quot;2323&quot; data-end=&quot;2343&quot;&gt;팀원 역할에 따른 로그 접근 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;2344&quot; data-end=&quot;2409&quot;&gt;&lt;b&gt;모니터링 대시보드 구축&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;2365&quot; data-end=&quot;2409&quot;&gt;
&lt;li data-start=&quot;2365&quot; data-end=&quot;2374&quot;&gt;API 호출량&lt;/li&gt;
&lt;li data-start=&quot;2377&quot; data-end=&quot;2384&quot;&gt;응답 시간&lt;/li&gt;
&lt;li data-start=&quot;2387&quot; data-end=&quot;2409&quot;&gt;에러율 등 핵심 지표를 실시간 시각화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;결과 및 성과&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-end=&quot;2455&quot; data-start=&quot;2428&quot; data-ke-size=&quot;size16&quot;&gt;2차 접근을 통해 다음과 같은 결과를 얻었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2601&quot; data-start=&quot;2457&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2505&quot; data-start=&quot;2457&quot;&gt;&lt;b&gt;비용 절감&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2505&quot; data-start=&quot;2471&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2505&quot; data-start=&quot;2471&quot;&gt;Azure 로그 관련 비용 &lt;b&gt;월 기준 약 80% 절감&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2557&quot; data-start=&quot;2506&quot;&gt;&lt;b&gt;운영 효율성 개선&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2557&quot; data-start=&quot;2524&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2538&quot; data-start=&quot;2524&quot;&gt;실시간 로그 조회 가능&lt;/li&gt;
&lt;li data-end=&quot;2557&quot; data-start=&quot;2541&quot;&gt;장애 대응 시간 대폭 단축&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2601&quot; data-start=&quot;2558&quot;&gt;&lt;b&gt;크레딧 활용 최적화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2601&quot; data-start=&quot;2577&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2601&quot; data-start=&quot;2577&quot;&gt;절감된 비용을 핵심 인프라에 재투자 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2026년 1월 11일 오후 02_02_04.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B1BKX/dJMcab3YVUL/BgnPIKTHH3Edlq3GZgW5MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B1BKX/dJMcab3YVUL/BgnPIKTHH3Edlq3GZgW5MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B1BKX/dJMcab3YVUL/BgnPIKTHH3Edlq3GZgW5MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB1BKX%2FdJMcab3YVUL%2FBgnPIKTHH3Edlq3GZgW5MK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;363&quot; height=&quot;545&quot; data-filename=&quot;ChatGPT Image 2026년 1월 11일 오후 02_02_04.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;기술 회고&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-end=&quot;2713&quot; data-start=&quot;2623&quot; data-ke-size=&quot;size16&quot;&gt;1차 접근에서는 AWS Glue, Data Lake 등 기술적으로 흥미로운 요소에 집중한 나머지, &lt;br /&gt;&lt;b&gt;실제 사용하는 팀원의 경험을 충분히 고려하지 못했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2822&quot; data-start=&quot;2715&quot; data-ke-size=&quot;size16&quot;&gt;돌이켜보면 처음부터 Azure Log Analytics와 Data Explorer의 &lt;b&gt;보관 정책, 쿼리 성능, 비용 옵션을 더 깊이 검토했다면&lt;/b&gt; 2차 접근으로 바로 갈 수 있었을 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;2848&quot; data-start=&quot;2824&quot; data-ke-size=&quot;size16&quot;&gt;이 경험을 통해 다음을 명확히 깨달았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2967&quot; data-start=&quot;2850&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2891&quot; data-start=&quot;2850&quot;&gt;비용 절감만을 목표로 하면 &lt;b&gt;개발 생산성이 오히려 떨어질 수 있다&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2934&quot; data-start=&quot;2892&quot;&gt;인프라 설계에서는 &lt;b&gt;기술적 설계보다 실용성과 지속가능성&lt;/b&gt;이 중요하다&lt;/li&gt;
&lt;li data-end=&quot;2967&quot; data-start=&quot;2935&quot;&gt;&amp;ldquo;운영자가 매일 쓰는 도구&amp;rdquo;라는 관점이 &quot;반드시&quot; 필요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3028&quot; data-start=&quot;2969&quot; data-ke-size=&quot;size16&quot;&gt;이후로는 인프라를 설계할 때&lt;br /&gt;&lt;b&gt;비용, 사용성, 운영 부담의 균형&lt;/b&gt;을 가장 먼저 고려하게 되었습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;2416&quot; data-end=&quot;2426&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;글 잼나게 보셨으면 좋아요 눌러주고 가세요&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;관심받는 거 좋아합니다&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;010&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/010.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/010.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Infra &amp;amp; System</category>
      <category>Azure log 비용 줄이기</category>
      <category>Azure 로그 최소 저장 30일</category>
      <category>인프라 로그 저장 비용 감소하는 방법</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/193</guid>
      <comments>https://study-easy-coding.tistory.com/193#entry193comment</comments>
      <pubDate>Sun, 11 Jan 2026 14:07:52 +0900</pubDate>
    </item>
    <item>
      <title>TypeDI 내부 구조 분석:  typeDI 코드 분석을 통한 의존성 주입(DI)이 되는 과정</title>
      <link>https://study-easy-coding.tistory.com/187</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;의존성 주입(DI)에 대해 이론적으로는 알고 있지만, &lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;실제로 내부에서 어떻게 동작하는지 이해하기는 쉽지 않았습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 글에서는 &lt;b&gt;TypeDI&lt;/b&gt; 오픈소스 코드를 직접 분석하며 &lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;데코레이터와 컨테이너가 어떻게 의존성을 관리하는지 파헤쳐봅니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;NestJS 팀에서 DI 개념을 TypeDI에서 아이디어를 얻었다고 하여 TypeDI를 분석하게 되었습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;소스코드를 10번 이상 직접 따라치며 얻은 내용을 공유합니다. &lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;평소에 당연하게 사용하던 @Injectable(), @Service() 같은 데코레이터들이&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt; 실제로는 어떤 원리로 작동하는지 핵심 로직 위주로 설명했습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 내용이 있다면 댓글 부탁드립니다.  &amp;zwj;♂️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  이 글은 다음과 같은 분들에게 도움이 됩니다.&lt;br /&gt;- TypeDI나 NestJS 같은 DI 프레임워크를 사용하지만 내부 동작을 이해하고 싶은 개발자 &lt;br /&gt;- JS/TS의 메타프로그래밍과 리플렉션에 관심 있는 개발자 &lt;br /&gt;- 직접 DI 컨테이너를 구현해보고 싶은 개발자 &lt;br /&gt;&lt;br /&gt;특히 &quot;➡️  container.get(ServiceA) 실행 그리고 동작원리&quot; 부분은 DI의 핵심 메커니즘을 다루므로 꼭 읽어보시기 바랍니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 아래 코드는 어떻게 실행될까요?&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1745038327247&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import 'reflect-metadata';

if (!Reflect || !(Reflect as any).getMetadata) {
  throw new Error(
    'TypeDI requires &quot;Reflect.getMetadata&quot; to work. Please import the &quot;reflect-metadata&quot; package at the very first line of your application.'
  );
}

import { ContainerRegistry } from './container-registry';
import { ServiceA } from './serviceA';

function main() {
  console.log('start');

  const container = ContainerRegistry.getContainer('default');
  const serviceA = container.get(ServiceA);
  serviceA.serviceB.sayHello();
  serviceA.sayHello();
}

main();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ reflect-metadata로 초기 설정값 존재유무 체크&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Reflect는 JS 내장 객체로 메타프로그래밍을 위한 메서드를 제공함&lt;/li&gt;
&lt;li&gt;reflect-metadata는 Reflect API를 확장하여 메타데이터 관련 기능을 추가하는 폴리필 라이브러리&lt;/li&gt;
&lt;li&gt;TS 컴파일러가 데코레이터와 함께 클래스 컴파일 시 생성자 매개변수의 &quot;타입 정보&quot;를 메타데이터로 저장&lt;/li&gt;
&lt;li&gt;TS 컴파일시 타입 정보가 사라지는데, 이 라이브러리를 통해 타입 정보를 메타데이터로 보존함&lt;/li&gt;
&lt;li&gt;런타임에서 Reflect.getMetadata로 정보 검증&lt;/li&gt;
&lt;li&gt;tsconfig.json에 다음 설정이 필수적:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1745124015864&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{ 
	&quot;compilerOptions&quot;: { 
		&quot;experimentalDecorators&quot;: true,
		&quot;emitDecoratorMetadata&quot;: true,
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ ContainerRegistry.getContainer('default'); 실행 과정&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeDI는 의존성 주입을 위한 컨테이너 개념을 중심으로 설계되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 서비스 인스턴스들을 관리하고 제공하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[container-registry.ts 일부]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745038729044&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { ContainerInstance } from './container-instance';
import { EMPTY_VALUE } from './empty-value';
import { ServiceMetadata } from './interfaces/service-metadata';
import { ContainerIdentifier } from './types/container-identifier';
import { ServiceIdentifier } from './types/service-identifier';
import { ServiceOptions } from './types/service-options';

export class ContainerRegistry {
  private static readonly containerMap: Map&amp;lt;ContainerIdentifier, ContainerInstance&amp;gt; = new Map();
  public static readonly defaultContainer: ContainerInstance = new ContainerInstance('default');

  public static registerContainer(container: ContainerInstance): void {
    if (container instanceof ContainerInstance === false) {
      throw new Error('error');
    }

    if (!!ContainerRegistry.defaultContainer &amp;amp;&amp;amp; container.id === 'default') {
      throw new Error('errror');
    }

    if (ContainerRegistry.containerMap.has(container.id)) {
      throw new Error('error');
    }

    ContainerRegistry.containerMap.set(container.id, container);
  }

  public static getContainer(id: ContainerIdentifier): ContainerInstance {
    const registedContainer = this.containerMap.get(id);
    if (registedContainer === undefined) {
      throw new Error('error');
    }

    return registedContainer;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[container-instance.ts 일부]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745038803984&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class ContainerInstance {
  public readonly id!: ContainerIdentifier;
  private metadataMap: Map&amp;lt;ServiceIdentifier, ServiceMetadata&amp;lt;unknown&amp;gt;&amp;gt; = new Map();

  constructor(identifier: ContainerIdentifier) {
    this.id = identifier;
    ContainerRegistry.registerContainer(this);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeDI가 초기화될 때 다음과 같은 과정이 발생합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;public static readonly defaultContainer: ContainerInstance = new ContainerInstance('default'); 실행&lt;/li&gt;
&lt;li&gt;ContainerInstance 생성자에서 this.id = 'default' 설정&lt;/li&gt;
&lt;li&gt;생성자 내부에서 ContainerRegistry.registerContainer(this) 호출&lt;/li&gt;
&lt;li&gt;registerContainer 메서드에서 컨테이너 맵에 새 컨테이너 등록: containerMap.set('default', container)&lt;/li&gt;
&lt;li&gt;ContainerRegistry.getContainer('default') 호출 시 이렇게 등록된 기본 컨테이너를 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-04-19_13-56-09.png&quot; data-origin-width=&quot;2970&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIr7If/btsNrfceDot/fp1ae0f1o0M9kFpwHbPvjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIr7If/btsNrfceDot/fp1ae0f1o0M9kFpwHbPvjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIr7If/btsNrfceDot/fp1ae0f1o0M9kFpwHbPvjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIr7If%2FbtsNrfceDot%2Ffp1ae0f1o0M9kFpwHbPvjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;908&quot; height=&quot;186&quot; data-filename=&quot;2025-04-19_13-56-09.png&quot; data-origin-width=&quot;2970&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ const serviceA = container.get(ServiceA); 실행&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;container.get(ServiceA)로 service 데코레이터(함수)로 등록된 ServiceA 클래스를 조회합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1745039776375&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Service } from './decorators/service-decorator';
import { ServiceB } from './serviceB';

@Service()
export class ServiceA {
  constructor(public serviceB: ServiceB) {}

  public sayHello() {
    console.log('Hello, serviceA!');
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회를 하기 전에 등록을 해야 하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 service 데코레이터로 ServiceA가 등록될 수 있을지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;service-decorator 등록&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[service-decorator.ts]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745040068881&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function service&amp;lt;T=unknown&amp;gt;(): Function;
export function service&amp;lt;T&amp;gt;(options: ServicOptions&amp;lt;T&amp;gt; ={}): ClassDecorator {
	return(targetConstructor) =&amp;gt; {
    	const serviceMetadata: ServiceMetadata&amp;lt;T&amp;gt; = { 
            id: options.id || targetConstructor,
            type: targetConstructor as unknown as Constructable&amp;lt;T&amp;gt;,
            factory: undefined,
            eager: false,
            multiple: false,
            scope:'container',
            referencedBy: new Map().set(ContainerRegistry.defaultContainer.id, containerRegistry.defaultContainer),
            value: EMPTY_VALUE
		};
        ContainerRegistry.defaultContainer.set(serviceMetadata);
    };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeDI serviceDecorator는 많은 serviceOptions 타입을 제공하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 자세히 다루지 않겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[설명]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service() 데코레이터를 클래스에 적용하면 위 과정을 통해 serviceMetadata가 ContainerRegistry에 등록됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 defaultContainer에 ServiceA 클래스의 메타 정보들이 등록됩니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;[service 함수 필드 역할]&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;id&lt;/b&gt;: 서비스로 등록할 클래스 (클래스 자체가 ID)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;type&lt;/b&gt;: 서비스로 등록할 클래스의 생성자&lt;/li&gt;
&lt;li&gt;&lt;b&gt;factory&lt;/b&gt;: 팩토리 클래스 옵션 (여기서는 undefined 정의)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;multiple&lt;/b&gt;: 다중 컨테이너 등록 유무 옵션 (여기서는 false)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;eager&lt;/b&gt;: 즉시로딩 옵션 (여기서는 지연로딩)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;scope&lt;/b&gt;: service의 스코프 옵션 설정('transient' | 'container' | 'singleton' 중 하나)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;referencedBy&lt;/b&gt;: 어떤 컨테이너와 연결되어 있는지 정보를 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;value&lt;/b&gt;: serviceMetadata 등록 전 초기값 (서비스 메타데이터가 등록되면 해당 value로 변경)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;[등록과정]&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Service() 실행 시 정의된 필드값 세팅 및 ContainerRegistry.defaultContainer.set(serviceMetadata); 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[container-instance.ts]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745041667967&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class ContainerInstance {
    private metadataMap: Map&amp;lt;ServiceIdentifier, ServiceMetadata&amp;lt;unknown&amp;gt;&amp;gt; = new Map();
    
    public set&amp;lt;T&amp;gt;(serviceOptions: ServiceOptions&amp;lt;T&amp;gt;): this {
        const newMetadata: ServiceMetadata&amp;lt;T&amp;gt; = {
          id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier,
          type: (serviceOptions as ServiceMetadata&amp;lt;T&amp;gt;).type || null,
          factory: (serviceOptions as ServiceMetadata&amp;lt;T&amp;gt;).factory,
          value: (serviceOptions as ServiceMetadata&amp;lt;T&amp;gt;).value || EMPTY_VALUE,
          multiple: serviceOptions.multiple || false,
          eager: serviceOptions.eager || false,
          scope: serviceOptions.scope || 'container',
          ...serviceOptions,
          referencedBy: new Map().set(this.id, this),
        };
        
        const existingMetadata = this.metadataMap.get(newMetadata.id);
        
        if (existingMetadata) {
          Object.assign(existingMetadata, newMetadata);
        } else {
          this.metadataMap.set(newMetadata.id, newMetadata);
        }
        return this;
      }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 메타데이터를 재설정하고, 컨테이너에 등록하는 함수:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;serviceOptions 매개변수를 ServiceMetadata 타입으로 재정의&lt;/li&gt;
&lt;li&gt;metadataMap 존재 유무 체크&lt;/li&gt;
&lt;li&gt;이미 존재하면 존재하는 값을 재정의한 메타데이터로 덮어쓰기&lt;/li&gt;
&lt;li&gt;미존재하면 새로운 서비스 metadataData 정의&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1745042979741&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export type ServiceIdentifier&amp;lt;T = unknown&amp;gt; =
  | Constructable&amp;lt;T&amp;gt;
  | AbstractConstructable&amp;lt;T&amp;gt;
  | CallableFunction
  | Token&amp;lt;T&amp;gt;
  | string;


export interface ServiceMetadata&amp;lt;T = unknown&amp;gt; {
  id: ServiceIdentifier;
  scope: ContainerScope;
  type: Constructable&amp;lt;T&amp;gt; | null;
  factory: [Constructable&amp;lt;unknown&amp;gt;, string] | CallableFunction | undefined;
  value: unknown | Symbol;
  multiple: boolean;
  eager: boolean;
  referencedBy: Map&amp;lt;ContainerIdentifier, ContainerInstance&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;  container.get(ServiceA) 실행 그리고 동작원리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 어떻게 service 데코레이터로 클래스가 등록되는지 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 container.get을 통해 어떻게 조회되는지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[container-instance.ts - get()]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745043243088&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class ContainerInstance {
    private metadataMap: Map&amp;lt;ServiceIdentifier, ServiceMetadata&amp;lt;unknown&amp;gt;&amp;gt; = new Map();
    public get&amp;lt;T = unknown&amp;gt;(identifier: ServiceIdentifier&amp;lt;T&amp;gt;): T {
        const global = ContainerRegistry.defaultContainer.metadataMap.get(identifier);
        const local = this.metadataMap.get(identifier);

        const metadata = global?.scope === 'singleton' ? global : local;

        if (metadata) {
          return this.getServiceValue(metadata);
        }

        throw new Error('서비스 반드시 존재해야함');
      }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 인스턴스에 등록된 서비스 메타데이터 정보를 조회하는 함수입니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;매개변수로 ServiceA가 들어옵니다.&lt;/li&gt;
&lt;li&gt;global = 싱글톤 패턴으로 전역에 적용된 defaultContainer의 서비스메타데이터를 조회합니다.&lt;/li&gt;
&lt;li&gt;local = 해당 컨테이너의 인스턴스의 메타데이터 맵에 접근하여 데이터를 조회합니다.&lt;/li&gt;
&lt;li&gt;getServiceValue를 호출합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[container-instance.ts - getServiceValue()]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745043802262&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export class ContainerInstance {
    public getServiceValue(serviceMetadata: ServiceMetadata&amp;lt;unknown&amp;gt;): any {
        let value: unknown = EMPTY_VALUE;

        if (serviceMetadata.value !== EMPTY_VALUE) {
          return serviceMetadata.value;
        }

        if (!serviceMetadata.factory &amp;amp;&amp;amp; serviceMetadata.type) {
          const constructableTargetType: Constructable&amp;lt;unknown&amp;gt; = serviceMetadata.type;
          const paramTypes: unknown[] = (Reflect as any)?.getMetadata('design:paramtypes', constructableTargetType) || [];
          const params = this.initializeParams(constructableTargetType, paramTypes);

          params.push(this);
          value = new constructableTargetType(...params);
        }

        if (serviceMetadata.scope !== 'transient' &amp;amp;&amp;amp; value !== EMPTY_VALUE) {
          serviceMetadata.value = value;
        }

        if (value === EMPTY_VALUE) {
          throw new Error('not ');
        }

        return value;
      }
      
    private initializeParams(target: Function, paramtypes: any[]): unknown[] {
        return paramtypes.map((paramtype, index) =&amp;gt; {
          if (paramtype &amp;amp;&amp;amp; paramtype.name &amp;amp;&amp;amp; !this.isPrimitiveTypes(paramtype.name)) {
            return this.get(paramtype);
          }
          return undefined;
        });
      }

    private isPrimitiveTypes(typeName: string) {
        return ['string', 'boolean', 'number', 'object'].includes(typeName.toLowerCase());
      }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;serviceValue를 조회하는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서비스의 인스턴스 또는 실제 객체값을 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript는 컴파일 시 타입 정보가 사라지지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 정보를 Reflect 메타데이터로 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;design:paramtypes 키워드로 Reflect 메타데이터를 조회합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;value 지역변수를 EMPTY_VALUE로 초기화합니다.&lt;/li&gt;
&lt;li&gt;매개변수 서비스 메타데이터의 value가 EMPTY_VALUE가 아니면 serviceMetadata.value를 리턴합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 초기화 과정을 거쳐 value가 등록되었다고 판단하기 때문입니다.&lt;/li&gt;
&lt;li&gt;이를 통해 순환참조를 자동으로 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서비스의 생명주기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 등록 시: value = EMPTY_VALUE (아직 미등록)&lt;/li&gt;
&lt;li&gt;첫 요청 시: value 저장&lt;/li&gt;
&lt;li&gt;이후 요청 시: 등록된 value 인스턴스 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;serviceMetadata.factory가 정의되지 않았고, serviceMetadata.type(생성자)이 존재하기 때문에 조건문이 실행됩니다.&lt;/li&gt;
&lt;li&gt;constructableTargetType 변수를 생성자 타입, serviceMetadata매개변수의 type으로 선언합니다.&lt;/li&gt;
&lt;li&gt;Reflect 메타데이터 API를 이용하여 'design' key와 constructableTargetType value로 생성자 매개변수의 타입정보 배열을 반환합니다.&lt;/li&gt;
&lt;li&gt;initializeParams() 메서드를 실행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;initializeParams 함수의 작동 방식과 재귀적 의존성 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;initializeParams 함수는 서비스 클래스의 생성자에 필요한 모든 의존성을 재귀적으로 해결합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;의존성 분석 과정&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수는 생성자의 매개변수 타입(paramtypes)을 순회하며 각 타입을 검사합니다.&lt;/li&gt;
&lt;li&gt;각 매개변수 타입에 대해 primitive 타입인지 확인합니다(isPrimitiveTypes 함수 사용).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재귀적 의존성 해결&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매개변수가 객체 타입(서비스)이면 this.get(paramtype)을 호출합니다.&lt;/li&gt;
&lt;li&gt;이 호출은 &lt;b&gt;재귀적으로&lt;/b&gt; 해당 의존성을 가져오는 과정을 시작합니다.&lt;/li&gt;
&lt;li&gt;예를 들어, ServiceA가 ServiceB에 의존하면:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ServiceA의 생성자 매개변수로 ServiceB가 필요함을 인식&lt;/li&gt;
&lt;li&gt;this.get(ServiceB) 호출 -&amp;gt; 재귀 시작&lt;/li&gt;
&lt;li&gt;ServiceB 인스턴스 생성 및 반환&lt;/li&gt;
&lt;li&gt;ServiceA 생성자에 ServiceB 주입하여 최종 인스턴스 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;params.push(this)&lt;/li&gt;
&lt;li&gt;value = new constructableTargetType(...params);
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;this를 params에 추가함으로써, 현재 컨테이너 인스턴스를 생성자의 마지막 매개변수로 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;b&gt;의존성 주입의 형태&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;params배열이 생성자 매개변수로 전달됩니다.&lt;/li&gt;
&lt;li&gt;이 과정에서 this가 생성자의 매개변수로 전달되어 객체가 컨테이너에 접근할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인스턴스가 성공적으로 생성되었다면 serviceMetadata.value에 value 등록&lt;/li&gt;
&lt;li&gt;value 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;최종 정리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;container.get(ServiceA) 호출 시 실행 과정:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컨테이너는 ServiceA의 서비스 메타데이터를 조회&lt;/li&gt;
&lt;li&gt;getServiceValue() 함수 호출로 인스턴스 생성 과정 시작&lt;/li&gt;
&lt;li&gt;getServiceValue() 내부에서:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ServiceA의 생성자 매개변수 타입 정보를 Reflect API로 조회&lt;/li&gt;
&lt;li&gt;paramTypes = [ServiceB] 배열 가져옴&lt;/li&gt;
&lt;li&gt;initializeParams(ServiceA, [ServiceB]) 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;initializeParams() 내부에서:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매개변수 타입 배열([ServiceB])을 순회&lt;/li&gt;
&lt;li&gt;ServiceB가 primitive 타입이 아니므로 재귀적으로 this.get(ServiceB) 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;재귀적 this.get(ServiceB) 처리:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ServiceB의 서비스 메타데이터 조회&lt;/li&gt;
&lt;li&gt;ServiceB에 대한 getServiceValue() 함수 호출&lt;/li&gt;
&lt;li&gt;ServiceB의 생성자 매개변수 타입 정보를 조회&lt;/li&gt;
&lt;li&gt;만약 ServiceB에 의존성이 없다면 빈 배열 반환 ([])&lt;/li&gt;
&lt;li&gt;new ServiceB() 인스턴스 생성하여 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ServiceA 인스턴스 생성 완료:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;new ServiceA(serviceB) 형태로 의존성이 주입된 ServiceA 인스턴스 생성&lt;/li&gt;
&lt;li&gt;완성된 ServiceA 인스턴스를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과적으로:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;const serviceA = container.get(ServiceA) 구문이 실행되면 ServiceA 인스턴스 생성&lt;/li&gt;
&lt;li&gt;이 인스턴스의 serviceB 속성에는 이미 ServiceB 인스턴스가 주입되어 있음&lt;/li&gt;
&lt;li&gt;따라서 serviceA.serviceB.sayHello() 함수 호출이 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-04-19_17-34-20.png&quot; data-origin-width=&quot;2962&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkaWcD/btsNrlXQEOd/PeKgM6G7RIqKmd6ycLgzJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkaWcD/btsNrlXQEOd/PeKgM6G7RIqKmd6ycLgzJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkaWcD/btsNrlXQEOd/PeKgM6G7RIqKmd6ycLgzJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkaWcD%2FbtsNrlXQEOd%2FPeKgM6G7RIqKmd6ycLgzJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2962&quot; height=&quot;1054&quot; data-filename=&quot;2025-04-19_17-34-20.png&quot; data-origin-width=&quot;2962&quot; data-origin-height=&quot;1054&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 실행 최종 결과.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-04-19_15-58-47.png&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb8Mqf/btsNrqq1nLB/jm65NMZAkoGzvimcH9YWEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb8Mqf/btsNrqq1nLB/jm65NMZAkoGzvimcH9YWEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb8Mqf/btsNrqq1nLB/jm65NMZAkoGzvimcH9YWEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb8Mqf%2FbtsNrqq1nLB%2Fjm65NMZAkoGzvimcH9YWEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;495&quot; height=&quot;176&quot; data-filename=&quot;2025-04-19_15-58-47.png&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ serviceA와 serviceB의 순환참조가 발생하지 않는 이유&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1745139223993&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Service } from './decorators/service-decorators';
import { ServiceB } from './serviceB';

@Service()
export class ServiceA {
  constructor(public serviceB: ServiceB) {}

  public sayHello() {
    console.log('Hello, serviceA!');
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745139232883&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Service } from './decorators/service-decorators';
import { ServiceA } from './serviceA';

@Service()
export class ServiceB {
  constructor(public serviceA: ServiceA) {}
  public sayHello() {
    console.log('Hello, ServiceB');
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 호출해도 순환참조가 발생하지 않는 이유는 무엇일까요.&lt;/p&gt;
&lt;pre id=&quot;code_1745141119578&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function main() {
  console.log('start');

  const container = ContainerRegistry.getContainer('default');
  const serviceA = container.get(ServiceA);
  serviceA.serviceB.sayHello();
  serviceA.sayHello();
}

main();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;지연 초기화&lt;/h3&gt;
&lt;pre id=&quot;code_1745139259574&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // container-instance.ts
   private getServiceValue(serviceMetadata: ServiceMetadata&amp;lt;unknown&amp;gt;): any {
     let value: unknown = EMPTY_VALUE;
     if (serviceMetadata.value !== EMPTY_VALUE) {
       return serviceMetadata.value;
     }
     // ...
   }&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서비스 인스턴스는 실제로 container.get()이 호출되기 전까지 생성되지 않습니다.&lt;/li&gt;
&lt;li&gt;serivceMetadata.value가 EMPTY_VALUE일 때만 인스턴스가 생성됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 불필요한 초기화를 방지하고 순환참조를 예방합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;의존성 해결&lt;/h3&gt;
&lt;pre id=&quot;code_1745140839178&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   // container-instance.ts
   private initializeParams(paramTypes: any[]) {
     return paramTypes.map(type =&amp;gt; {
       if (type &amp;amp;&amp;amp; type.name &amp;amp;&amp;amp; !this.isPrimitiveTypes(type.name)) {
         return this.get(type);
       }
       return undefined;
     });
   }&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;serviceA가 초기화될 때 serviceB를 의존성으로 요청합니다.&lt;/li&gt;
&lt;li&gt;serviceB가 초기화될 때 serviceA를 의존성으로 요청하지만, 이미 serviceA는 초기화중이므로 EMPTY_VALUE가 아닌 serviceMetadata.value를 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;index.ts에서 container.get(ServiceA) 호출&lt;/li&gt;
&lt;li&gt;ServiceA 초기화 시작&lt;/li&gt;
&lt;li&gt;ServiceA의 생성자에서 ServiceB 의존성 요청&lt;/li&gt;
&lt;li&gt;ServiceB 초기화 시작&lt;/li&gt;
&lt;li&gt;ServiceB의 생성자에서 ServiceA 의존성 요청&lt;/li&gt;
&lt;li&gt;ServiceA는 이미 초기화 중이라 EMPTY_VALUE가 아닌 값을 반환&lt;/li&gt;
&lt;li&gt;ServiceB 초기화 완료&lt;/li&gt;
&lt;li&gt;ServiceA 초기화 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, TypeDI는 자동으로 의존성 주입을 관리하여 순환참조를 방지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeDI는 프로토타입에서 주로 사용하고, 복잡한 설정 없이도 알아서 자동으로 처리하는 데 중점을 둡니다. 더 간단하고 유연한 의존성 주입을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS에서는 forwardRef()를 사용하여 명시적으로 의존성 주입 시스템에 순환참조가 있음을 알리고, 이를 처리하는 방법을 제공합니다. 개발자가 순환참조를 인식하고 명시하여 직접 처리하도록 하는 데 중점을 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스코드 본인 깃허브 계정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/chlcken2/typedi&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/chlcken2/typedi&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1745922956864&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - chlcken2/typedi: typedi 구현&quot; data-og-description=&quot;typedi 구현. Contribute to chlcken2/typedi development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/chlcken2/typedi&quot; data-og-url=&quot;https://github.com/chlcken2/typedi&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dI50Fz/hyYIawwHyj/D9O7kBzTiz7OpHvv5DyO3k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b6lw5e/hyYIjtre7Q/kww9RTnlmvAawPkIAy4101/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/chlcken2/typedi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/chlcken2/typedi&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dI50Fz/hyYIawwHyj/D9O7kBzTiz7OpHvv5DyO3k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b6lw5e/hyYIjtre7Q/kww9RTnlmvAawPkIAy4101/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - chlcken2/typedi: typedi 구현&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;typedi 구현. Contribute to chlcken2/typedi development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>⭐ Featured</category>
      <category>di동작원리</category>
      <category>di란</category>
      <category>di의존성</category>
      <category>typedi 동작원리</category>
      <category>typedi란</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/187</guid>
      <comments>https://study-easy-coding.tistory.com/187#entry187comment</comments>
      <pubDate>Mon, 14 Apr 2025 09:38:55 +0900</pubDate>
    </item>
    <item>
      <title>개발자 &amp;quot;잘&amp;quot; 말하고 &amp;quot;잘&amp;quot; 쓰기 + 꿀팁 추가</title>
      <link>https://study-easy-coding.tistory.com/182</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 대두되는 요즘, 개발자에게는 코딩 실력만으로는 부족합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스를 이해하고 효과적으로 소통하는 능력이 더욱 중요해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 종합적인 역량은 불필요한 의사소통 비용을 줄이고 효율적인 개발로 이어져,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 회사와 개발자 본인의 가치를 높이는 핵심 요소가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 개발자는 기술과 비즈니스의 균형을 맞추며, 이를 명확히 전달할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 글들을 통해 자신의 능력을 올리는데 도움이 되었으면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⬇️ 꿀팁은 맨아래&lt;/b&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;  목차&lt;br /&gt;✅ SCQA를 생각하자&amp;nbsp;&lt;br /&gt;✅ 조사를 제거하자&lt;br /&gt;✅ 듣는이를 고려하자&lt;/blockquote&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;SCQA를 생각하자&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SCQA란?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상황(Situation)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 현재 상황/배경 설명&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex: &quot;AI 반환 실행 횟수 초과 에러 관련하여 문의 드립니다.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;복잡성(Complication)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 문제점/이슈 설명&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex: &quot;현재 개발팀 내부에서 해당 에러로 인한 노이즈가 있어...&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;질문(Question)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 확인하고 싶은 내용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex: &quot;혹시 이 에러 현상을 모니터링하고 계신가요?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;행동(Action)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 제안/요청사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex: &quot;비활성화를 고려 중입니다.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 원본과 SCQA를 적용한 수정본을 비교해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SQCA 적용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;OO님 AI 결과값 반환 실행 횟수 초과 에러 보고 계신가요?&lt;br /&gt;개발팀 내부에서 레시피 AI 반환 에러가 노이즈해서 끄고자 하는 니즈가 있어서 확인 차 말씀드립니다&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정본:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;결과값 반환 실행 횟수 초과 에러 관련하여 문의 드립니다.&lt;br /&gt;현재 개발팀 내부에서 해당 에러로 인한 노이즈가 있어 비활성화를 고려 중입니다.&lt;br /&gt;혹시 이 에러 현상을 모니터링하고 계신가요?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;주요 차이점:&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;구조화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본: 한 문장에 여러 내용이 혼재&lt;/li&gt;
&lt;li&gt;수정본: 상황 설명 &amp;rarr; 문제점 &amp;rarr; 질문 순으로 구조화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전문성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본: &quot;니즈가 있어서&quot;, &quot;노이즈해서&quot; 등 구어체 사용&lt;/li&gt;
&lt;li&gt;수정본: &quot;비활성화를 고려 중입니다&quot; 등 공식적인 표현 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;명확성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본: 의도가 간접적으로 표현됨&lt;/li&gt;
&lt;li&gt;수정본: 문제 상황과 확인하고자 하는 내용이 명확히 구분됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실제 적용 예시들:&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;단순 확인이 필요할 때:&lt;br /&gt;OO 기능 배포 관련 문의드립니다.&lt;br /&gt;현재 배포 가능한 상태인가요?&amp;nbsp;&lt;/li&gt;
&lt;li&gt;문제 해결이 필요할 때:&lt;br /&gt;[상황] 프로덕션 환경의&amp;nbsp; API 응답 지연이 발생하고 있습니다.&lt;br /&gt;[복잡성] 현재 평균 응답시간이 2초를 초과하고 있으며, 사용자 불만이 접수되고 있습니다.&lt;br /&gt;[잘문] 혹시 이 현상을 인지하고 계신가요?&lt;br /&gt;[행동] 긴급 대응이 필요할 것 같아 회의를 제안드립니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조로 작성하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메시지의 목적이 명확해집니다&lt;/li&gt;
&lt;li&gt;받는 사람이 빠르게 이해할 수 있습니다&lt;/li&gt;
&lt;li&gt;필요한 액션을 쉽게 파악할 수 있습니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;조사를 제거하자&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수정전&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OO님 AI 결과값&lt;b&gt;을&lt;/b&gt; 반환&lt;b&gt;하기위해&lt;/b&gt; 실행 횟수 초과 에러를 보고 계신가요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발팀 내부에서 AI&lt;b&gt;의&lt;/b&gt; 반환 에러가 노이즈해서 끄고자 하는 니즈가 있어서 확인 차 말씀드립니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수정후&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;OO님 AI 결과값&lt;span&gt;&amp;nbsp;&lt;/span&gt;반환&lt;span&gt;&amp;nbsp;&lt;/span&gt;실행 횟수 초과 에러를&lt;span&gt;&amp;nbsp;&lt;/span&gt;보고 계신가요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발팀 내부에서 AI&lt;span&gt;&amp;nbsp;&lt;/span&gt;반환 에러가 노이즈해서 끄고자 하는 니즈가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;듣는이를 고려하자&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대가 비개발직군이거나, 해당 도메인에 대한 지식이 없다고 할 때&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자의 언어로 말을 한다면 1도 이해할 수 없는 상황에 놓이게 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적 용어보다는 현재 발생하고, 해결하고자 하는 &quot;모두가 알 수 있는 단어&quot;로 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ex: &lt;br /&gt;QA에서 API에러가 발생해서 안되나봐요 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 고쳐봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트환경에서 백엔드에 문제가 있어서 안되나봐요&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;(보통 이정도는 &lt;b&gt;이해하겠지라는&lt;/b&gt;&amp;nbsp;생각을 할까봐 극적인 예시를 가져왔습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. QA가 누군가한테는 테스트 환경, 브랜치, 검수 과정으로 해석될 수 있습니다. 정확한 의미를 전달합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. API에서 에러가 어떻게 발생했고, 무엇이 문제인지 &quot;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;관심 없는 경우가 많습니다.&lt;/span&gt;&lt;/b&gt;&quot; 그래서 말하고자 하는바가 무엇인지에 집중하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말을 하거나 글이 어려운게 결론부터 말을 하고 시작할지, 상황을 먼저 말해야할지, 결론이 2개인것 같기도합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우들을 기술이 포함되어 있는 경우는 더 말을 복잡하게 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCQA 구조를 꼭 기억하셔서 회사 생활에 도움이 되었으면 좋겠습니다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 자연스러운 문맥구조와 듣는이를 고려해서 말해봐요~!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⭐️ 꿀팁&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 글 쓰는게 너무 어렵다! 싶으면&amp;nbsp;&lt;br /&gt;gpt, claud, gemini등에 &lt;b&gt;&quot;&lt;b&gt;SCQA 구조로 변경해줘&quot; &lt;/b&gt;&lt;/b&gt;프로젝트를 만들어둡시다&lt;b&gt;.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알아서 우리의 문법을 듣기 좋게 수정해줍니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;글 잼나게 보셨으면 좋아요 눌러주고 가세요&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;관심받는 거 좋아합니다&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;010&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/010.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/010.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Reflections/Work &amp;amp; Career</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/182</guid>
      <comments>https://study-easy-coding.tistory.com/182#entry182comment</comments>
      <pubDate>Tue, 18 Feb 2025 16:01:37 +0900</pubDate>
    </item>
    <item>
      <title>(공식문서) NestJS Pipes와 queryString, Body가 데이터 처리하는 방법</title>
      <link>https://study-easy-coding.tistory.com/179</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;파이프는&amp;nbsp;@Injectable()&amp;nbsp;데코레이터로 주석이 달린 클래스로,&amp;nbsp;PipeTransform&amp;nbsp;인터페이스를 구현합니다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-18_13-25-05.png&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u7Uqm/btsMlAOUIF5/JzKMr2k4bDPQXIY6IzcPj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u7Uqm/btsMlAOUIF5/JzKMr2k4bDPQXIY6IzcPj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u7Uqm/btsMlAOUIF5/JzKMr2k4bDPQXIY6IzcPj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu7Uqm%2FbtsMlAOUIF5%2FJzKMr2k4bDPQXIY6IzcPj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1188&quot; height=&quot;638&quot; data-filename=&quot;2025-02-18_13-25-05.png&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련된 링크 : &lt;a href=&quot;https://docs.nestjs.com/pipes&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.nestjs.com/pipes&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  목차&lt;br /&gt;✅ 파이프의 일반적 사례&lt;br /&gt;✅ 파이프 바인딩과 적용&lt;br /&gt;✅ queryString과 body의 데이터 처리 차이점&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;파이프의 일반적 사례&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transfromation: 입력 데이터를 원하는 형식(예: 문자열에서 정수로)으로 변환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;validation: 입력 데이터를 평가하여 유효하면 변경 없이 통과하고, 그렇지 않으며 예외를 던집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 경우 모두 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;컨트롤러 경로 핸들러&lt;/span&gt;에서 처리 중인 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;인수&lt;/span&gt;에 대해 작동합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;10:1-10:12&quot; data-ke-size=&quot;size26&quot;&gt;파이프 작동 방식&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-sourcepos=&quot;12:1-17:0&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-sourcepos=&quot;12:1-12:32&quot;&gt;&lt;b&gt;요청&lt;/b&gt;: 클라이언트가 서버로 요청을 보냅니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;13:1-13:43&quot;&gt;&lt;b&gt;파이프 실행&lt;/b&gt;: NestJS는 요청 데이터를 파이프에 전달합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;14:1-14:47&quot;&gt;&lt;b&gt;변환 &amp;amp; 검사&lt;/b&gt;: 파이프는 데이터를 변환하고 유효성을 검사합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;15:1-15:46&quot;&gt;&lt;b&gt;결과 반환&lt;/b&gt;: 파이프는 변환되고 검증된 데이터를 컨트롤러에 전달합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;16:1-17:0&quot;&gt;&lt;b&gt;컨트롤러 실행&lt;/b&gt;: 컨트롤러는 파이프가 제공한 데이터를 사용하여 로직을 처리합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프는 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;예외영역&lt;/span&gt;에서 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프에서 예외가 발생하면 exception filters에서 처리됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;파이프 바인딩과 적용&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;queryString 에서 Pipe&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1739853144376&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Get(':id') async findOne(@Param('id', ParseIntPipe) id: number) 
{ 
    return this.catsService.findOne(id); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 &lt;b&gt;ParseIntPipe&lt;/b&gt;를 사용했다고 가정합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 id값은 number이거나 예외를 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GET localhost:3000/개발개맛집이 들어왔다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typeof(id)의 결과는 string이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 예외 필터에 걸려서 아래의 문구를 보게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #f7f7f7; text-align: left;&quot; data-dl-translated=&quot;true&quot; data-dl-original=&quot;true&quot; data-dl-uid=&quot;165&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;{
  &quot;statusCode&quot;: 400,
  &quot;message&quot;: &quot;Validation failed (numeric string is expected)&quot;,
  &quot;error&quot;: &quot;Bad Request&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 pipe가 예외를 던지는 이유는 findOne() 함수가 실행되지 않도록 함에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;body에서 Pipe 적용은?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 내용은 쿼리스트링에서 pipe를 적용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 POST 메소드의 body 값에는 어떻게 적용할 수 있을까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉽게도 NestJS의 pipe를 이용하여 body 값을 체크할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 class-decorator를 import 하여 사용하거나 nestia의 typechecking 함수를 이용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uuId 체크 예시:&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739853845825&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { IsUUID } from 'class-validator';
export interface CreateEventBody {
  @IsUUID()
  uuid?: string;
  prevUrl?: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;queryString과 body의 데이터 처리 차이점&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h2 data-sourcepos=&quot;5:1-5:18&quot; data-ke-size=&quot;size26&quot;&gt;데이터가 도착하는 시점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;7:1-9:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;7:1-7:70&quot;&gt;&lt;b&gt;쿼리 스트링&lt;/b&gt;: 쿼리 스트링은 URL의 일부로, 요청이 서버에 도착하는 &lt;b&gt;즉시&lt;/b&gt; 파싱되어 파이프에 전달됩니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;8:1-9:0&quot;&gt;&lt;b&gt;본문(Body)&lt;/b&gt;: 본문 데이터는 요청의 내용 부분으로, 서버에 도착한 후 &lt;b&gt;별도의 처리&lt;/b&gt;를 거쳐야 파이프에 전달될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-sourcepos=&quot;10:1-10:15&quot; data-ke-size=&quot;size26&quot;&gt;데이터 처리 방식&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;12:1-14:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;12:1-12:94&quot;&gt;&lt;b&gt;쿼리 스트링&lt;/b&gt;: 쿼리 스트링은 주로 &lt;b&gt;문자열 형태&lt;/b&gt;로 전달됩니다. 파이프는 이 문자열을 원하는 타입(숫자, 날짜 등)으로 변환하거나 유효성을 검사합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;13:1-14:0&quot;&gt;&lt;b&gt;본문(Body)&lt;/b&gt;: 본문 데이터는 다양한 형식(JSON, XML 등)으로 전달될 수 있습니다. 파이프는 이 데이터를 &lt;b&gt;파싱 하고,&lt;/b&gt; 필요한 경우 &lt;b&gt;변환&lt;/b&gt;하거나 유효성을 검사합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-sourcepos=&quot;15:1-15:15&quot; data-ke-size=&quot;size26&quot;&gt;파이프 적용 시점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;17:1-19:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;17:1-17:61&quot;&gt;&lt;b&gt;쿼리 스트링&lt;/b&gt;: 쿼리 스트링은 요청과 함께 도착하므로, 파이프가 &lt;b&gt;즉시&lt;/b&gt; 적용될 수 있습니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;18:1-19:0&quot;&gt;&lt;b&gt;본문(Body)&lt;/b&gt;: 본문 데이터는 파싱 과정을 거쳐야 하므로, 파이프는 &lt;b&gt;파싱 후&lt;/b&gt;에 적용될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-18_13-48-59.png&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OTB0e/btsMm3WD0G2/bei8wdKkQlZib0PHSa3AC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OTB0e/btsMm3WD0G2/bei8wdKkQlZib0PHSa3AC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OTB0e/btsMm3WD0G2/bei8wdKkQlZib0PHSa3AC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOTB0e%2FbtsMm3WD0G2%2Fbei8wdKkQlZib0PHSa3AC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1350&quot; height=&quot;316&quot; data-filename=&quot;2025-02-18_13-48-59.png&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;글 잼나게 보셨으면 좋아요 눌러주고 가세요&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;관심받는 거 좋아합니다&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;010&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/010.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/010.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Backend Engineering</category>
      <category>nestjs pipe</category>
      <category>nestjs pipe body</category>
      <category>validation failed (numeric string is expected)</category>
      <category>서이추</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/179</guid>
      <comments>https://study-easy-coding.tistory.com/179#entry179comment</comments>
      <pubDate>Tue, 18 Feb 2025 13:51:05 +0900</pubDate>
    </item>
    <item>
      <title>GitHub 라벨 또는 태그로 Azure API Container에 CI/CD 적용하기</title>
      <link>https://study-easy-coding.tistory.com/178</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;브랜치를 푸시하거나, 라벨을 붙이거나, 태그를 추가할 경우 &lt;br /&gt;GitHub Action을 이용하여 Azure API Container에 자동으로 배포하는 방법을 공유합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 팀에서는 변형된 Git flow 전략으로 버전 관리를 하고 있습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그중 QA 브랜치만 다루도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  목차&lt;br /&gt;✅ CI/CD에서 원하는 동작&lt;br /&gt;✅ GitHub Action workflow 생성&lt;br /&gt;✅ Azure 배포 프로세스의 이해 ⭐️⭐️⭐️&lt;br /&gt;✅ 만약 안되는 부분이 있다면&lt;br /&gt;✅ 회고&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;CI/CD에서 원하는 동작&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;QA 브랜치에 PR을 만들고 &quot;QA&quot;라벨을 추가하면 자동 CI/CD가 되도록 구현하려고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-13_23-43-58.png&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EJUIm/btsMh2Rtt2s/s4VMNnAGaYgCdq7JpysM11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EJUIm/btsMh2Rtt2s/s4VMNnAGaYgCdq7JpysM11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EJUIm/btsMh2Rtt2s/s4VMNnAGaYgCdq7JpysM11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEJUIm%2FbtsMh2Rtt2s%2Fs4VMNnAGaYgCdq7JpysM11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;698&quot; height=&quot;560&quot; data-filename=&quot;2025-02-13_23-43-58.png&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;GitHub&amp;nbsp;Action workflow 생성&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;workflow 파일 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1739458183581&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Trigger auto deployment for heroines-qa-api

# When this action will be executed
on:
  pull_request:                  #  Pull Request 이벤트에 대해 워크플로우가 트리거됨
    types: [labeled]             # 그중에서도 라벨이 추가될 때만

  # Automatically trigger it when detected changes in repo
  push:
    branches:                   # qa 브랜치에 푸시가 되었가나,  
      - qa
    tags:                       # QA 태그가 붙거나,
      - QA
    paths:
      - '**'
      - '.github/workflows/heroines-qa-api-AutoDeployTrigger-1cb86b57-8b3a-403e-8699-b788c8702b1b.yml'

  # Allow manual trigger
  workflow_dispatch:

jobs: # 작업을 실행할 때 라벨 중 QA 태그가 붙었거나, 푸시가 있거나, work_dispatch 실행했으면 조건 실행 
  build-and-deploy:
    if: |
      github.event_name == 'pull_request' &amp;amp;&amp;amp; github.event.label.name == 'QA' ||
      github.event_name == 'push' ||
      github.event_name == 'workflow_dispatch' ||
      startsWith(github.ref, 'refs/tags/QA')
    runs-on: ubuntu-latest
    permissions:
      id-token: write # This is required for requesting the OIDC JWT Token
      contents: read # Required when GH token is used to authenticate with private repo

    steps:
      - name: Checkout to the branch
        uses: actions/checkout@v2

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id:
          tenant-id:
          subscription-id: 

      - name: Build and push container image to registry
        uses: azure/container-apps-deploy-action@v2
        with:
          appSourcePath:
          dockerfilePath: 
          registryUrl: 
          registryUsername: 
          registryPassword:
          containerAppName: 
          resourceGroup: 
          imageToBuild: 
          _buildArgumentsKey_: 

        if: always()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;Azure 배포 프로세스의 이해&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;공식문서를 대충 읽어보고 구현에만 집중해서 정확하지 않을 수 있습니다. 잘못된 내용이라면 댓글 부탁드립니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Azure API container 배포 이용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure API Container 서비스에서 CD기능을 기본적으로 제공한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-13 오후 11.54.18.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;932&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ohnXU/btsMgKEqn36/PDywhCyRQl4rKeAtHWI6b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ohnXU/btsMgKEqn36/PDywhCyRQl4rKeAtHWI6b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ohnXU/btsMgKEqn36/PDywhCyRQl4rKeAtHWI6b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FohnXU%2FbtsMgKEqn36%2FPDywhCyRQl4rKeAtHWI6b1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1632&quot; height=&quot;932&quot; data-filename=&quot;스크린샷 2025-02-13 오후 11.54.18.png&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;932&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;단, 푸시에 관련해서만 가능한 것으로 확인된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;pull_request 방식은 다른 방식을 federated identity crdential Subject라면서 안된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-13 오후 11.56.26.png&quot; data-origin-width=&quot;3038&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPqtYI/btsMhc1vuKN/3Nwyf3AXVuKZHwZs6NHhek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPqtYI/btsMhc1vuKN/3Nwyf3AXVuKZHwZs6NHhek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPqtYI/btsMhc1vuKN/3Nwyf3AXVuKZHwZs6NHhek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPqtYI%2FbtsMhc1vuKN%2F3Nwyf3AXVuKZHwZs6NHhek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3038&quot; height=&quot;160&quot; data-filename=&quot;스크린샷 2025-02-13 오후 11.56.26.png&quot; data-origin-width=&quot;3038&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이것 때문에 삽질을 열심히 했다..&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-13 오후 11.58.15.png&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;1808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmopQq/btsMhkSEp8V/dnuYmuR5ng7bYduYJG90A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmopQq/btsMhkSEp8V/dnuYmuR5ng7bYduYJG90A1/img.png&quot; data-alt=&quot;복붙 아닙니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmopQq/btsMhkSEp8V/dnuYmuR5ng7bYduYJG90A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmopQq%2FbtsMhkSEp8V%2FdnuYmuR5ng7bYduYJG90A1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;242&quot; height=&quot;787&quot; data-filename=&quot;스크린샷 2025-02-13 오후 11.58.15.png&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;1808&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;복붙 아닙니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;또다른 방법 Azure Active Directory 앱 설정하기&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-14_00-01-24.png&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwT94v/btsMhTtFimP/Vhm2RadwwOxFS00yTR4Zsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwT94v/btsMhTtFimP/Vhm2RadwwOxFS00yTR4Zsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwT94v/btsMhTtFimP/Vhm2RadwwOxFS00yTR4Zsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwT94v%2FbtsMhTtFimP%2FVhm2RadwwOxFS00yTR4Zsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1466&quot; height=&quot;166&quot; data-filename=&quot;2025-02-14_00-01-24.png&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1. 모든 서비스 검색: azure active directory&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;2. 좌측 탭: 앱등록 클릭&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;3. 상단 새 등록 버튼 클릭&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;4. 이름 할당 및 이 조직 디렉터리의 계정만(기본 디렉터리만 - 단일 테넌트) 생성&lt;br /&gt;5. 리디렉션은 생략&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;6. 생성&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아래 ID를 필히 기억하자!!&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;애플리케이션 ID : 깃헙 시크릿매니저 등록용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;개체 ID : Azure 구독 등록용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;디렉터리 ID: 깃헙 시크릿매니저 등록용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-14_00-03-43.png&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;1550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rB08c/btsMhnPklT8/nmvt9NGvG6wxu8joP7uYH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rB08c/btsMhnPklT8/nmvt9NGvG6wxu8joP7uYH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rB08c/btsMhnPklT8/nmvt9NGvG6wxu8joP7uYH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrB08c%2FbtsMhnPklT8%2Fnmvt9NGvG6wxu8joP7uYH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1746&quot; height=&quot;1550&quot; data-filename=&quot;2025-02-14_00-03-43.png&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;1550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;6. 좌측 API 사용권한 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-14_00-05-53.png&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;33&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WlTw8/btsMivZ5WAa/5Cwk1JBnw80vikB66kSTaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WlTw8/btsMivZ5WAa/5Cwk1JBnw80vikB66kSTaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WlTw8/btsMivZ5WAa/5Cwk1JBnw80vikB66kSTaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWlTw8%2FbtsMivZ5WAa%2F5Cwk1JBnw80vikB66kSTaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;267&quot; height=&quot;33&quot; data-filename=&quot;2025-02-14_00-05-53.png&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;33&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;7. 권한추가 클릭&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;8. azure service Management 클릭&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;9. 권한추가&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-14_00-06-51.png&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLDkSP/btsMg5nUTO1/SzJecFjUjSkafUrcFPGPa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLDkSP/btsMg5nUTO1/SzJecFjUjSkafUrcFPGPa0/img.png&quot; data-alt=&quot;여기까지 따라오면 잘한 것!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLDkSP/btsMg5nUTO1/SzJecFjUjSkafUrcFPGPa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLDkSP%2FbtsMg5nUTO1%2FSzJecFjUjSkafUrcFPGPa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;415&quot; height=&quot;392&quot; data-filename=&quot;2025-02-14_00-06-51.png&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1548&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;여기까지 따라오면 잘한 것!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;10. 다시 모든 서비스 검색: 구독으로 이동&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;11. 마이크로 스폰서십 클릭&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;12. 좌측 액세스 제어(IAM) 클릭&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;13. 상단 추가 클릭 -&amp;gt; 역할 할당 추가&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;14. 권한 부여&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f89009;&quot;&gt;⚠️ 여기서 정말 삽질한 부분 2가지가 있다. 삽질을 하다가 이글까지 흘러온 개발자라면 아래를 놓치지 말 것.&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f89009;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 부여를 위에 6번에서 생성한 앱을 등록해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색할 때 개체 ID로 검색해야 한다.(개체 아이디 없으면 애플리케이션 ID, 디렉터리 ID로 검색)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성원 검색이라는 말이 Azure 사용자인 줄 알았는데 앱이라니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다른 곳에서는 개체 ID가 아닌 애플리케이션 ID를 넣으면 된다고 했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 안 떠서 설마.. 하고 개체 ID 넣었는데 검색이 되었다...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-14 오전 12.10.41.png&quot; data-origin-width=&quot;1513&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYa8ea/btsMgImm8Xs/xZNKxj4Rp8VIcvYmVKsG11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYa8ea/btsMgImm8Xs/xZNKxj4Rp8VIcvYmVKsG11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYa8ea/btsMgImm8Xs/xZNKxj4Rp8VIcvYmVKsG11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYa8ea%2FbtsMgImm8Xs%2FxZNKxj4Rp8VIcvYmVKsG11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1513&quot; height=&quot;776&quot; data-filename=&quot;스크린샷 2025-02-14 오전 12.10.41.png&quot; data-origin-width=&quot;1513&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;15. 권한 부여 방법: 구성원 - &quot;기여자&quot; 선택 -&amp;gt; 구성원 선택 -&amp;gt; 개체 ID로 검색 -&amp;gt; 할당&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-14_00-22-34.png&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKkZR0/btsMhxj13JM/QLRI5WvV9KNsirLgZ2abMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKkZR0/btsMhxj13JM/QLRI5WvV9KNsirLgZ2abMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKkZR0/btsMhxj13JM/QLRI5WvV9KNsirLgZ2abMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKkZR0%2FbtsMhxj13JM%2FQLRI5WvV9KNsirLgZ2abMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;309&quot; height=&quot;325&quot; data-filename=&quot;2025-02-14_00-22-34.png&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-02-14_00-24-32.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkwwrC/btsMi3PxObr/hpkklBRNVmQLtsSt99XpPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkwwrC/btsMi3PxObr/hpkklBRNVmQLtsSt99XpPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkwwrC/btsMi3PxObr/hpkklBRNVmQLtsSt99XpPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkwwrC%2FbtsMi3PxObr%2FhpkklBRNVmQLtsSt99XpPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1464&quot; height=&quot;260&quot; data-filename=&quot;2025-02-14_00-24-32.png&quot; data-origin-width=&quot;1464&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;만약 안 되는 부분이 있다면&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-end=&quot;86&quot; data-start=&quot;63&quot; data-ke-size=&quot;size18&quot;&gt;올바른 디렉터리(테넌트) 확인&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;271&quot; data-start=&quot;88&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;271&quot; data-start=&quot;88&quot;&gt;&lt;b&gt;디렉터리 일치 여부:&lt;/b&gt;&lt;br /&gt;서비스 프린시플(앱 등록)이 생성된 Azure Active Directory와 현재 역할 할당을 진행하는 디렉터리가 동일한지 확인&lt;br /&gt;Azure Portal 오른쪽 상단에 표시되는 디렉터리 이름이 맞는지, 필요하다면 상단의 디렉터리 전환 메뉴에서 올바른 디렉터리로 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;323&quot; data-start=&quot;278&quot; data-ke-size=&quot;size18&quot;&gt;Enterprise Applications에서 Object ID 확인&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;624&quot; data-start=&quot;325&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;527&quot; data-start=&quot;325&quot;&gt;&lt;b&gt;Enterprise Applications에서 서비스 프린시플 찾기:&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;527&quot; data-start=&quot;374&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;468&quot; data-start=&quot;374&quot;&gt;Azure Portal 좌측 메뉴에서 &lt;b&gt;&quot;Azure Active Directory&quot;&lt;/b&gt; &amp;gt; &quot;Enterprise Applications&quot;로 이동&lt;/li&gt;
&lt;li data-end=&quot;527&quot; data-start=&quot;471&quot;&gt;&quot;모든 애플리케이션(All applications)&quot; 목록에서 해당 서비스 프린시플을 찾기 &lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;624&quot; data-start=&quot;528&quot;&gt;&lt;b&gt;Object ID 복사:&lt;/b&gt;&lt;br /&gt;3. 해당 애플리케이션을 클릭한 후, &lt;b&gt;개요(Overview)&lt;/b&gt; 페이지에서 객체 ID(Object ID)를 복사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;815&quot; data-start=&quot;792&quot; data-ke-size=&quot;size18&quot;&gt;검색 필터 및 보기 옵션 조정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1035&quot; data-start=&quot;817&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;917&quot; data-start=&quot;817&quot;&gt;&lt;b&gt;필터 확인:&lt;/b&gt;&lt;br /&gt;역할 할당 선택 창 상단에 있는 필터(예: &amp;ldquo;모두 보기&amp;rdquo;, &amp;ldquo;서비스 주체&amp;rdquo;, &amp;ldquo;사용자&amp;rdquo;) 옵션을 &amp;ldquo;서비스 주체&amp;rdquo; 또는 &amp;ldquo;모두 보기&amp;rdquo;로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1062&quot; data-start=&quot;1042&quot; data-ke-size=&quot;size18&quot;&gt;권한 및 생성 상태 확인&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1250&quot; data-start=&quot;1064&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1161&quot; data-start=&quot;1064&quot;&gt;&lt;b&gt;앱 등록 상태 확인:&lt;/b&gt;&lt;br /&gt;앱 등록에서 해당 애플리케이션이 정상적으로 생성되었는지, 그리고 Enterprise Applications에도 나타나는지 재확인&lt;/li&gt;
&lt;li data-end=&quot;1250&quot; data-start=&quot;1162&quot;&gt;&lt;b&gt;추가 생성:&lt;/b&gt;만약 계속 보이지 않는다면, 앱 등록을 다시 한번 확인하고, 필요시 새 앱 등록을 만들어 동일한 과정을 진행해 볼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ff691d;&quot;&gt;&lt;b&gt;회고&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당초 계획했던 기능 개발이 일정보다 빨리 완료되어,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CTO님의 업무를 자진해서 맡았다가 중간에 살짝 도전적이었던 순간들이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 Azure DB 관련 이슈로 인해 인프라 작업에 대한 부담감이 있었지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 기회를 통해 Azure 시스템을 직접 다루면서 그동안의 심리적 장벽을 조금이나마 허물 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 새로운 방식으로 CI/CD를 구현하면서 기술적 성장을 체감할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 해결 과정에서는 시스템 전반을 단계별로 분석하고 이슈 발생 지점을 추적하는 체계적인 접근 방식을 취했 다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 세운 가설이 실제 원인과 일치하여 신속한 문제 해결이 가능했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 기술적 직관력과 문제 해결 능력이 한층 성장했음을 느낄 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;글 잼나게 보셨으면 좋아요 눌러주고 가세요&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;관심받는 거 좋아합니다&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;010&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/010.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/010.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Troubleshooting &amp;amp; Ops</category>
      <category>azure ci/cd</category>
      <category>githubaction</category>
      <category>githubaction 라벨 ci/cd</category>
      <category>githubaction 라벨 배포</category>
      <category>서이추</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/178</guid>
      <comments>https://study-easy-coding.tistory.com/178#entry178comment</comments>
      <pubDate>Fri, 14 Feb 2025 00:37:36 +0900</pubDate>
    </item>
    <item>
      <title>NestJS Exception Filters 완벽 정리 (라이프사이클부터 내부 동작까지)</title>
      <link>https://study-easy-coding.tistory.com/177</link>
      <description>&lt;h1&gt;NestJS Exception Filters 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS에는 &lt;b&gt;애플리케이션 전역에서 처리되지 않은 모든 예외를 담당하는 예외 계층(Exception Layer)&lt;/b&gt; 이 내장되어 있습니다.&lt;br /&gt;애플리케이션 코드에서 예외가 처리되지 않고 발생하면, 이 계층이 이를 포착하여 &lt;b&gt;사용자 친화적인 HTTP 응답을 자동으로 반환&lt;/b&gt;합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서: &lt;a href=&quot;https://docs.nestjs.com/exception-filters&quot;&gt;https://docs.nestjs.com/exception-filters&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NestJS 요청 라이프사이클&lt;/li&gt;
&lt;li&gt;Exception Filters의 주요 목적&lt;/li&gt;
&lt;li&gt;Exception Filters가 에러를 잡을 수 있는 이유&lt;/li&gt;
&lt;li&gt;예외 처리 방법들&lt;/li&gt;
&lt;li&gt;Exception Filters를 활용한 커스텀&lt;/li&gt;
&lt;li&gt;Filters vs Pipes&lt;/li&gt;
&lt;li&gt;Interceptor 개념 정리&lt;/li&gt;
&lt;li&gt;Guard 개념 정리&lt;/li&gt;
&lt;li&gt;Pipes와 Query / Body 처리 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NestJS 요청 라이프사이클&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-01-13_10-51-39.png&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BTAyN/dJMcadgoQVw/jgs7CHrMPYAqSPeGqn4Zik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BTAyN/dJMcadgoQVw/jgs7CHrMPYAqSPeGqn4Zik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BTAyN/dJMcadgoQVw/jgs7CHrMPYAqSPeGqn4Zik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBTAyN%2FdJMcadgoQVw%2Fjgs7CHrMPYAqSPeGqn4Zik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1696&quot; height=&quot;480&quot; data-filename=&quot;2026-01-13_10-51-39.png&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 요청이 컨트롤러에 도달하기까지의 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트 요청&lt;/li&gt;
&lt;li&gt;미들웨어 실행 (로깅, 인증 등)&lt;/li&gt;
&lt;li&gt;가드 실행 (권한 체크: AuthGuard, AdminGuard 등)&lt;/li&gt;
&lt;li&gt;인터셉터 시작&lt;/li&gt;
&lt;li&gt;파이프 실행 (데이터 변환 / 유효성 검사)&lt;/li&gt;
&lt;li&gt;컨트롤러 메서드 실행&lt;/li&gt;
&lt;li&gt;서비스 로직 실행&lt;/li&gt;
&lt;li&gt;인터셉터 종료&lt;/li&gt;
&lt;li&gt;응답 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 에러가 발생하면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정 중 &lt;b&gt;어디에서든 예외가 발생하면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Exception Filter가 에러를 캐치&lt;/li&gt;
&lt;li&gt;적절한 에러 응답을 클라이언트에 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Exception Filters의 주요 목적&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ 에러 캐치&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 라이프사이클 전역에서 발생한 예외를 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ 에러 커스텀&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러 응답 포맷을 프로젝트 기준에 맞게 통일&lt;/li&gt;
&lt;li&gt;메시지, timestamp, path 등 추가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Exception Filters가 에러를 잡을 수 있는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 @Catch() 데코레이터입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;export declare function Catch(
  ...exceptions: Array&amp;lt;Type&amp;lt;any&amp;gt; | Abstract&amp;lt;any&amp;gt;&amp;gt;
): ClassDecorator;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의미 정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;...exceptions
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 예외 타입을 Rest Parameter로 전달 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Type
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구체적인 클래스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Abstract
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추상 클래스 또는 인터페이스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반환값
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ClassDecorator&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// Type 예시
class HttpException {}

// Abstract 예시
abstract class BaseException {}
interface IException {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  즉, &lt;b&gt;특정 예외 타입만 골라서 필터링 가능&lt;/b&gt;합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Node Modules 내부 동작 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BaseExceptionFilter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 예외 처리 로직을 담당하는 추상 필터 클래스입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;export declare class BaseExceptionFilter&amp;lt;T = any&amp;gt;
  implements ExceptionFilter&amp;lt;T&amp;gt; {
  catch(exception: T, host: ArgumentsHost): void;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 catch() 구현 제공&lt;/li&gt;
&lt;li&gt;알 수 없는 에러 처리 로직 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BaseExceptionFilterContext&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터 인스턴스 생성 및 관리&lt;/li&gt;
&lt;li&gt;DI 컨테이너와 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ExceptionsHandler&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외 처리 시스템의 중심&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;발생한 예외를 적절한 필터로 라우팅&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;export declare class ExceptionsHandler extends BaseExceptionFilter {
  next(exception: any, ctx: ArgumentsHost): void;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 흐름 요약&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;예외 발생
  &amp;darr;
Router / Middleware (최초 캐치)
  &amp;darr;
ExceptionsHandler
  &amp;darr;
BaseExceptionFilterContext
  &amp;darr;
BaseExceptionFilter or ExternalExceptionsFilter
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예외 처리 방법들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 제공 방식&lt;/h3&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;throw new HttpException('접근 금지!', HttpStatus.FORBIDDEN);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;statusCode&quot;: 403,
  &quot;message&quot;: &quot;접근 금지!&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;응답을 더 커스터마이징하고 싶다면?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ Exception Filter를 사용합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Exception Filters를 활용한 커스텀&lt;/h2&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@Catch(HttpException)
export class MyExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse();

    response.status(403).json({
      statusCode: 403,
      timestamp: new Date(),
      message: '죄송합니다. 접근 권한이 없습니다.',
      path: '/admin',
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 쓰면 좋을까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러 로깅을 하고 싶을 때&lt;/li&gt;
&lt;li&gt;에러 응답 포맷을 통일하고 싶을 때&lt;/li&gt;
&lt;li&gt;특정 예외만 다르게 처리하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 예외 처리&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;@Catch(HttpException, ValidationError)
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@Catch()
export class CatchAllFilter implements ExceptionFilter {}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Exception Filters vs Pipes&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exception Filters&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;발생한 예외를 처리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;클라이언트에 반환할 응답을 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pipes&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러 진입 &lt;b&gt;이전&lt;/b&gt;에 실행&lt;/li&gt;
&lt;li&gt;데이터 변환 / 유효성 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ Pipe에서 발생한 예외도 결국 Exception Filter에서 처리됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Interceptor 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 실행 전/후 로직 추가 (AOP)&lt;/li&gt;
&lt;li&gt;응답 변환&lt;/li&gt;
&lt;li&gt;예외 변환&lt;/li&gt;
&lt;li&gt;캐싱, 로깅, 타임아웃 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;any&amp;gt; {
    const now = Date.now();
    console.log('Before...');

    return next.handle().pipe(
      tap(() =&amp;gt; console.log(`After... ${Date.now() - now}ms`)),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Guard 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Guard란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;라우트 핸들러 실행 여부를 결정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;권한, 인증, 역할 기반 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export interface CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise&amp;lt;boolean&amp;gt; | Observable&amp;lt;boolean&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Guard vs Middleware&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분GuardMiddleware&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&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;/td&gt;
&lt;td&gt;요청/응답 객체 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;ExecutionContext 접근 가능&lt;/td&gt;
&lt;td&gt;핸들러 정보 접근 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pipes와 Query / Body 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Query String&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;statusCode&quot;: 400,
  &quot;message&quot;: &quot;Validation failed (numeric string is expected)&quot;,
  &quot;error&quot;: &quot;Bad Request&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  잘못된 값이면 &lt;b&gt;컨트롤러 진입 자체를 막음&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Body 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pipe로 직접 검증 ❌&lt;/li&gt;
&lt;li&gt;class-validator 사용 ⭕️&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;export class CreateEventBody {
  @IsUUID()
  uuid?: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Exception Filter는 &lt;b&gt;에러 처리의 마지막 보루&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Pipe / Guard / Interceptor와 함께 사용할 때 진짜 위력이 나옵니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  글이 도움이 됐다면 좋아요 한 번 눌러주세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심 받는 거 좋아합니다  &lt;/p&gt;</description>
      <category>  Backend Engineering</category>
      <category>NestJS</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/177</guid>
      <comments>https://study-easy-coding.tistory.com/177#entry177comment</comments>
      <pubDate>Wed, 12 Feb 2025 10:36:49 +0900</pubDate>
    </item>
    <item>
      <title>테스트 코드 현실 적용기: Unit Testing 책을 통한 인사이트와 실무 경험</title>
      <link>https://study-easy-coding.tistory.com/176</link>
      <description>&lt;h1&gt;Unit Testing 책 리뷰와 실무 적용기&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Unit Testing: Principles, Practices, and Patterns&quot;(Vladimir Khorikov 저) 책을 읽고 실무에 적용하면서 느낀 점을 정리해보았습니다. 테스트 코드를 작성하는 여러 방법론과 실제 현장에서의 적용 과정에서 배운 점들을 공유하고자 합니다.&lt;br /&gt;(실은 읽다가 어려워서 누락된 내용도 꽤 많아요 ㅎㅎ이해하고 넘어가주세요)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;책의 핵심 내용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트의 기본 원칙&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트는 비즈니스 시나리오를 반영해야 함&lt;/li&gt;
&lt;li&gt;테스트 가독성이 중요&lt;/li&gt;
&lt;li&gt;테스트가 구현이 아닌 동작을 검증해야 함&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단위 테스트의 정의&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;작은 코드조각을 검증하고&lt;/li&gt;
&lt;li&gt;빠르게 수행하고&lt;/li&gt;
&lt;li&gt;격리된 방식으로 처리하는 자동화된 테스트&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고전파 vs 런던파&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;격리 문제로 인한 두 학파의 차이&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고전파: 실제 의존성을 사용&lt;/li&gt;
&lt;li&gt;런던파: 모든 의존성을 mock으로 대체&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738158463215&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 고전파 예시
test('주문 생성', () =&amp;gt; {
  // 실제 의존성 사용
  const db = new Database();
  const payment = new Payment();
  
  const order = new Order(db, payment);
  order.create();
  
  expect(db.findOrder()).toBeDefined();
});

// 런던파 예시
test('주문 생성', () =&amp;gt; {
  // 모든 의존성을 mock으로 대체
  const mockDB = { save: jest.fn() };
  const mockPayment = { process: jest.fn() };
  
  const order = new Order(mockDB, mockPayment);
  order.create();
  
  expect(mockDB.save).toHaveBeenCalled();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 학파의 주요 차이점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트 주도 개발을 통한 시스템 설계 방식&lt;/li&gt;
&lt;li&gt;과도한 명세 문제&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 주도 개발(TDD)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TDD의 핵심 원칙&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실패하는 테스트를 먼저 작성&lt;/li&gt;
&lt;li&gt;코드가 완벽하지 않아도 됨&lt;/li&gt;
&lt;li&gt;읽기 쉽고 유지보수가 용이하도록 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 적용 경험&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 진행 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기획 완료 후 프론트엔드/백엔드 동시 개발 시작&lt;/li&gt;
&lt;li&gt;데이터 모델 설계&lt;/li&gt;
&lt;li&gt;Swagger를 통한 API 문서화 및 소통&lt;/li&gt;
&lt;li&gt;개발 완료 후 API 테스트 및 QA&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무에서 겪은 어려움과 해결책&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger 문서 작성이 TDD로 인해 지연되는 문제 발생&lt;/li&gt;
&lt;li&gt;해결책: Swagger용 mock 객체 먼저 생성 후 TDD 진행&lt;/li&gt;
&lt;li&gt;현재는 API 개발 후 AI 도움을 받아 테스트 케이스 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 유형별 특징&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;통합 테스트와 E2E 테스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통합 테스트: 여러 컴포넌트간 상호작용 검증&lt;/li&gt;
&lt;li&gt;E2E 테스트: 전체 시스템 흐름 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 대역(Test Double)의 이해&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;더미(Dummy): 단순 전달용 객체&lt;/li&gt;
&lt;li&gt;스텁(Stub): 미리 준비된 응답 제공&lt;/li&gt;
&lt;li&gt;스파이(Spy): 메서드 호출 기록&lt;/li&gt;
&lt;li&gt;목(Mock): 예상 동작 검증&lt;/li&gt;
&lt;li&gt;페이크(Fake): 간소화된 실제 구현체&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블랙박스/화이트박스 테스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;블랙박스: 내부 구조를 모르는 상태에서 테스트&lt;/li&gt;
&lt;li&gt;화이트박스: 내부 구조를 아는 상태에서 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 피라미드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 코드의 이상적인 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트 &amp;gt;&amp;gt; 통합 테스트 &amp;gt;&amp;gt; E2E 테스트 순으로 작성 권장&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;피라미드 구조를 권장하는 이유&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;E2E 테스트의 한계
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;높은 유지보수 비용&lt;/li&gt;
&lt;li&gt;긴 실행 시간&lt;/li&gt;
&lt;li&gt;불안정성 (깨지기 쉬움)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단위 테스트의 장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 실행 속도&lt;/li&gt;
&lt;li&gt;안정적인 결과&lt;/li&gt;
&lt;li&gt;문제 발생 지점 특정 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 유형별 코드 예시&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 단위 테스트&lt;/h4&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1738158415898&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 독립적인 단위 기능 테스트
test('이메일 유효성 검사', () =&amp;gt; {
  const validator = new EmailValidator();
  expect(validator.isValid('test@email.com')).toBe(true);
  expect(validator.isValid('invalid-email')).toBe(false);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 통합 테스트&lt;/h4&gt;
&lt;pre id=&quot;code_1738158403365&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 여러 컴포넌트 연동 테스트
test('유저 생성 및 이메일 발송', async () =&amp;gt; {
  const userService = new UserService(database, emailService);
  const user = await userService.createUser({
    email: 'test@email.com',
    name: 'John'
  });
  
  // 실제 DB 저장 및 이메일 발송 검증
  const savedUser = await database.findUser(user.id);
  expect(savedUser).toBeDefined();
});&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. E2E 테스트&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1738158388180&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 전체 시스템 흐름 테스트
test('회원가입 프로세스', async () =&amp;gt; {
  // 회원가입
  const signupRes = await request(app)
    .post('/api/signup')
    .send({
      email: 'test@email.com',
      password: 'password123'
    });
  
  // 로그인 및 토큰 발급
  const loginRes = await request(app)
    .post('/api/login')
    .send({
      email: 'test@email.com',
      password: 'password123'
    });
  
  // 인증이 필요한 API 호출
  const profileRes = await request(app)
    .get('/api/profile')
    .set('Authorization', loginRes.body.token);
  
  expect(profileRes.status).toBe(200);
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 접근 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블랙박스 테스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 구현을 모르는 상태에서 테스트&lt;/li&gt;
&lt;li&gt;What에 집중 (어떤 결과가 나와야 하는가)&lt;/li&gt;
&lt;li&gt;명세와 요구사항 기반 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;화이트박스 테스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 구현을 아는 상태에서 테스트&lt;/li&gt;
&lt;li&gt;How까지 고려 (어떻게 동작하는가)&lt;/li&gt;
&lt;li&gt;더 철저한 테스트 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 대역의 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 대역의 다섯 가지 유형과 특징:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;더미(Dummy)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 단순한 형태&lt;/li&gt;
&lt;li&gt;실제로 사용되지 않고 전달만 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스텁(Stub)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미리 준비된 응답 제공&lt;/li&gt;
&lt;li&gt;상태 기반 테스트에 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스파이(Spy)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호출 정보를 기록&lt;/li&gt;
&lt;li&gt;실제 객체처럼 동작하면서 정보 수집&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;목(Mock)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기대하는 동작 정의 및 검증&lt;/li&gt;
&lt;li&gt;행위 검증에 중점&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;페이크(Fake)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 구현을 단순화한 대체제&lt;/li&gt;
&lt;li&gt;실제와 유사하게 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 대역의 선택은 테스트의 목적과 상황에 따라 결정해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 적용 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 팀의 테스트 전략&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;단위 테스트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 레이어의 비즈니스 로직 검증&lt;/li&gt;
&lt;li&gt;DB 의존성 없는 순수 로직 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;E2E 테스트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 엔드포인트 검증&lt;/li&gt;
&lt;li&gt;실제 사용자 시나리오 기반 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 작성 프로세스&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;API 설계 및 개발&lt;/li&gt;
&lt;li&gt;시나리오 도출&lt;/li&gt;
&lt;li&gt;테스트 케이스 작성&lt;/li&gt;
&lt;li&gt;AI 도구를 활용한 테스트 보완&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>  Backend Engineering</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/176</guid>
      <comments>https://study-easy-coding.tistory.com/176#entry176comment</comments>
      <pubDate>Wed, 29 Jan 2025 22:19:00 +0900</pubDate>
    </item>
    <item>
      <title>2024년 연말회고</title>
      <link>https://study-easy-coding.tistory.com/175</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-28-53.png&quot; data-origin-width=&quot;2650&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwBgdO/btsLDmB8YaW/nick8ZSsZvoAakGqZo1eOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwBgdO/btsLDmB8YaW/nick8ZSsZvoAakGqZo1eOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwBgdO/btsLDmB8YaW/nick8ZSsZvoAakGqZo1eOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwBgdO%2FbtsLDmB8YaW%2Fnick8ZSsZvoAakGqZo1eOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2650&quot; height=&quot;1012&quot; data-filename=&quot;2025-01-01_13-28-53.png&quot; data-origin-width=&quot;2650&quot; data-origin-height=&quot;1012&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-28-59.png&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;968&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RFeCZ/btsLCLoPYzJ/KnaVzxf05GaTvyk1SQ47DK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RFeCZ/btsLCLoPYzJ/KnaVzxf05GaTvyk1SQ47DK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RFeCZ/btsLCLoPYzJ/KnaVzxf05GaTvyk1SQ47DK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRFeCZ%2FbtsLCLoPYzJ%2FKnaVzxf05GaTvyk1SQ47DK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2956&quot; height=&quot;968&quot; data-filename=&quot;2025-01-01_13-28-59.png&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;968&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자잘자잘하게 따져보면 flutter, JSP, 노코드 등등도 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-03.png&quot; data-origin-width=&quot;2966&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/db29eO/btsLCsQzDsi/OAvEUP8Kf2guidzSrtW060/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/db29eO/btsLCsQzDsi/OAvEUP8Kf2guidzSrtW060/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/db29eO/btsLCsQzDsi/OAvEUP8Kf2guidzSrtW060/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdb29eO%2FbtsLCsQzDsi%2FOAvEUP8Kf2guidzSrtW060%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2966&quot; height=&quot;1120&quot; data-filename=&quot;2025-01-01_13-29-03.png&quot; data-origin-width=&quot;2966&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽히 NestJS + TS 개발자가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 특히 TyepsScript 핸드북 Doc을 모두 정독하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 시험을 보면서 TS와 TS 모듈에 대한 더 깊은 이해를 가지게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-07.png&quot; data-origin-width=&quot;3002&quot; data-origin-height=&quot;1470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMqb4L/btsLBgX3HAG/qR9LzeYudeF8ce2zXSP8xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMqb4L/btsLBgX3HAG/qR9LzeYudeF8ce2zXSP8xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMqb4L/btsLBgX3HAG/qR9LzeYudeF8ce2zXSP8xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMqb4L%2FbtsLBgX3HAG%2FqR9LzeYudeF8ce2zXSP8xK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3002&quot; height=&quot;1470&quot; data-filename=&quot;2025-01-01_13-29-07.png&quot; data-origin-width=&quot;3002&quot; data-origin-height=&quot;1470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;외부 시스템 연동&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오퍼월과 기프티콘 시스템을 적용하기 위해 외부 업체와 협업한 일이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소통 단계에서 요구해야할 사항들(ex: 개발계/운영계/테스트계 화이트리스트 IP등록)과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 업체와 메일 소통의 방법에 대해 많이 배우게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 기프티콘 시스템을 도입하게 되면서 도메인에 대한 이해와 예외적으로 발생할 수 있는 상황들을 경험하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ex: 업체의 문제로 상품 구매가 안될 경우가 존재 : 임시발급 단계 -&amp;gt; 발급과정을 거치고, try-catch로 상품 구매 에러가 발생할 경우&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬랙 알림 메시지 알림 + 상품 구매 시스템 점검으로 바꾸기 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-12.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buA9Ic/btsLCnB02AU/ZYY8U5vI1hg38vUo4WXCj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buA9Ic/btsLCnB02AU/ZYY8U5vI1hg38vUo4WXCj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buA9Ic/btsLCnB02AU/ZYY8U5vI1hg38vUo4WXCj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuA9Ic%2FbtsLCnB02AU%2FZYY8U5vI1hg38vUo4WXCj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1518&quot; data-filename=&quot;2025-01-01_13-29-12.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;성능 최적화&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. HardDelete를 SoftDelete로 바꾸고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;무거운 부하는 배치 작업으로 새벽에 돌리도록 바꾸었다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 페이지네이션 방식을 Offest -&amp;gt; Cursor 방식으로&amp;nbsp; 바꾸었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. Index를 적용했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;25년 현재 Redis와 MessageQueue를 이용하여 최적화 작업을 계속 진행 중에 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-16.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2qv7y/btsLAyRVuSY/lf40k4AzqTAglKJksRnEhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2qv7y/btsLAyRVuSY/lf40k4AzqTAglKJksRnEhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2qv7y/btsLAyRVuSY/lf40k4AzqTAglKJksRnEhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2qv7y%2FbtsLAyRVuSY%2Flf40k4AzqTAglKJksRnEhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1464&quot; data-filename=&quot;2025-01-01_13-29-16.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-19.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8lcjX/btsLByw44St/wEq828s3GW8PW6oo2h5JHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8lcjX/btsLByw44St/wEq828s3GW8PW6oo2h5JHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8lcjX/btsLByw44St/wEq828s3GW8PW6oo2h5JHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8lcjX%2FbtsLByw44St%2FwEq828s3GW8PW6oo2h5JHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1272&quot; data-filename=&quot;2025-01-01_13-29-19.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주말근무와 야근으로 절대적인 시간이 많아졌다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-27.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pPgQ6/btsLBjmr5V8/liktwDpxlgdjD0eyEtAjR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pPgQ6/btsLBjmr5V8/liktwDpxlgdjD0eyEtAjR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pPgQ6/btsLBjmr5V8/liktwDpxlgdjD0eyEtAjR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpPgQ6%2FbtsLBjmr5V8%2FliktwDpxlgdjD0eyEtAjR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;850&quot; data-filename=&quot;2025-01-01_13-29-27.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-32.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xHWso/btsLCpzR53O/8MTfNbl3LVz8Uh6kuOagc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xHWso/btsLCpzR53O/8MTfNbl3LVz8Uh6kuOagc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xHWso/btsLCpzR53O/8MTfNbl3LVz8Uh6kuOagc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxHWso%2FbtsLCpzR53O%2F8MTfNbl3LVz8Uh6kuOagc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1334&quot; data-filename=&quot;2025-01-01_13-29-32.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-35.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QeEOZ/btsLBldC9ga/QG2I1NULm8tqn0U618Rq00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QeEOZ/btsLBldC9ga/QG2I1NULm8tqn0U618Rq00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QeEOZ/btsLBldC9ga/QG2I1NULm8tqn0U618Rq00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQeEOZ%2FbtsLBldC9ga%2FQG2I1NULm8tqn0U618Rq00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1336&quot; data-filename=&quot;2025-01-01_13-29-35.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-37.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIaSWW/btsLCpT3KHX/RiDKuONsrTfy8JkNhYFGXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIaSWW/btsLCpT3KHX/RiDKuONsrTfy8JkNhYFGXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIaSWW/btsLCpT3KHX/RiDKuONsrTfy8JkNhYFGXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIaSWW%2FbtsLCpT3KHX%2FRiDKuONsrTfy8JkNhYFGXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1156&quot; data-filename=&quot;2025-01-01_13-29-37.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-29-41.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFXZgz/btsLBw0kqsb/VgSFFHJiZvrO14x6zxrlHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFXZgz/btsLBw0kqsb/VgSFFHJiZvrO14x6zxrlHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFXZgz/btsLBw0kqsb/VgSFFHJiZvrO14x6zxrlHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFXZgz%2FbtsLBw0kqsb%2FVgSFFHJiZvrO14x6zxrlHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;878&quot; data-filename=&quot;2025-01-01_13-29-41.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-33-35.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1094&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s2lHf/btsLBwy8OhT/IRw4BOL2iJBhNkvUwenRgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s2lHf/btsLBwy8OhT/IRw4BOL2iJBhNkvUwenRgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s2lHf/btsLBwy8OhT/IRw4BOL2iJBhNkvUwenRgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs2lHf%2FbtsLBwy8OhT%2FIRw4BOL2iJBhNkvUwenRgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1094&quot; data-filename=&quot;2025-01-01_13-33-35.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1094&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-33-40.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AIiXo/btsLCEcnzm0/P9q3KBGGhU4aar3fX5f3OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AIiXo/btsLCEcnzm0/P9q3KBGGhU4aar3fX5f3OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AIiXo/btsLCEcnzm0/P9q3KBGGhU4aar3fX5f3OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAIiXo%2FbtsLCEcnzm0%2FP9q3KBGGhU4aar3fX5f3OK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1194&quot; data-filename=&quot;2025-01-01_13-33-40.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-33-44.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kYeFp/btsLBlq6GBR/FXUYwKssxtkwGR5x1455v1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kYeFp/btsLBlq6GBR/FXUYwKssxtkwGR5x1455v1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kYeFp/btsLBlq6GBR/FXUYwKssxtkwGR5x1455v1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkYeFp%2FbtsLBlq6GBR%2FFXUYwKssxtkwGR5x1455v1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1214&quot; data-filename=&quot;2025-01-01_13-33-44.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-01-01_13-34-05.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cP4nEP/btsLDwY1BDh/WknAMKC4OxcMk3DXvT98Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cP4nEP/btsLDwY1BDh/WknAMKC4OxcMk3DXvT98Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cP4nEP/btsLDwY1BDh/WknAMKC4OxcMk3DXvT98Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcP4nEP%2FbtsLDwY1BDh%2FWknAMKC4OxcMk3DXvT98Wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;636&quot; data-filename=&quot;2025-01-01_13-34-05.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Reflections/Year-in-Review</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/175</guid>
      <comments>https://study-easy-coding.tistory.com/175#entry175comment</comments>
      <pubDate>Wed, 1 Jan 2025 13:34:20 +0900</pubDate>
    </item>
    <item>
      <title>2년 치 회사 DB 날려서 법원 갈 뻔한 사람이 기록하는 회고록</title>
      <link>https://study-easy-coding.tistory.com/173</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VDKS5/btsI1nKAYKQ/AlqrQKEn8EYB0CwAjxfc00/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VDKS5/btsI1nKAYKQ/AlqrQKEn8EYB0CwAjxfc00/img.jpg&quot; data-alt=&quot;타노스도 우주의 절반만 날리는데...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VDKS5/btsI1nKAYKQ/AlqrQKEn8EYB0CwAjxfc00/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVDKS5%2FbtsI1nKAYKQ%2FAlqrQKEn8EYB0CwAjxfc00%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;333&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;333&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;타노스도 우주의 절반만 날리는데...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;'딸깍'&amp;nbsp;&lt;br /&gt;약 200GB 데이터가 84KB가 되었던 순간.&lt;br /&gt;대략 99.99996%를 날려버린 나는, 타노스보다 더 한 악당이 되었다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;사건의 시작 - 부제: 앱 접속 시 회원가입이 되는데요?&lt;/h4&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2024 07.29 오전 11시 30분&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;욕 한마디 안 하시던 CTO님이&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아침 회의 이후 화장실에 다녀오시더니 급히 문을 열고 들어오셨다.&lt;br /&gt;평소엔 차분한 그분의 목소리가, 그날만은 달랐다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;'시x 다들 앱 실행해 보세요!'&lt;br /&gt;나는 &quot;응? 무슨 일이지&quot; 하는 마음으로 앱을 열었는데, 회원가입이 되는 걸 보았고,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;뭔가 단단히 잘못되었다고 느꼈다.&lt;br /&gt;그 순간 운영 업무 하는 분이 '갑자기 고객문의가 계속 들어오고 있어요!'&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;말을 전해주면서 등에 식은땀이 나기 시작했다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;select * from users limit 10;&amp;nbsp; result = 0;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;DB 유저 정보를 조회하는데 하나도 뜨지 않았다.&lt;br /&gt;&quot;응?&quot;,&lt;br /&gt;&quot;local DB에 쿼리를 날렸나?&quot;&lt;br /&gt;다시 한번 DB 이름을 확인했다.&amp;nbsp;&lt;br /&gt;&quot;응...?&quot;&lt;br /&gt;main DB 이름이 맞았다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;.&lt;br /&gt;&lt;i&gt;&quot;이건 꿈일 거야&quot;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;문제가 될 수 있는 부분들을 파악해 보았다.&lt;br /&gt;그리고 곧바로 알아낼 수 있었다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;i&gt;. env 파일에 main DB URL과 local DB URL 모두 주석이 해제되어 있었다.&lt;/i&gt;&lt;br /&gt;(필자는 prisma orm을 사용한다)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;정말 내가 한 실수 때문일까?&amp;nbsp;&lt;br /&gt;잘못된 DB 주소를 검색한 게 아닐까?&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;라는 생각들을 하며&amp;nbsp;&lt;br /&gt;내 실수로 발생한 게 아니길 바랐다.&lt;br /&gt;&lt;br /&gt;그러나 나도 알았다.&amp;nbsp;&lt;br /&gt;내 실수로 일어난 일이란 것을...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;이실직고&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;'제가 잘못한 것 같습니다.'&lt;br /&gt;나는 이 말을 뱉을 수밖에 없었고&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;말하지 싫었던, 하지만 말할 수밖에 없던&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그 순간의 감정과 심장박동을 잊을 수 없다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;prima ORM을 이용해서 테이블을 생성할 때&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;--create-only 옵션을 키지 않고, 실서버로 연결해 두어서 발생했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;사내 모든 업무 중지&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;에어컨을 최저로 낮추고 있던 사무실이었는데도&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;온몸이 땀에 젖어 축축했다.&lt;br /&gt;일단은 DB 복구가 제일 먼저다라는 생각이 들었다. &lt;br /&gt;그러면서도&lt;br /&gt;&lt;br /&gt;시간이 갈수록 점점 패닉상태가 되었다.&lt;br /&gt;속으로 &quot;하 xx x 됐다...&quot;와 &quot;개발자 인생은 이제 끝이다&quot;라는 생각이 계속 맴돌었다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;우선 고객 대응을 위해&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;대표님 포함, 개발직군 외에&amp;nbsp; CS팀에 편성되어 고객응대를 하였고,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;프런트 개발자와 디자이너분이&lt;br /&gt;서비스에는 비상 전면 앱 공지 팝업을 띄웠다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 백엔드를 담당하는 CTO님과 나는&amp;nbsp;&lt;br /&gt;계속 해결법을 찾았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;속으로 &quot;하 xx...&quot;이라는 말이 계속 나왔다...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;한 줄기의 빛 aurora DB... 사랑해...&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;CTO님과 붙어 &lt;br /&gt;초기화 됐던 시점, DB 스펙 점검, DB스펙에서 특정 시점으로 롤백하는 방법을 포함하여 쿼리 로그를 찾아보았다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그러던 중 CTO님이 aurora DB에서 시점으로 롤백하는 기능이 있는 것을 알아내셨고,&amp;nbsp;&lt;br /&gt;사건이 터진 시점은 10시 37분이었지만 여러 가지를 고려해 2분 차이를 두고 10시 35분으로 롤백할 수 있었다.&lt;br /&gt;(2분 차이를 둔 이유는 찰나의 순간으로 생성된 데이터 보존을 위함이었다)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;DB upgrade내역을 찾아보니&lt;br /&gt;약 3달 전인 24년 4월 DB lack이 있어서 aurora DB로 업그레이드했었는데,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;aurora DB 스펙에서 롤백기능을 제공했고,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이전에 쓰던 DB에서는 롤백 기능을 제공하지 않았다.&lt;br /&gt;만약 4월 이전에 이런 일이 있었다면...&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;하아.... 난 감방 가야 했다&lt;br /&gt;&amp;nbsp;&lt;br /&gt;새로운 auroraDB를 띄우고 proxy 서버와 연결을 해주고, proxy DB 설정도 다 해주고&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;3시간 정도 흐른&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;오후 2시 즈음이 되어야 DB를 복구를 할 수 있었다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;동료들과 CTO님께 감사하고 너무 죄송했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;이때 끊었던 담배를 다시 피우게 되었고, &lt;/s&gt;&lt;br /&gt;&lt;s&gt;등과 바지가 흥건히 땀에 절어 있었다.&lt;/s&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;해결 이후&amp;nbsp;&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;로그인이 되지 않아 해당 시간에 접속한 유저뿐 아니라 모든 유저에게 보상 금액을 지원했다.&lt;br /&gt;대략 n천만 원의 포인트가 유저들에게 지급되었다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;다른 팀원들도 당일 해야 하는 업무가 있었는데 업무가 모두 밀렸다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;대표님께 면담을 요청했다.&lt;br /&gt;이 회사에서 서버를 날린 이력도 있었는데,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이번엔 DB를 날려버렸다.&lt;br /&gt;나의 개발자 인생은 &quot;여기까지다&quot;라고 생각했다.&lt;br /&gt;암울했다. 죄송했다. 죽고 싶은 심정이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;대표님에게 책임지고 퇴사를 하겠다고 했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;대표님은&lt;br /&gt;&quot;당연히 징계는 있다. 하지만 OO 씨의 퇴사를 생각한 건 아니다&quot;라는 말과 함께&lt;br /&gt;&quot;퇴사는 책임지는 게 아니라 회피다&quot;&lt;br /&gt;&quot;이 계기로 더 성장했으면 좋겠다.&quot;&lt;br /&gt;&quot;바빠서 인프라를 못 챙긴 회사 책임도 있다&quot;&amp;nbsp;&lt;br /&gt;라고 해주셨다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;나는 대표님과 CTO님을 포함한 동료들이&lt;br /&gt;누구보다 일찍 출근하고 늦게 퇴근하고,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;고객에게 어떤 기쁨을 줄지 매일 고민하는 것을 보면서&amp;nbsp;&lt;br /&gt;어떤 마음으로 서비스를 키워가고 있는지 너무도 잘 아는 마음에&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;죄송함으로 며칠을 보낸 기억이 아직도 생생하다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;재발 방지 그리고 회고&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이번 사건을 회고해 보았을 때 왜 이런 일이 발생했을까 생각해 보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;1. 바쁘다고 미루고 미루던 DB 계정 별 접근 권한을 주지 않았다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;너무도 당연한 이야기지만,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;DB 접근 계정에 권한을 주지 않았다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발은 CTO님과 나 둘이 담당하고&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;워낙 많은 업무를 처리하다 보니 넘어갔었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;(&lt;s&gt;귀찮음이 컸다.&lt;/s&gt;)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 날 이후로&amp;nbsp;&lt;br /&gt;&amp;nbsp;Main, QA DB 별 계정 권한에 read-only 계정과&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;create, update, read권한을 부여한 계정을 생성했다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 모두 deleted, drop 권한은 주지 않았다.&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 바쁘다고 업무 루틴을 지키지 않았다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;핑계라면 핑계지만,,, 전 날 노트북이 맛 가서 초기화를 진행했다.&lt;br /&gt;그 이후로 키보드 배열 세팅등 사소한 부분들이 모두 초기화되었는데,&amp;nbsp;&lt;br /&gt;달라진 환경에서 prisma 배포하는 과정에서 체크하는 루틴이 있었는데&amp;nbsp;&lt;br /&gt;업무 루틴을 지키지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;싸함은 틀리지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 컨디션 관리&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;아침부터 뇌가 멍했다.&lt;br /&gt;과로로 인해 수면을 충분히 취하지 못했고,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;카페인에 뇌가 절여진 것 같은 집중력이 한몫했던 것 같다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;앞으로는 절대, 어떤 회사를 가더라도 DB 권한 체크부터 할 것이다.&lt;br /&gt;그리고 멘탈도 많이 나갔는데, 정신 다 잡고, 공부를 더 해야겠다고 느꼈다.&lt;br /&gt;요즘은 아침 1-2시간 이른 출근 후 공부를 하고 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;To. 이 글을 보고 있는 친애하는 개발자 분들에게.&amp;nbsp;&lt;/b&gt;&lt;br /&gt;나의 이 당시 감정이 100% 전달될 순 없겠지만,&lt;br /&gt;세상의 어떤 개발자에게 이런 비극적이고 참혹한 일이 일어났었습니다.&lt;br /&gt;자신이 현재 겪고 있는 개발적인 어려움은 저의 일로 위안이 되시길 바랍니다.&lt;br /&gt;일정에 쫓기고, 상사한테 욕을 먹을 때마다&lt;br /&gt;&quot;에이 그래... DB 날린 것도 아닌데 뭐~&quot; 이런 마인드셋으로 이겨내시길 바랍니다.&lt;br /&gt;그리고 권한 부여는 꼭 합시다...&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Reflections/Work &amp;amp; Career</category>
      <category>db 날린 썰</category>
      <category>db 날림</category>
      <category>db 초기화</category>
      <category>개발썰</category>
      <category>서이추</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/173</guid>
      <comments>https://study-easy-coding.tistory.com/173#entry173comment</comments>
      <pubDate>Sat, 10 Aug 2024 15:37:12 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 게시판 DB 스냅샷 구조 vs 히스토리 구조 차이점 및 정리</title>
      <link>https://study-easy-coding.tistory.com/172</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 NestJS 개발 중 알게 된 스냅샷과 히스토리 개념에 대해 이야기해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS 개발 커뮤니티에서 유명한 '삼촌님'의 깃허브 레포지토리를 참고하면서 스냅샷 방식을 도입하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 히스토리성 로그 방식의 테이블을 사용했는데요,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 두 방식의 차이점과 각각의 특징, 그리고 도입 예제에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스냅샷과 히스토리성 테이블의 차이점&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 저장 방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;히스토리 테이블:&amp;nbsp; 변경된 내용만 순차적으로 기록합니다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&quot;어제 뭐 했더라...&quot; (매일매일 일기 쓰는 느낌)&lt;/li&gt;
&lt;li&gt;스냅샷: 특정 시점의 전체 상태를 저장합니다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&quot;자, 여기 내 전재산!&quot; (인생 통장 사진 찍기)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조회 성능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;히스토리 테이블&lt;/b&gt;: 최신 상태를 얻기 위해 여러 레코드를 조회하고 계산해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스냅샷&lt;/b&gt;: 최신 스냅샷만 조회하면 되어 빠른 조회가 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저장 공간&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;히스토리 테이블&lt;/b&gt;: 변경 사항만 저장하여 공간 효율적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스냅샷&lt;/b&gt;: 전체 상태를 저장하므로 더 많은 공간이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복원 용이성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;히스토리 테이블&lt;/b&gt;: 특정 시점으로의 복원이 복잡할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스냅샷&lt;/b&gt;: 원하는 시점의 스냅샷을 선택하여 쉽게 복원이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현 복잡도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;히스토리 테이블&lt;/b&gt;: 변경 추적 로직이 복잡할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스냅샷&lt;/b&gt;: 상대적으로 단순한 구현입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 삼촌님의 ERD 설계도 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 링크를 클릭하시면 상세히 보실 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721551792498&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;bbs-backend/docs/ERD.md at master &amp;middot; samchon/bbs-backend&quot; data-og-description=&quot;Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot; data-og-url=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b3px4S/hyWCCuflve/B95AHXrbVC3zygwn3HDU0k/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_155_1061_248&quot;&gt;&lt;a href=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b3px4S/hyWCCuflve/B95AHXrbVC3zygwn3HDU0k/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_155_1061_248');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;bbs-backend/docs/ERD.md at master &amp;middot; samchon/bbs-backend&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2024-07-21_17-49-18.png&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;1454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sobYA/btsIHHXqYIE/3z3JImjrK2Lj3s6d6lifK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sobYA/btsIHHXqYIE/3z3JImjrK2Lj3s6d6lifK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sobYA/btsIHHXqYIE/3z3JImjrK2Lj3s6d6lifK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsobYA%2FbtsIHHXqYIE%2F3z3JImjrK2Lj3s6d6lifK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1704&quot; height=&quot;1454&quot; data-filename=&quot;2024-07-21_17-49-18.png&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;1454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 스냅샷 예제를 기반으로 prisma와 nestjs Repository는 어떻게 만들어야하는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Prisma 예시 코드&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1721551494385&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;model OnboardingGoal {
  id         String                   @id @default(uuid())
  user       User                     @relation(fields: [userId], references: [id])
  userId     String                   @unique @map(&quot;user_id&quot;)
  createdAt  DateTime                 @default(now()) @map(&quot;created_at&quot;) @db.Timestamptz(3)
  updatedAt  DateTime                 @updatedAt @map(&quot;updated_at&quot;) @db.Timestamptz(3)
  snapshots  OnboardingGoalSnapShot[] @relation(&quot;snapshots&quot;)
  snapshot   OnboardingGoalSnapShot?  @relation(fields: [snapshotId], references: [id])
  snapshotId String?

  @@map(&quot;onboarding_goal&quot;)
}

model OnboardingGoalSnapShot {
  id               String           @id @default(uuid())
  onBoardingGoal   OnboardingGoal   @relation(&quot;snapshots&quot;, fields: [onBoardingGoalId], references: [id])
  onBoardingGoalId String           @map(&quot;onboarding_goal_id&quot;)
  goalStartDate    DateTime         @map(&quot;goal_start_date&quot;) @db.Timestamptz(3)
  goalEndDate      DateTime         @map(&quot;goal_end_date&quot;) @db.Timestamptz(3)
  goalHours        Int              @map(&quot;goal_hours&quot;)
  goalSteps        Int              @map(&quot;goal_steps&quot;)
  createdAt        DateTime         @default(now()) @map(&quot;created_at&quot;) @db.Timestamptz(3)
  updatedAt        DateTime         @updatedAt @map(&quot;updated_at&quot;) @db.Timestamptz(3)
  OnboardingGoal   OnboardingGoal[]

  @@map(&quot;onboarding_goal_snapshot&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Repository 생성 예시 코드&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1721551475024&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const createGoal = async ({
    goalEndDate,
    goalSteps,
    goalStartDate,
    goalHours,
    userId,
    tx,
  }: {
    goalEndDate: Date;
    goalSteps: number;
    goalStartDate: Date;
    goalHours: number;
  } &amp;amp; GlobalTypes.PrismaTransactionParams &amp;amp;
    GlobalTypes.UserIdParam) =&amp;gt; {
    const goal = await getPrismaDB(tx).onboardingGoal.create({
      data: {
        userId,
        snapshots: {
          create: {
            goalEndDate,
            goalSteps,
            goalStartDate,
            goalHours,
          },
        },
      },
    });

    const snapshot = await getPrismaDB(tx).onboardingGoalSnapShot.findFirst({
      where: {
        onBoardingGoalId: goal.id,
      },
    });

    await getPrismaDB(tx).onboardingGoal.update({
      where: {
        id: goal.id,
      },
      data: {
        snapshotId: snapshot?.id,
      },
    });
    return goal;
  };&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스냅샷 방식 도입 후 경험&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅샷을 도입하면서 초기에는 원본 테이블과 스냅샷 테이블의 ID를 연결하는 과정에서 어려움을 겪었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 Prisma Schema에서 snapshotId를 nullable하게 설정하여 문제를 해결했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 비용이 DB 비용보다 저렴합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 UUID를 생성하여 주입하면 DB에서 UUID를 생성하는 비용을 줄이고 로직을 더 간단하게 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;수정된 Repository 예시 코드&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1721551457003&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { v4 as uuidv4 } from 'uuid';

.
.
.
&amp;lt;생략&amp;gt;

    const uuid = uuidv4();
    const goal = await getPrismaDB(tx).onboardingGoal.create({
      data: {
        id: uuid,
        userId,
        snapshots: {
          create: {
            goalEndDate,
            goalSteps,
            goalStartDate,
            goalHours,
          },
        },
      },
    });

    await getPrismaDB(tx).onboardingGoal.update({
      where: {
        id: goal.id,
      },
      data: {
        snapshotId: uuid,
      },
    });
    return goal;
  };​&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅샷 방식을 도입한 후 실시간 데이터 분석이나 상태 복원이 필요한 상황에서 큰 도움을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 모든 상황에 적합한 방식은 아니므로, 프로젝트의 특성과 요구사항을 잘 고려하여 선택하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글이 NestJS 개발을 하시는 분들께 조금이나마 도움이 되었으면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문이나 의견이 있으시면 댓글로 남겨주세요. 함께 고민하고 배우는 과정이 되었으면 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다!(좋아요는 큰 힘이 됩니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/samchon/bbs-backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/samchon/bbs-backend&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721551776155&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;bbs-backend/docs/ERD.md at master &amp;middot; samchon/bbs-backend&quot; data-og-description=&quot;Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot; data-og-url=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b3px4S/hyWCCuflve/B95AHXrbVC3zygwn3HDU0k/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_155_1061_248&quot;&gt;&lt;a href=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b3px4S/hyWCCuflve/B95AHXrbVC3zygwn3HDU0k/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_155_1061_248');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;bbs-backend/docs/ERD.md at master &amp;middot; samchon/bbs-backend&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1721551655699&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - samchon/bbs-backend: Simple Bullet-in Board System Backend&quot; data-og-description=&quot;Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/samchon/bbs-backend&quot; data-og-url=&quot;https://github.com/samchon/bbs-backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/chTHR8/hyWCEFCVEq/VV2mQxFYS0AfIAUqdTof0K/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_155_1061_248&quot;&gt;&lt;a href=&quot;https://github.com/samchon/bbs-backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/samchon/bbs-backend&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/chTHR8/hyWCEFCVEq/VV2mQxFYS0AfIAUqdTof0K/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_155_1061_248');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - samchon/bbs-backend: Simple Bullet-in Board System Backend&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>⭐ Featured</category>
      <category>디비스냅샷</category>
      <category>스냅샷</category>
      <category>스냅샷과 히스토리</category>
      <category>스냅샷과로그성</category>
      <category>스냅샷과히스토리성 차이</category>
      <category>스냅샷방식</category>
      <author>foodev</author>
      <guid isPermaLink="true">https://study-easy-coding.tistory.com/172</guid>
      <comments>https://study-easy-coding.tistory.com/172#entry172comment</comments>
      <pubDate>Sun, 21 Jul 2024 17:51:13 +0900</pubDate>
    </item>
  </channel>
</rss>