<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>이찬진 컴퓨터 교실</title>
    <link>https://devnm.tistory.com/</link>
    <description>경험을 기술합니다
https://github.com/ImNM</description>
    <language>ko</language>
    <pubDate>Sat, 9 May 2026 09:04:00 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ImNM</managingEditor>
    <image>
      <title>이찬진 컴퓨터 교실</title>
      <url>https://tistory1.daumcdn.net/tistory/5204540/attach/0c425778b95643e9b4f471ff2aa46b2d</url>
      <link>https://devnm.tistory.com</link>
    </image>
    <item>
      <title>[Nextjs]  Sharp Missing In Production ( memory leak 이슈 )</title>
      <link>https://devnm.tistory.com/38</link>
      <description>&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;는 한 ec2 안에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx를 프록시로 활용하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next js , redis , spring , react 어플리케이션을 도커로 띄우고 있다.&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;금일 오후 오후 1시 3분...&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nextjs 프론트 도커이미지가 다운되어버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출시이후 3개월만에 처음 있는 일이였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_6052.jpg&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsOtZ0/btsgDLAqjwI/fKhxsZlbwhZzI4K1c9fe71/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsOtZ0/btsgDLAqjwI/fKhxsZlbwhZzI4K1c9fe71/img.jpg&quot; data-alt=&quot;이때 철렁했다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsOtZ0/btsgDLAqjwI/fKhxsZlbwhZzI4K1c9fe71/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsOtZ0%2FbtsgDLAqjwI%2FfKhxsZlbwhZzI4K1c9fe71%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;663&quot; height=&quot;223&quot; data-filename=&quot;IMG_6052.jpg&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이때 철렁했다..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3개월째 잘 굴러가던 서비스가&lt;/b&gt;&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 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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;목차&lt;/u&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 에러 원인찾기 : 도커 event,syslog&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 이유가 뭘까 : 이미지 처리&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 해결하기 : sharp install&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; 3.1. 근데왜 squoosh 가 문제일까?&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 원인찾기 : 도커 event ,syslog&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두둥 서비스&lt;/b&gt;는 근검절약(돈이없따 ㅠ)을 위해 ec2 micro 에 swap memory 설정으로 꽉꽉 채워서 돌아가는 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리가 아예 넘치게되면 ec2 ssh 접속조차 안되는데 이번경우는 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;sudo docker ps&lt;/b&gt;&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;sudo docker logs -f &amp;lt;image-id&amp;gt; --tail &amp;lt;tail line&amp;gt;&lt;/b&gt;&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;sudo docker events --slice '2023-05-20'&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;a href=&quot;https://docs.docker.com/engine/reference/commandline/events/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.docker.com/engine/reference/commandline/events/&lt;/a&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;두둥프론트2.jpg&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HKCO1/btsgGjbHDY4/zWtcCu8r5Peu4zNCe6akik/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HKCO1/btsgGjbHDY4/zWtcCu8r5Peu4zNCe6akik/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HKCO1/btsgGjbHDY4/zWtcCu8r5Peu4zNCe6akik/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHKCO1%2FbtsgGjbHDY4%2FzWtcCu8r5Peu4zNCe6akik%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;971&quot; height=&quot;330&quot; data-filename=&quot;두둥프론트2.jpg&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oom, exitCode=129 라는 내용으로 왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;exitCode 의 경우 128번 까지는 도커관련한 코드고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 128 + n 형식으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n은 리눅스관련 코드이다.&lt;/p&gt;
&lt;figure id=&quot;og_1684664484429&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;What is the authoritative list of Docker Run exit codes?&quot; data-og-description=&quot;Apologies if this has been asked, but nowhere in the Docker documentation can I find an authoritative list of exit codes (also called exit status). Surprising! I see suggestions about making it&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/31297616/what-is-the-authoritative-list-of-docker-run-exit-codes&quot; data-og-url=&quot;https://stackoverflow.com/questions/31297616/what-is-the-authoritative-list-of-docker-run-exit-codes&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/byXTWD/hySG6sES4S/wZZIGkvp5pPaRZllOqys00/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/31297616/what-is-the-authoritative-list-of-docker-run-exit-codes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/31297616/what-is-the-authoritative-list-of-docker-run-exit-codes&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/byXTWD/hySG6sES4S/wZZIGkvp5pPaRZllOqys00/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;What is the authoritative list of Docker Run exit codes?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Apologies if this has been asked, but nowhere in the Docker documentation can I find an authoritative list of exit codes (also called exit status). Surprising! I see suggestions about making it&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;cat&amp;nbsp;/var/log/syslog&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어로 &lt;b&gt;syslog를 봐도 oom-killer&lt;/b&gt; 가 동작해서 다운되어있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2958&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AxuSL/btsgDLmNJGJ/QFTpXtNZKukhVknnRBfXxk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AxuSL/btsgDLmNJGJ/QFTpXtNZKukhVknnRBfXxk/img.jpg&quot; data-alt=&quot;스왑메모리 1기가 까지 차곡차곡 다 드셨다 ㅋㅌ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AxuSL/btsgDLmNJGJ/QFTpXtNZKukhVknnRBfXxk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAxuSL%2FbtsgDLmNJGJ%2FQFTpXtNZKukhVknnRBfXxk%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;2958&quot; height=&quot;622&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2958&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스왑메모리 1기가 까지 차곡차곡 다 드셨다 ㅋㅌ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이유가 뭘까 : 이미지처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 메모리 oom 이 났을 까.. 생각해보면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넥스트가 ssr이고, node 런타임을 기반으로 하고 있어 생긴 문제라고 생각했다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서가서 프로덕션 모드로 배포하는 부분을 살펴보았다.&lt;/p&gt;
&lt;figure id=&quot;og_1684666387249&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Building Your Application: Deploying | Next.js&quot; data-og-description=&quot;Using App Router Features available in /app&quot; data-og-host=&quot;nextjs.org&quot; data-og-source-url=&quot;https://nextjs.org/docs/app/building-your-application/deploying#self-hosting&quot; data-og-url=&quot;https://nextjs.org/docs/app/building-your-application/deploying#self-hosting&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d1vs2F/hySG6sGCnh/JEMBd6KJuwCoZ4XkXjA2p0/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/e86Oo/hySG1SweU9/kGm4lE8efHKdXV3qYSwsK1/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/deploying#self-hosting&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nextjs.org/docs/app/building-your-application/deploying#self-hosting&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d1vs2F/hySG6sGCnh/JEMBd6KJuwCoZ4XkXjA2p0/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441,https://scrap.kakaocdn.net/dn/e86Oo/hySG1SweU9/kGm4lE8efHKdXV3qYSwsK1/img.png?width=843&amp;amp;height=441&amp;amp;face=0_0_843_441');&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;Building Your Application: Deploying | Next.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Using App Router Features available in /app&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nextjs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_6050.JPG&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;1242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sbc49/btsgE7JMEEm/q2NSvkof16KRk6qSOsswZ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sbc49/btsgE7JMEEm/q2NSvkof16KRk6qSOsswZ0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sbc49/btsgE7JMEEm/q2NSvkof16KRk6qSOsswZ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsbc49%2FbtsgE7JMEEm%2Fq2NSvkof16KRk6qSOsswZ0%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;528&quot; height=&quot;495&quot; data-filename=&quot;IMG_6050.JPG&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;1242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;to prevent excessive memory usage... 두둥에서는 공연 상세화면에서 &lt;b&gt;이미지 사이즈의 최적화&lt;/b&gt;를 위해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;next/image&lt;/b&gt; 를 쓰고 있었고.,.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdD4cF/btsgEBRQLAR/Psdx6ldoQlMyeV8B9oGSF1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdD4cF/btsgEBRQLAR/Psdx6ldoQlMyeV8B9oGSF1/img.jpg&quot; data-alt=&quot;이번 참사의 원인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdD4cF/btsgEBRQLAR/Psdx6ldoQlMyeV8B9oGSF1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdD4cF%2FbtsgEBRQLAR%2FPsdx6ldoQlMyeV8B9oGSF1%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;707&quot; height=&quot;258&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이번 참사의 원인&lt;/figcaption&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;(프론트 배포때 이거 안하고 뭐했어..)  &lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결하기 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: sharp install&lt;/span&gt;&lt;/h2&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;figure id=&quot;og_1684666570940&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;sharp-missing-in-production | Next.js&quot; data-og-description=&quot;Sharp Missing In Production The next/image component's default loader uses squoosh because it is quick to install and suitable for a development environment. For a production environment using next start, it is strongly recommended you install sharp. You a&quot; data-og-host=&quot;nextjs.org&quot; data-og-source-url=&quot;https://nextjs.org/docs/messages/sharp-missing-in-production#why-this-error-occurred&quot; data-og-url=&quot;https://nextjs.org/docs/messages/sharp-missing-in-production&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/messages/sharp-missing-in-production#why-this-error-occurred&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nextjs.org/docs/messages/sharp-missing-in-production#why-this-error-occurred&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;sharp-missing-in-production | Next.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Sharp Missing In Production The next/image component's default loader uses squoosh because it is quick to install and suitable for a development environment. For a production environment using next start, it is strongly recommended you install sharp. You a&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nextjs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발환경에서는 가벼운 squoosh 를 사용하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션환경에서는 sharp 를 사용하는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 sharp를 install 하지 않은 채로 프로덕션으로 올려&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 배포 어플리케이션에서 squoosh를 사용하기 때문이다.&lt;/b&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;간단하게 yarn add sharp 로 처리했다.&lt;/b&gt;&lt;/p&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;근데 왜 squoosh가 문제일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1684670843421&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;Fix memory leak in image optimization by shuding &amp;middot; Pull Request #23565 &amp;middot; vercel/next.js&quot; data-og-description=&quot;This RP fixes the problem that the image optimization API uses a large amount of memory, and is not correctly freed afterwards. There're multiple causes of this problem: 1. Too many WebAssembly ins...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/vercel/next.js/pull/23565&quot; data-og-url=&quot;https://github.com/vercel/next.js/pull/23565&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/G008e/hySHb1Vifg/CzJNFINCfbCWHse7i32CwK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/vercel/next.js/pull/23565&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/vercel/next.js/pull/23565&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/G008e/hySHb1Vifg/CzJNFINCfbCWHse7i32CwK/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;Fix memory leak in image optimization by shuding &amp;middot; Pull Request #23565 &amp;middot; vercel/next.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This RP fixes the problem that the image optimization API uses a large amount of memory, and is not correctly freed afterwards. There're multiple causes of this problem: 1. Too many WebAssembly ins...&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_1684671491780&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;Wasm needs a better memory management story &amp;middot; Issue #1397 &amp;middot; WebAssembly/design&quot; data-og-description=&quot;Hi all, after a video call with google last week, I was encouraged to raise a conversation here around issues we at Unity have with Wasm memory allocation. The short summary is that currently Wasm ...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/WebAssembly/design/issues/1397&quot; data-og-url=&quot;https://github.com/WebAssembly/design/issues/1397&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/9p5U8/hySHhA9kHP/YK8btFhk03FdOIfBYoReC1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/WebAssembly/design/issues/1397&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/WebAssembly/design/issues/1397&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/9p5U8/hySHhA9kHP/YK8btFhk03FdOIfBYoReC1/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;Wasm needs a better memory management story &amp;middot; Issue #1397 &amp;middot; WebAssembly/design&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hi all, after a video call with google last week, I was encouraged to raise a conversation here around issues we at Unity have with Wasm memory allocation. The short summary is that currently Wasm ...&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;squoosh 는 &lt;b&gt;웹어셈블리 기반&lt;/b&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;문젠 node js 런타임이 서버에서 돌아간다는것이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node 위에서 웹 어셈블리를 통해 돌아가는 ( &lt;a href=&quot;https://nodejs.dev/en/learn/nodejs-with-webassembly/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://nodejs.dev/en/learn/nodejs-with-webassembly/&lt;/a&gt; )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;squoosh 의경우..점점 메모리만 늘어나는 꼴이라고 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOw7ML/btsgGjJBPY1/okvFBRwNK7mBajuYO5rBbK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOw7ML/btsgGjJBPY1/okvFBRwNK7mBajuYO5rBbK/img.jpg&quot; data-alt=&quot;https://github.com/vercel/next.js/issues/20915&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOw7ML/btsgGjJBPY1/okvFBRwNK7mBajuYO5rBbK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOw7ML%2FbtsgGjJBPY1%2FokvFBRwNK7mBajuYO5rBbK%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;750&quot; height=&quot;443&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1276&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://github.com/vercel/next.js/issues/20915&lt;/figcaption&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;물론 sharp 의경우에도 메모리관련해서 문제가있지만 동시성을 낮추면서 문제를 해결했다고 한다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 사실 두둥 세팅이면 그럼 이미지가 하나에 한번씩 sharp 되는 꼴이다 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능을 제대로 확보할라면 메모리 할당자를 jemalloc 같은걸로 바꾸라고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 도커이미지를 아래와 같이 하면된다.&lt;/p&gt;
&lt;pre id=&quot;code_1684673260210&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RUN apt-get update &amp;amp;&amp;amp; apt-get install --force-yes -yy \
  libjemalloc1 \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# Change memory allocator to avoid leaks
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;https://github.com/lovell/sharp/issues/955#issuecomment-475532037&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결국 사실 공식문서 제대로 안읽고, sharp 안깔고 배포해서 생긴 문제였지만..&lt;/b&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;이미지를 sharp하는걸 서버에서... 처리하는게 이상한 그림이긴하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 많이쓰는 작업이고, 백엔드도 서버에서 처리안하고 람다에서 sharp 사용해서 리사이징 하는 등..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 부분 고려하면 next/image 자체를 안쓰는것도 좋은 방안이라고 생각한다.&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;아무튼 기존에 이미지 리사이징에 sharp를 사용한 경험이 있어서 한번 다시 돌아볼 수 있는&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;</description>
      <category>리액트</category>
      <category>memory</category>
      <category>nextjs</category>
      <category>SHARP</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/38</guid>
      <comments>https://devnm.tistory.com/38#entry38comment</comments>
      <pubDate>Sun, 21 May 2023 21:48:49 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] spring redisson 분산락 Aop 적용기</title>
      <link>https://devnm.tistory.com/37</link>
      <description>&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-origin-width=&quot;731&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r6Iuj/btr2O1CeNen/yk7EZ9PLca5vp4dr1RwekK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r6Iuj/btr2O1CeNen/yk7EZ9PLca5vp4dr1RwekK/img.png&quot; data-alt=&quot;둥둥즈&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r6Iuj/btr2O1CeNen/yk7EZ9PLca5vp4dr1RwekK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr6Iuj%2Fbtr2O1CeNen%2Fyk7EZ9PLca5vp4dr1RwekK%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;521&quot; height=&quot;293&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;둥둥즈&lt;/figcaption&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;재고 감소나 , 주문 과정에서의 티켓 정보 확인등 동시성을 고려해야할 상황이 생겼다.&lt;/p&gt;
&lt;figure id=&quot;og_1678260371074&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;재고시스템으로 알아보는 동시성이슈 해결방법 - 인프런 | 강의&quot; data-og-description=&quot;동시성 이슈란 무엇인지 알아보고 처리하는 방법들을 학습합니다., - 강의 소개 | 인프런&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bfvbqz/hyRRDk6BP2/OkckZJEBiCKLNYDwMII0i0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/OBmaQ/hyRSLPmP6c/WpKFynuCXvp40FR3zYhal0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/d3cPWU/hyRSF9r4J6/aQkUKwZ90glksSO7YQjtN1/img.png?width=2510&amp;amp;height=506&amp;amp;face=0_0_2510_506&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bfvbqz/hyRRDk6BP2/OkckZJEBiCKLNYDwMII0i0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/OBmaQ/hyRSLPmP6c/WpKFynuCXvp40FR3zYhal0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/d3cPWU/hyRSF9r4J6/aQkUKwZ90glksSO7YQjtN1/img.png?width=2510&amp;amp;height=506&amp;amp;face=0_0_2510_506');&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;재고시스템으로 알아보는 동시성이슈 해결방법 - 인프런 | 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;동시성 이슈란 무엇인지 알아보고 처리하는 방법들을 학습합니다., - 강의 소개 | 인프런&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이중에서 Redisson 을 활용해서 분산락을 적용하고있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두둥에서는 RedissonLock 이라는 Aop를 만들어서&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;분산락을 Aop로 만드는 과정을 담은 블로그들은 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://devroach.tistory.com/82&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://devroach.tistory.com/82&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://devroach.tistory.com/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://devroach.tistory.com/83&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://devfunny.tistory.com/888&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://devfunny.tistory.com/888&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;Redisson 분산락 Aop 를 구현하는 방법이 아닌 실제 적용하면서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고민 되었던 포인트들을 공유하고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 두둥의 분산락 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. IllegalMonitorStateException&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 2.1. 왜 IllegalMonitorStateException 발생하나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 동시성을 테스팅을 디비를 안거쳐도 되지않을까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 동시성 실패 테스트를 만들고 싶다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 두둥에서 동시성 테스트를 하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 왜 트랜잭션 전파속성이 REQUIRES_NEW 여야하지..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 새로운 트랜잭션으로 인해서, 정보가 새로 안받아와져요. ( mapper 레이어 에서 처리 )&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 두둥의 분산락 구성&lt;/h2&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://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Domain/src/main/java/band/gosrock/domain/common/aop/redissonLock/RedissonLock.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RedissonLock.java&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Domain/src/main/java/band/gosrock/domain/common/aop/redissonLock/RedissonLockAop.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RedissonLockAop.java&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Domain/src/main/java/band/gosrock/domain/common/aop/redissonLock/RedissonCallNewTransaction.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RedissonCallNewTransaction.java&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// Aop 적용
@RedissonLock(LockName = &quot;주문&quot;, identifier = &quot;orderUuid&quot;)
public String execute(String orderUuid) {
    Order order = orderAdaptor.findByOrderUuid(orderUuid);
    order.approve(orderValidator);
    return orderUuid;
}&lt;/code&gt;&lt;/pre&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;pre&gt;&lt;code&gt;@RedissonLock(
        LockName = &quot;주문&quot;,
        identifier = &quot;orderId&quot;,
        paramClassType = ConfirmPaymentsRequest.class)
public String execute(ConfirmPaymentsRequest confirmPaymentsRequest, Long currentUserId) {&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 궁금하신 분들은 RedissonLockAop 에서 참고 하시면된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. IllegalMonitorStateException&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson lock 안으로 진입할때에 &lt;b&gt;leaseTime&lt;/b&gt; 을 설정할 수 있는데.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;boolean available = rLock.tryLock(waitTime, leaseTime, timeUnit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;leaseTime&lt;/b&gt;은 처리 작업이 해당 시간만큼 지나게 되면 &lt;b&gt;소스 구조상 IllegalMonitorStateException&lt;/b&gt; 을 발생 시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산락 안으로 들어간 상황에서 10초 ( 두둥에서는 leaseTime을 10초 디폴트로 잡았다 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 지난 상황에서 &lt;b&gt;IllegalMonitorStateException&lt;/b&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;문제는 leaseTime이 TransactionTimeOut보다 짧을 때 벌어진다.&lt;/b&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;leaseTime이 지나면 IllegalMonitorStateException 이 뜨는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 에러는 트랜잭션 안에서 발생하는 에러가 아니라 트랜잭션을 콜한 상위 콜스택에서 발생하는 에러다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;분산락 -&amp;gt; tryLock -&amp;gt; 락휙득 -&amp;gt; 트랜잭션 안으로 진입&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&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;무제.jpg&quot; data-origin-width=&quot;2346&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wJlro/btr2OR0FKAj/HMIy3T2AnR4lvGF1GIIg20/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wJlro/btr2OR0FKAj/HMIy3T2AnR4lvGF1GIIg20/img.jpg&quot; data-alt=&quot;에러가났는데.. 커밋이 된다. 당황스럽지않은가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wJlro/btr2OR0FKAj/HMIy3T2AnR4lvGF1GIIg20/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwJlro%2Fbtr2OR0FKAj%2FHMIy3T2AnR4lvGF1GIIg20%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;2346&quot; height=&quot;234&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2346&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;에러가났는데.. 커밋이 된다. 당황스럽지않은가&lt;/figcaption&gt;
&lt;/figure&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;그래서 IllegalMonitorStateException 에러에 대해 대응을 해줘야한다.&lt;/b&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;b&gt;leaseTime&lt;/b&gt; 을 넘어도 커밋이 되면안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기위해선 간단하게&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 9)
public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 타임아웃을 leaseTime 보다 적게 잡으면된다&lt;/b&gt;. 그럼 안에서 leaseTime이 넘기전에 rollback 되어버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 이러면 &lt;b&gt;TransactionTimeout&lt;/b&gt; 에러가 발생하므로 적절한 에러처리도 동반해야줘야한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// try rLock.tryLock
} catch (DuDoongCodeException | DuDoongDynamicException | TransactionTimedOutException e) {
    throw e;
} finally {
    try {
    // 비즈니스 에러 또는 , TransactionTimedOutException 발생시 즉시 반환해줘야한다.
        rLock.unlock();
    // IllegalMonitorStateException 은 rLock.unlock 을 실행시킬때 발생하는 에러다.
    } catch (IllegalMonitorStateException e) {
        //적절한 로그 처리
        // forceUnlock 하면 안된다. rLock.unlock에서 이미 락이 풀린상태다
    }
}&lt;/code&gt;&lt;/pre&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;2.1. 왜 IllegalMonitorStateException 이 발생하나요?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2408&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU655s/btr2OrH1WRs/3ZVsKPdVBHukODMxnBwEk0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU655s/btr2OrH1WRs/3ZVsKPdVBHukODMxnBwEk0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU655s/btr2OrH1WRs/3ZVsKPdVBHukODMxnBwEk0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU655s%2Fbtr2OrH1WRs%2F3ZVsKPdVBHukODMxnBwEk0%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;2408&quot; height=&quot;358&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2408&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그외 leaseTime에 제한이 되지않는 , 정상적으로 종료한 경우에 자신의 락을 본인이 해제를 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 finally 에 rLock.unlock() 메소드가 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WKVhg/btr2GsVgf1A/SPr5ZjwsJS2luuRNzl8U1K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WKVhg/btr2GsVgf1A/SPr5ZjwsJS2luuRNzl8U1K/img.jpg&quot; data-alt=&quot;https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WKVhg/btr2GsVgf1A/SPr5ZjwsJS2luuRNzl8U1K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWKVhg%2Fbtr2GsVgf1A%2FSPr5ZjwsJS2luuRNzl8U1K%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;1782&quot; height=&quot;110&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;leaseTime&lt;/b&gt; 이 지난 경우 자동으로 해제되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;finally 쪽에서는 unlock을 시도하므로( 정상상태일때 본인 락은 본인이 해제해야함 )&lt;/b&gt; , 다른 스레드가 기달리다가 들어와있는 상태에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;leaseTime으로 풀려난 스레드가 락을 unlock할려고 하면, &lt;b&gt;IllegalMonitorStateException&lt;/b&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;이미 자신의 락은 자동으로 풀린상태이기 때문에 error 로그 정도... 남기고 적당한처리를 해주시면 된다.&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;이제 leaseTime이 지나기전 TransactionTimeOut을 이용해서 커밋을 하기전 롤백처리를 적절하게 할 수 있게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;leaseTime 으로 인한 자동으로 락이 풀릴시에, 해당 스레드가 다른 스레드의 락을 풀려고 할 때 발생할 수 있는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IllegalMonitorStateException 에 대해 적당한 로그처리를 진행해 주면된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동시성을 테스팅을 디비를 안거쳐도 되지않을까&lt;/h2&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;/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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;b&gt; stub으로 order 객체를 만들어서 활용 할 수 있지 않을 까 라는 생각이 들었다.&lt;/b&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIynGx/btr2OQHzKfH/olemJ6j5HpEArYUZ93i7OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIynGx/btr2OQHzKfH/olemJ6j5HpEArYUZ93i7OK/img.png&quot; data-alt=&quot;https://www.geeksforgeeks.org/java-memory-management/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIynGx/btr2OQHzKfH/olemJ6j5HpEArYUZ93i7OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIynGx%2Fbtr2OQHzKfH%2FolemJ6j5HpEArYUZ93i7OK%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;583&quot; height=&quot;302&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.geeksforgeeks.org/java-memory-management/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;힙영역 , Method area&lt;/b&gt;는 여러 쓰레드가 동시에 접근이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙영역의&lt;b&gt; field에 stub 용 order 객체&lt;/b&gt;를 선언하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시 접근가능한 공유 자원이 생기는 샘이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@DomainIntegrateSpringBootTest
@DisableDomainEvent
@Slf4j
class OrderApproveServiceConcurrencyTest {
	// 레포지토리를 통해 디비에 갔다오지 않아도. 공유자원으로 
    // 테스팅 관련한 환경 구성이 가능하다.
    Order order;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@BeforeEach
void setUp() {
    given(orderLineItem.getTotalOrderLinePrice()).willReturn(Money.ZERO);
    order =
            Order.builder()
                    .orderMethod(OrderMethod.APPROVAL)
                    .orderStatus(OrderStatus.PENDING_APPROVE)
                    .orderLineItems(List.of(orderLineItem))
                    .build();
    order.addUUID();
    willDoNothing().given(orderValidator).validCanDone(any());
    willDoNothing().given(orderValidator).validUserNotDeleted(any());
    willCallRealMethod().given(orderValidator).validCanApproveOrder(any());
    willCallRealMethod().given(orderValidator).validStatusCanApprove(any());
    given(orderAdaptor.findByOrderUuid(any())).willReturn(order);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 적절하게 order 객체를 테스팅 관련한 환경으로 만들고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 레포지토리( 두둥에서는 orderAdaptor로 한번 레포지토리를 감쌌다)를 적절하게 @MockBean 을통해&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유되는 order 객체를 리턴하게 해주면된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 동시성 실패 테스트를 만들고 싶다면?&lt;/h2&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;분산락이 이미 어노테이션과 AOP로 적용되어있는 상태이기 때문이다.&lt;/b&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;&lt;b&gt;@RedissonRock&lt;/b&gt; 어노테이션을 떼어버려야 하는 환경을 구성해야하는데.&lt;/p&gt;
&lt;figure id=&quot;og_1678274602102&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[스프링] Spring disable Aop in test&quot; data-og-description=&quot;오랜만에... 글을 씁니다.! 디프만 12기 끝나고 ( 13기 운영진도 할 예정입니다. ㅎㅎ ) 고스락 티켓예매 세번째 프로젝트로 두둥이라는 프로젝트를 시작하게 되었다. 기존엔 고스락만을 위한 예매&quot; data-og-host=&quot;devnm.tistory.com&quot; data-og-source-url=&quot;https://devnm.tistory.com/24&quot; data-og-url=&quot;https://devnm.tistory.com/24&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cym5aK/hyRSFuY5bs/BlokC98gMzixP9COGVEOh0/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411,https://scrap.kakaocdn.net/dn/DweEB/hyRRQx8MeT/xPFu9KUKzgguhYqSwF73u1/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411,https://scrap.kakaocdn.net/dn/GtWDB/hyRRB8PuBW/6oE4kb0K07HUtiXKRmBxUK/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411&quot;&gt;&lt;a href=&quot;https://devnm.tistory.com/24&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devnm.tistory.com/24&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cym5aK/hyRSFuY5bs/BlokC98gMzixP9COGVEOh0/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411,https://scrap.kakaocdn.net/dn/DweEB/hyRRQx8MeT/xPFu9KUKzgguhYqSwF73u1/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411,https://scrap.kakaocdn.net/dn/GtWDB/hyRRB8PuBW/6oE4kb0K07HUtiXKRmBxUK/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411');&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;[스프링] Spring disable Aop in test&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오랜만에... 글을 씁니다.! 디프만 12기 끝나고 ( 13기 운영진도 할 예정입니다. ㅎㅎ ) 고스락 티켓예매 세번째 프로젝트로 두둥이라는 프로젝트를 시작하게 되었다. 기존엔 고스락만을 위한 예매&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devnm.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 포스팅을 참고하면된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@DomainIntegrateSpringBootTest
@DisableDomainEvent
// 분산락 꺼버리기
@DisableRedissonLock
@Slf4j
class OrderApproveServiceConcurrencyFailTest {&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 두둥에서 동시성 테스트를 하는 방법&lt;/h2&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;동시에 주문 승인 요청이 와도 하나의 요청만 성공해야한다.&quot;)
void 동시성_주문승인() throws InterruptedException {
    // 성공 횟수를 측정하기 위한 atomicLong
    AtomicLong successCount = new AtomicLong();
    CunCurrencyExecutorService.execute(
            () -&amp;gt; orderApproveService.execute(order.getUuid()), successCount);
    // then
    assertThat(successCount.get()).isEqualTo(1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
public class CunCurrencyExecutorService {
    static int numberOfThreads = 10;
    static int numberOfThreadPool = 5;

    public static void execute(Executable executable, AtomicLong successCount)
            throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreadPool);
        CountDownLatch latch = new CountDownLatch(numberOfThreads);

        for (long i = 1; i &amp;lt;= numberOfThreads; i++) {
            executorService.submit(
                    () -&amp;gt; {
                        try {
                            executable.execute();
                            // 오류없이 성공을 하면 성공횟수를 증가시킵니다.
                            successCount.getAndIncrement();
                        } catch (Throwable e) {
                            // 에러뜨면 여기서 확인해보셔요!
                            log.info(e.getClass().getName());
                        } finally {
                            latch.countDown();
                        }
                    });
        }
        latch.await();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하다 &lt;b&gt;Exexcutable&lt;/b&gt; ( 두둥에서는 jupiter.api.funcution 꺼를 씀 ) 형태의 &lt;b&gt;함수형 인터페이스&lt;/b&gt;로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행가능한 람다를 넘겨 받으면 된다. 자바기본 인터페이스인 Consumer 도 괜찮을 것 같다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 왜 트랜잭션 전파속성이 REQUIRES_NEW 여야하지..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 9)
public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}&lt;/code&gt;&lt;/pre&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;REQUIRES_NEW&lt;/b&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;mysql 은 기본 격리수준이 REPEATABLE READ 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 트랜잭션이 시작할 때 id 기준으로 자신보다 더 일찍 커밋된 정보만 최신으로 가져오기 때문에,&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;재고가 100 이있다 가정한다면&lt;/p&gt;
&lt;pre id=&quot;code_1678275093046&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;B 트랜잭션 시작(재고 100) -&amp;gt; A 트랜잭션 시작(재고 100) -&amp;gt; A 분산락 진입 후 재고 1 감소 (재고99) -&amp;gt; 
A가 분산락 벗어났지만 트랜잭션 전파로인해서 아직 커밋이 안됨 -&amp;gt;
B 분산락 진입 ( 재고 100 ) 진입후 재고 1 감소 ( 재고 99) -&amp;gt; 최종 99로 기록됨.&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;&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;따라서 항상 &lt;b&gt;REQUIRES_NEW&lt;/b&gt;로 트랜잭션 전파 속성을 둬야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 새로운 트랜잭션으로 인해서, 정보가 새로 안받아와져요. ( mapper 레이어 에서 처리 )&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 6번의 조건으로 인해 &lt;b&gt;트랜잭션 전파 속성&lt;/b&gt;으로 인해서 항상 &lt;b&gt;새로운 트랜잭션을 분산락 안에서 실행&lt;/b&gt;시킨다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// ConfirmOrderUseCase 의 잘못된 예
@Transactional(readOnly = true)
public OrderResponse execute(String orderUuid, ConfirmOrderRequest confirmOrderRequest) {
    Long currentUserId = SecurityUtils.getCurrentUserId();
    ConfirmPaymentsRequest confirmPaymentsRequest =
            ConfirmPaymentsRequest.builder()
                    .paymentKey(confirmOrderRequest.getPaymentKey())
                    .amount(confirmOrderRequest.getAmount())
                    .orderId(orderUuid)
                    .build();
                    // orderConfirmService 는 분산락으로 동작한다.
    String confirmOrderUuid =
            orderConfirmService.execute(confirmPaymentsRequest, currentUserId);
    return orderMapper.toOrderResponse(confirmOrderUuid);
}&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;두둥에서는 멀티 모듈 구조로 usecase와 도메인 서비스를 분리 해놨고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모듈 ( orderConfrimService ) 에서만 분산락을 적용하고 있다.&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;만일 위와같이 usecase ( facade 형태로 여러 도메인의 응답값을 받아 클라이언트한테 응답을 하는 레이어 ) 를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transaction으로 감싼다고 가정하면,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;usecase 의 execute를 실행할 때에 트랜잭션이 먼저 시작되고,&lt;/b&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;&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;b&gt;트랜잭션 고립 수준에 의하여 , usecase execute의 트랜잭션이 먼저 시작되었으므로,&lt;/b&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;&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// ConfirmOrderUseCase
public OrderResponse execute(String orderUuid, ConfirmOrderRequest confirmOrderRequest) {
    Long currentUserId = SecurityUtils.getCurrentUserId();
    ConfirmPaymentsRequest confirmPaymentsRequest =
            ConfirmPaymentsRequest.builder()
                    .paymentKey(confirmOrderRequest.getPaymentKey())
                    .amount(confirmOrderRequest.getAmount())
                    .orderId(orderUuid)
                    .build();
    String confirmOrderUuid =
            orderConfirmService.execute(confirmPaymentsRequest, currentUserId);
    return orderMapper.toOrderResponse(confirmOrderUuid);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// OrderMapper
@Transactional(readOnly = true)
public OrderResponse toOrderResponse(String orderUuid) {
    Order order = orderAdaptor.findByOrderUuid(orderUuid);

    Event event = getEvent(order);

    List&amp;lt;OrderLineTicketResponse&amp;gt; orderLineTicketResponses = getOrderLineTicketResponses(order);

    return OrderResponse.of(order, event, orderLineTicketResponses);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;위와 같이 mapper 레이어를 두어서 클라이언트한테 응답을 전달 할 때는&lt;/b&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;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;김영한 선생님께서 말씀하시는 커맨드성 함수는 엔티티를 리턴시키지말고 id 와 같은 정보만 넘겨줘서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 조회하게 끔 만들어야 한다는 경우가 이런 경우이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;figure id=&quot;og_1678275546458&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ciwfdE/hyRRELfdom/qwkiWIIPc2Aq8xHzI1Wdo1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ciwfdE/hyRRELfdom/qwkiWIIPc2Aq8xHzI1Wdo1/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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;위 해당하는 소스들은 두둥 프로젝트에서 진행하며 작성한 코드들이다.&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;/p&gt;</description>
      <category>스프링</category>
      <category>Redisson</category>
      <category>분산락</category>
      <category>스프링</category>
      <category>테스트</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/37</guid>
      <comments>https://devnm.tistory.com/37#entry37comment</comments>
      <pubDate>Wed, 8 Mar 2023 20:47:56 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] 멀티모듈 jacoco , sonarqube (cloud) 세팅</title>
      <link>https://devnm.tistory.com/36</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbAGAe/btr2ugabMJX/S3NkxfIJ7RXpBCcenGzRPk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbAGAe/btr2ugabMJX/S3NkxfIJ7RXpBCcenGzRPk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbAGAe/btr2ugabMJX/S3NkxfIJ7RXpBCcenGzRPk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbAGAe%2Fbtr2ugabMJX%2FS3NkxfIJ7RXpBCcenGzRPk%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;542&quot; height=&quot;359&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;596&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;두둥 프로젝트를 진행하면서 sonarcloud 를 활용해서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pr 간의 테스트 코드 측정 , 머지후 dev브런치의 테스트 커버리지 측정등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ci 단에서 테스트 코드를 돌렸다.&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;아직 전체 테스트커버리지가 20퍼 대 이긴하지만, 중요한 도메인 로직들은&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;잘못 변경한 부분이나, 빌드 페일등을 방지하면서 안정성 있게 운영했었다.&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;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 전체적인 구성방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. jacoco 세팅하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. sonarqube 세팅하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3.1. 소나 클라우드 관련&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3.2. 그래들 관련&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3.3. 깃헙 액션&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 전체적인 구성방식&lt;/h2&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;무제.jpg&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wKRIF/btr2H6XnYzD/liN4vm9AER032wxyWknMC0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wKRIF/btr2H6XnYzD/liN4vm9AER032wxyWknMC0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wKRIF/btr2H6XnYzD/liN4vm9AER032wxyWknMC0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwKRIF%2Fbtr2H6XnYzD%2FliN4vm9AER032wxyWknMC0%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;523&quot; height=&quot;320&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 돌리면 jacocoTestReports 그래들 태스크를 실행시키면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.reports 에 테스트 커버리지 관련 내용들이 나오고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;xml을 sonarcloud 에 전송해서 테스트 커버리지를 측정한다.&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;pre class=&quot;yaml&quot;&gt;&lt;code&gt;- name: test and analyze
  env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
  run: ./gradlew test sonar --info --stacktrace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github ci단에서는 dev 브런치에 머지 리퀘스트가 날라올때 , dev에 푸시가 될때 두번 날려서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디폴트 브런치인 dev 브런치의 테스트 커버리지 측정과 피알 단위의 테스트 커버리지 측정 ,&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. jacoco 세팅하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 커버리지 측정을 위해선 jacoco를 세팅해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 바깥 모듈의 gradle에서&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;subprojects {
	// 다른 부분은 생략
    apply plugin: 'jacoco'
    jacoco {
        toolVersion = '0.8.8'
    }
    jacocoTestReport {
        dependsOn test
        reports {
            html.enabled true // html 설정
            csv.enabled true // csv 설정
            xml.enabled true
            //xml 의 위치 조정
            xml.destination file(&quot;${buildDir}/reports/jacoco.xml&quot;)
        }
        def Qdomains = []
        for (qPattern in '**/QA'..'**/QZ') { // qPattern = '**/QA', '**/QB', ... '*.QZ'
            Qdomains.add(qPattern + '*')
        }
        afterEvaluate {
            classDirectories.setFrom(
                    files(classDirectories.files.collect {
                        fileTree(dir: it, excludes: [
                        		// 측정 안하고 싶은 패턴
                                &quot;**/*Application*&quot;,
                                &quot;**/*Config*&quot;,
                                &quot;**/*Dto*&quot;,
                                &quot;**/*Request*&quot;,
                                &quot;**/*Response*&quot;,
                                &quot;**/*Interceptor*&quot;,
                                &quot;**/*Exception*&quot;
                                // Querydsl 관련 제거
                        ] + Qdomains)
                    })
            )
        }
    }
    test {
        useJUnitPlatform()
        finalizedBy jacocoTestReport
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 jacoco 관련 세팅을 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빼고싶은 부분은 제거하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test 그래들 task를 실행시키면 jacocoTestReport 도 실행된다.&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;subproject 안에 작성된거여서 각 서브 모듈마다 적용이된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같은 설정을하게되면 test task 를 돌릴시에 테스크 커버리지 측정이된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. sonarqube 설정하기&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;3.1. 소나 클라우드 관련&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소나큐브를 직접 띄워도 되지만, 두둥 프로젝트에서는 sonarcloud를 사용했다.&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;그리고 항상 ci로 분석을 진행한다고 선택한다. 즉 자동 분석은 꺼준다.&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;무제.jpg&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5FHBJ/btr2FglTNQ4/KKLAKEUI9frLMBV0GKCkP1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5FHBJ/btr2FglTNQ4/KKLAKEUI9frLMBV0GKCkP1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5FHBJ/btr2FglTNQ4/KKLAKEUI9frLMBV0GKCkP1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5FHBJ%2Fbtr2FglTNQ4%2FKKLAKEUI9frLMBV0GKCkP1%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;618&quot; height=&quot;312&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;874&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;Quality Gate만.. 최소 테스트 커버리지 그런것만 팀원들이랑 상의해서 정하면된다.&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;무제.jpg&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eZROaZ/btr2FcRkRP9/FBMbfxcz9YvriVUg23SWe0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eZROaZ/btr2FcRkRP9/FBMbfxcz9YvriVUg23SWe0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eZROaZ/btr2FcRkRP9/FBMbfxcz9YvriVUg23SWe0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeZROaZ%2Fbtr2FcRkRP9%2FFBMbfxcz9YvriVUg23SWe0%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;484&quot; height=&quot;245&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;874&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxqdnm/btr2C9gK8bO/Q3RZ7Ej5pUGNLkBKWcjFMK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxqdnm/btr2C9gK8bO/Q3RZ7Ej5pUGNLkBKWcjFMK/img.jpg&quot; data-alt=&quot;env 세팅&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxqdnm/btr2C9gK8bO/Q3RZ7Ej5pUGNLkBKWcjFMK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdxqdnm%2Fbtr2C9gK8bO%2FQ3RZ7Ej5pUGNLkBKWcjFMK%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;585&quot; height=&quot;81&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;env 세팅&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙 액션의 환경변수를 위와 같이 설정하면 된다.&lt;/p&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;3.2. 그래들 관련&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p0kvE/btr2zmU4lNc/TJruBYBqlUAdf7QXuKp1x1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p0kvE/btr2zmU4lNc/TJruBYBqlUAdf7QXuKp1x1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p0kvE/btr2zmU4lNc/TJruBYBqlUAdf7QXuKp1x1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp0kvE%2Fbtr2zmU4lNc%2FTJruBYBqlUAdf7QXuKp1x1%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;643&quot; height=&quot;322&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 env 세팅에서 밑에 Gradle 버튼을 누르면 projectKey 랑 organization을 볼수있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복사해서 밑에 그래들을 공통 부분에 작성한다. ( 공통부분이라 함은 subproject 바깥을의미 )&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 공통으로 들어갈 부분
sonarqube {
    properties {
        property 'sonar.projectKey', 'Gosrock_DuDoong-Backend' // 본인 꺼 집어넣으세용
        property 'sonar.organization', 'dudung-gosrock' // 이것두
        property 'sonar.host.url', 'https://sonarcloud.io'
        property 'sonar.sources', 'src'
        property 'sonar.language', 'java'
        property 'sonar.sourceEncoding', 'UTF-8'
        property 'sonar.test.inclusions', '**/*Test.java'
        // 테스트 커버리지에서 빼고싶은거 넣어야함
        property 'sonar.exclusions', '**/test/**, **/Q*.java, **/*Doc*.java, **/resources/** ,**/*Application*.java , **/*Config*.java,' +
                '**/*Dto*.java, **/*Request*.java, **/*Response*.java ,**/*Exception*.java ,**/*ErrorCode*.java'
        property 'sonar.java.coveragePlugin', 'jacoco'
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;subprojects {
    apply plugin: 'org.sonarqube'

    sonarqube {
        properties {
        	// 각 프로젝트마다 적용해야하는부분.
            property 'sonar.java.binaries', &quot;${buildDir}/classes&quot;
            property 'sonar.coverage.jacoco.xmlReportPaths', &quot;${buildDir}/reports/jacoco.xml&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통으로 들어갈 부분과 각 모듈마다 정해야할 부분을 잘 분리해서 작성하면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 binaries 와 xmlReportPaths 는 무조건 따로 지정해야 잘된다.&lt;/p&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;3.3. 깃헙 액션&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 풀리퀘 날려올때마다 측정
name: ci
on:
  pull_request:
    branch: 'dev'

jobs:
  spotlessJavaCheck:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: SetUp JDK 17
        uses: actions/setup-java@v2
        with:
          java-version: &quot;17&quot;
          distribution: 'adopt'

      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew

      - name: spotless check
        run: ./gradlew spotlessCheck

      - name: test and analyze
        env:
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
            SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        run: ./gradlew test sonar --info --stacktrace&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 데브 브런치 기준으로 측정
name: ci
on:
  push:
    branches:
      - dev&lt;/code&gt;&lt;/pre&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;위처럼 ci 파일을 두개 만들게되면, 데브브런치 기준, 피알 기준으로 테스트 커버리지 측정이 가능하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;figure id=&quot;og_1678214941484&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vO0us/hyRRJZqmkK/mH1gv2k5wTngeCHKoYJLZk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vO0us/hyRRJZqmkK/mH1gv2k5wTngeCHKoYJLZk/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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;소스 링크 걸어두니 참고하시길 바란다.!&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;ci 단이라도 빌드가 되는지 안되는지 테스트 돌려보시기라도 하면 좋다.&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;spotless 관련 설정도 있으시 참고해보면 좋겠다.&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://github.com/Gosrock/DuDoong-Backend/blob/dev/.github/workflows/dev-merged-test-coverage.yml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;데브브런치 ci&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/.github/workflows/ci.yml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;풀리퀘 ci&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/build.gradle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gradle&lt;/a&gt;&lt;/p&gt;</description>
      <category>스프링</category>
      <category>멀티모듈</category>
      <category>스프링</category>
      <category>테스트</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/36</guid>
      <comments>https://devnm.tistory.com/36#entry36comment</comments>
      <pubDate>Wed, 8 Mar 2023 03:50:36 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] spring oauth Open ID Connect with kakao</title>
      <link>https://devnm.tistory.com/35</link>
      <description>&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;edited_IMG_5551.JPG&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;665&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb8b0d/btr2ufvzQwJ/JX4ruGqKfcRkWN3GclSylk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb8b0d/btr2ufvzQwJ/JX4ruGqKfcRkWN3GclSylk/img.png&quot; data-alt=&quot;두둥서비스의 회원가입전 약관 동의화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb8b0d/btr2ufvzQwJ/JX4ruGqKfcRkWN3GclSylk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb8b0d%2Fbtr2ufvzQwJ%2FJX4ruGqKfcRkWN3GclSylk%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;398&quot; height=&quot;398&quot; data-filename=&quot;edited_IMG_5551.JPG&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;665&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;두둥서비스의 회원가입전 약관 동의화면&lt;/figcaption&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;두둥 프로젝트와 , 디프만 낙낙 프로젝트를 진행하면서 ,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입하는 과정이 oauth 서버에서 인증뿐만아니라,&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;oauth 스펙중 하나인 &lt;b&gt;OIDC&lt;/b&gt; 를 사용해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이글을통해서 &lt;b&gt;직접 구현하는 방법&lt;/b&gt;을 공유하고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&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; 1.1. Oauth AccessToken을 이용하는 여러가지 해결방안&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 1.2. Oauth AccessToken으로 회원가입 할 때의 문제점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Open ID Connect&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 적용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3.1. 공개키 목록 조회하기 , feign으로 캐싱하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3.2. ID 토큰 유효성 검증하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 3.2.1 서명 검증전 페이로드 검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 3.2.2 서명 검증&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oauth 인증 과정을 수행하게 되면, 보통 code 요청을 받게되면&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;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XjuBR/btr2wtmo1Hd/oZejkIevHY8kgsVlIxZNzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XjuBR/btr2wtmo1Hd/oZejkIevHY8kgsVlIxZNzK/img.jpg&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;1440&quot; data-is-animation=&quot;false&quot; data-filename=&quot;IMG_5551.JPG&quot; style=&quot;width: 33.1226%; margin-right: 10px;&quot; data-widthpercent=&quot;33.91&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XjuBR/btr2wtmo1Hd/oZejkIevHY8kgsVlIxZNzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXjuBR%2Fbtr2wtmo1Hd%2FoZejkIevHY8kgsVlIxZNzK%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;665&quot; height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3DCBS/btr2GKmx6wc/74fn9J1fx7k194pU7r6VH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3DCBS/btr2GKmx6wc/74fn9J1fx7k194pU7r6VH1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;800&quot; data-filename=&quot;회원가입.png&quot; style=&quot;width: 32.2759%; margin-right: 10px;&quot; data-widthpercent=&quot;33.04&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3DCBS/btr2GKmx6wc/74fn9J1fx7k194pU7r6VH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3DCBS%2Fbtr2GKmx6wc%2F74fn9J1fx7k194pU7r6VH1%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;360&quot; height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dr0XDq/btr2D7Joaw7/S9xGIBL467DAZvyb4RiiCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dr0XDq/btr2D7Joaw7/S9xGIBL467DAZvyb4RiiCK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1600&quot; data-filename=&quot;논의시안- 수정하기 버튼.png&quot; data-widthpercent=&quot;33.05&quot; style=&quot;width: 32.2759%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dr0XDq/btr2D7Joaw7/S9xGIBL467DAZvyb4RiiCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdr0XDq%2Fbtr2D7Joaw7%2FS9xGIBL467DAZvyb4RiiCK%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;720&quot; height=&quot;1600&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;두둥의 약관 동의하고 시작하기 / 카카오톡 시작하기 이후 닉네임을 정해야함&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 보통의 서비스에선 사실 oauth 인증후에 바로 회원가입으로 끝나는 어플은 보기 힘들다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진처럼 oauth로 카카오에서 정보를 가져오더라도 , 약관동의나 마케팅 수신여부 .&amp;nbsp; 닉네임 프로필 사진등을 고르거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부족한 정보(주소지같은 부가정보)들을 oauth 인증 이후에 따로 채우고 있다.&lt;/p&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;1.1. Oauth AccessToken을 이용하는 여러가지 해결방안&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;oauth에 인증이 되어있다는 상태를 알려면&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서는&lt;/p&gt;
&lt;pre id=&quot;code_1678200012935&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. oauth 정보를 따로 저장하기
2. 실제 유저의 동의를 받고 회원가입을 한것은 아니지만, 유저관련데이타를 미리 생성하기
3. Oauth의 accessToken 으로 유저동의받고 회원가입 하기 ( 백엔드에서 Oauth accessToken 검증 )
4. registerToken jwt 를 따로만들어서 회원가입할 수 있는 토큰으로 oauth 인증 했다는 토큰 발급하기&lt;/code&gt;&lt;/pre&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;1,2,3,4 번의 방법 모두다 결국 어떤형태로든 클라이언트한테 인증이 되었다는 상태를 &lt;b&gt;검증&lt;/b&gt;할수 있는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿠키든 토큰이든 클라이언트가 들고있다가 회원가입때 같이 보내줘야만 한다.&lt;/b&gt; ( 회원가입용 api를 따로 만들어야하니깐 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;1.2. Oauth AccessToken으로 회원가입 할 때의 문제점&lt;/h3&gt;
&lt;pre id=&quot;code_1678200060791&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;3. Oauth의 accessToken 으로 유저동의받고 회원가입 하기 ( 백엔드에서 Oauth accessToken 검증 )&lt;/code&gt;&lt;/pre&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;Oauth 인증을 통해서 나오는 &lt;b&gt;accessToken,refreshToken&lt;/b&gt; 은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oauth 리소스 서버 ( 카카오톡의 사진, 친구목록, 메시지 보내기 ) 등 에 쓰이는 토큰이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oauth 인증 과정을 거쳐서 회원가입, 로그인을 한뒤에 자신의 서버에서 인증용 토큰을 발급해줘야한다.&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;즉 Oauth 인증과정을 수행후나오는 &lt;b&gt;accessToken은 Oauth한 서버의 리소스&lt;/b&gt;에 접근하는 용도로 쓰이는 것이다.&lt;/p&gt;
&lt;figure id=&quot;og_1678191766090&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/edXe5F/hyRRBtpOch/iZkZYxWBu64v3rED7kjjn1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/wuqgs/hyRRBmG7fg/uNZzjMlkb2V1NpHQtAnKN0/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/JlNl4/hyRRLbBWK3/u7wAvOL3Gv6muxJDTo5ACk/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/edXe5F/hyRRBtpOch/iZkZYxWBu64v3rED7kjjn1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/wuqgs/hyRRBmG7fg/uNZzjMlkb2V1NpHQtAnKN0/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/JlNl4/hyRRLbBWK3/u7wAvOL3Gv6muxJDTo5ACk/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000');&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;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1678192153921&quot; class=&quot;groovy&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET/POST /v2/user/me HTTP/1.1
Host: kapi.kakao.com
Authorization: Bearer ${ACCESS_TOKEN}/KakaoAK ${APP_ADMIN_KEY}
Content-type: application/x-www-form-urlencoded;charset=utf-8&lt;/code&gt;&lt;/pre&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한점은 &lt;b&gt;응답값에 Oauth AccessToken이 두둥에서 발급되었다는 사실을 확인을 할 수 없다는점이다.&lt;/b&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;그럼 발급된 AccessToken으로 사용자의 프로필 정보를 확인한 뒤에 회원가입을 시키는것은.&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만약 테스트 어플리케이션으로 카카오톡 디밸로퍼스에 &lt;b&gt;두둥2&lt;/b&gt; 라고 만든뒤.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가입을 한뒤에 해당 &lt;b&gt;AccessToken&lt;/b&gt;을 기존 &lt;b&gt;두둥 서버&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;figure id=&quot;og_1678192089423&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#get-token-info&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ceLbod/hyRRRbWbnh/6EUuA35Kf6zuQqyUrYdft0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#get-token-info&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#get-token-info&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ceLbod/hyRRRbWbnh/6EUuA35Kf6zuQqyUrYdft0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400');&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;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1678192151299&quot; class=&quot;routeros&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -v -X GET &quot;https://kapi.kakao.com/v1/user/access_token_info&quot; \
  -H &quot;Authorization: Bearer ${ACCESS_TOKEN}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 따로 &lt;b&gt;토큰 정보보기&lt;/b&gt;를 요청해서&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678192175783&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 200 OK
{
    &quot;id&quot;:123456789,
    &quot;expires_in&quot;: 7199,
    &quot;app_id&quot;:1234
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;응답값으로 넘어온 app_id가 우리 두둥의 app_id 인지 이차로 확인하는 과정이 필요하다.&lt;/b&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;다른 구글 같은 oauth&lt;/b&gt;에도 적용할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 정보보기같은 api는 카카오가 친절하게 만들어준것이지 &lt;b&gt;Oauth의 스펙&lt;/b&gt;이 아니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1699&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cONscm/btr2D29bDTf/LKgZaphnrAYxxfdBBQEcs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cONscm/btr2D29bDTf/LKgZaphnrAYxxfdBBQEcs0/img.png&quot; data-alt=&quot;출처 카카오 로그인 과정 (https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#before-you-begin-process)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cONscm/btr2D29bDTf/LKgZaphnrAYxxfdBBQEcs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcONscm%2Fbtr2D29bDTf%2FLKgZaphnrAYxxfdBBQEcs0%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;556&quot; height=&quot;738&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1699&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 카카오 로그인 과정 (https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#before-you-begin-process)&lt;/figcaption&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;&lt;b&gt;사실 Code 까지 받는건 클라이언트에서 진행해도된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어차피 Code는 사용자 브라우저에 url에 뜬다.&lt;/b&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;그러나 Step 2. 의 토큰 받기는 서버까지만의 그림으로 그려져있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AccessToken 을받아서 클라이언트한테 넘겨주고,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 AccessToken 을 회원가입때 프로필 정보확인하며 회원가입시키는건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누구나 &lt;b&gt;회원가입 Api로 다른 서비스에서 발급받은 AccessToken으로 회원가입 할 수 있다는점이다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;즉 Oauth AccessToken은 인가(리소스 서버에 접근을 하기위한) 토큰일 뿐이다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;1078&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8vKzm/btr2D8O62fA/ryLtrfs5NAkEg91mND4PH0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8vKzm/btr2D8O62fA/ryLtrfs5NAkEg91mND4PH0/img.jpg&quot; data-alt=&quot;꼭 한번다시 읽어주길바랍니당..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8vKzm/btr2D8O62fA/ryLtrfs5NAkEg91mND4PH0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8vKzm%2Fbtr2D8O62fA%2FryLtrfs5NAkEg91mND4PH0%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;640&quot; height=&quot;376&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;1078&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;꼭 한번다시 읽어주길바랍니당..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Open ID Connect ( OIDC )&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 사진의 Step 3. 사용자 로그인 처리의 OpenID Connect 사용시 ID 토큰 유효성 검증이라고 적혀져 있다.&lt;/blockquote&gt;
&lt;figure id=&quot;og_1678192942330&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/common#oidc&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3R78O/hyRRGammiE/UE44lR4ynC6mMpkROh9Crk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/cZeZiV/hyRRQD5Ytd/qHkaeKkOFIt6gkdh5yuSdK/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/be9ClS/hyRREjh0jk/FBn6Yc9haeiX3kNYFHJ3e1/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/common#oidc&quot; data-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/common#oidc&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3R78O/hyRRGammiE/UE44lR4ynC6mMpkROh9Crk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/cZeZiV/hyRRQD5Ytd/qHkaeKkOFIt6gkdh5yuSdK/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/be9ClS/hyRREjh0jk/FBn6Yc9haeiX3kNYFHJ3e1/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000');&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;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;OpenID Connect(OIDC)는 사용자가 안전하게 로그인하는 데 사용할 수 있는 &lt;br /&gt;OAuth 2.0 기반의 표준 인증 프로토콜입니다.&lt;br /&gt;&lt;/span&gt;&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;중요한점은 &lt;b&gt;인증 프로토콜&lt;/b&gt;이다. 표준이라서 &lt;b&gt;구글에서등 OIDC를 지원하는 곳이라면 다가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OIDC 의 토큰은 jwt 형태를 따른다. (AccessToken,RefreshToken 은 jwt 형식이 아니여도된다)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678193143554&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
	// 이 jwt 토큰을 받은사람
    &quot;iss&quot;: &quot;https://kauth.kakao.com&quot;,
    // 내앱키 , 두둥이면 230322(예시임) 이런식
    &quot;aud&quot;: &quot;${APP_KEY}&quot;,
    // 실제 유저의 고유 아이디 ( 카카오 유저의 고유 번호 )
    &quot;sub&quot;: &quot;166959&quot;,
    &quot;iat&quot;: 1647183250,
    &quot;exp&quot;: 1647190450,
    &quot;nonce&quot;: &quot;${NONCE}&quot;,
    &quot;auth_time&quot;: 1647183250
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용의 정보를 보면 위와같은 정보를 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발급한 곳의 정보와, 내 앱키의 정보를 jwt 토큰에서 얻을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1678200258591&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;내 앱키의 정보를 jwt 토큰에서 얻을 수 있으므로, 
검증된 토큰이라면 내 두둥서버에서 등록한 어플리케이션으로
발급한 id_token이라는점을 알 수 있다.&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;&lt;b&gt;세션 유지가 가능하다면 회원가입시에 추가 정보가 필요할경우,&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IdToken(OIDC)을 활용해서 oauth에 인증된 사용자임을 검증 할 수 있다는 점이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&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;&lt;b&gt;jwt 에서 정보를 얻을 수 있다는 말은,&lt;/b&gt; header, payload 부분에 있다는 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 누구에게나 공개된 정보다. jwt.io 에가서 발급된 idToken을 넣어봐도 나오는 정보다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;jwt 인증을 할때 내 시크릿키로 암호화 복호화하듯이&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 서버에서도 키를 준다. 근데 하나의 키가아니라 &lt;b&gt;RSA 암호화 방식으로 공개키&lt;/b&gt;를 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 jwt 암호화 할땐 비밀키로 암호화를 하고, 그정보를 인증되었는지 확인할땐 공개키로 복호화를 하는것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678200433883&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 메타데이터 확인
2. 카카오 로그인 구현
3. ID 토큰 유효성 검증하기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;바로 위링크에서는 구현 방법으로 위와같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;나뉘어져있고,&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;2. 카카오 로그인 구현은 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설정에서 Open Id connect 를 허용해주고 각자의 서비스에서 잘 구현했다고 가정하고,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드를 보내 토큰 발급요청 후에 AccessToken,RefreshToken,IdToken이 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;발급된다고 가정한 상태에서 구현방법을 설명하도록 하겠다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 적용하기&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두둥 프로젝트에서는 api 클라이언트로 &lt;b&gt;feign&lt;/b&gt;을 사용중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http템플릿써서 구현해도 캐싱방식에서만 구현의 편의성이 달라지지 다 똑같이 구현할 수 있다는 점 말씀드리고 싶다.&lt;/p&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;3.1. 공개키 목록 조회하기&amp;nbsp;, feign으로 캐싱하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;b&gt;IdToken&lt;/b&gt;을 받았을 때에 , 인증되었는지에 대한 확인을 하길 위해선 공개키로 확인을 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 공개키를 받아오는 api는 아래 문서에서 확인가능하다.&lt;/p&gt;
&lt;figure id=&quot;og_1678196272624&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#oidc-find-public-key&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bOk6sH/hyRROTPsKZ/kqUZPyHvrIQKf7zs3FPUJ1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/csrHbe/hyRRMVZ04K/HS255jKHdKBk0q1vvqox91/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/buK0Vj/hyRRF3DUb8/mNl6AielkcNmmc5fGKpnyK/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#oidc-find-public-key&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#oidc-find-public-key&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bOk6sH/hyRROTPsKZ/kqUZPyHvrIQKf7zs3FPUJ1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/csrHbe/hyRRMVZ04K/HS255jKHdKBk0q1vvqox91/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000,https://scrap.kakaocdn.net/dn/buK0Vj/hyRRF3DUb8/mNl6AielkcNmmc5fGKpnyK/img.png?width=3840&amp;amp;height=1000&amp;amp;face=0_0_3840_1000');&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;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1678196290286&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -v -X GET &quot;https://kauth.kakao.com/.well-known/jwks.json&quot;
HTTP/1.1 200 OK
{
    &quot;keys&quot;: [
        {
            &quot;kid&quot;: &quot;3f96980381e451efad0d2ddd30e3d3&quot;,
            &quot;kty&quot;: &quot;RSA&quot;,
            &quot;alg&quot;: &quot;RS256&quot;,
            &quot;use&quot;: &quot;sig&quot;,
            &quot;n&quot;: &quot;q8zZ0b_MNaLd6Ny8wd4cjFomilLfFIZcmhNSc1ttx_oQdJJZt5CDHB8WWwPGBUDUyY8AmfglS9Y1qA0_fxxs-ZUWdt45jSbUxghKNYgEwSutfM5sROh3srm5TiLW4YfOvKytGW1r9TQEdLe98ork8-rNRYPybRI3SKoqpci1m1QOcvUg4xEYRvbZIWku24DNMSeheytKUz6Ni4kKOVkzfGN11rUj1IrlRR-LNA9V9ZYmeoywy3k066rD5TaZHor5bM5gIzt1B4FmUuFITpXKGQZS5Hn_Ck8Bgc8kLWGAU8TzmOzLeROosqKE0eZJ4ESLMImTb2XSEZuN1wFyL0VtJw&quot;,
            &quot;e&quot;: &quot;AQAB&quot;
        }, {
            &quot;kid&quot;: &quot;9f252dadd5f233f93d2fa528d12fea&quot;,
            &quot;kty&quot;: &quot;RSA&quot;,
            &quot;alg&quot;: &quot;RS256&quot;,
            &quot;use&quot;: &quot;sig&quot;,
            &quot;n&quot;: &quot;qGWf6RVzV2pM8YqJ6by5exoixIlTvdXDfYj2v7E6xkoYmesAjp_1IYL7rzhpUYqIkWX0P4wOwAsg-Ud8PcMHggfwUNPOcqgSk1hAIHr63zSlG8xatQb17q9LrWny2HWkUVEU30PxxHsLcuzmfhbRx8kOrNfJEirIuqSyWF_OBHeEgBgYjydd_c8vPo7IiH-pijZn4ZouPsEg7wtdIX3-0ZcXXDbFkaDaqClfqmVCLNBhg3DKYDQOoyWXrpFKUXUFuk2FTCqWaQJ0GniO4p_ppkYIf4zhlwUYfXZEhm8cBo6H2EgukntDbTgnoha8kNunTPekxWTDhE5wGAt6YpT4Yw&quot;,
            &quot;e&quot;: &quot;AQAB&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답값을 보면&lt;b&gt; n,e 를 받아서 공개키를 만들 수 있고,&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjNQe2/btr2D0p4gWQ/kUuOGeyg8Slb4BHxVS1i20/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjNQe2/btr2D0p4gWQ/kUuOGeyg8Slb4BHxVS1i20/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjNQe2/btr2D0p4gWQ/kUuOGeyg8Slb4BHxVS1i20/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjNQe2%2Fbtr2D0p4gWQ%2FkUuOGeyg8Slb4BHxVS1i20%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;544&quot; height=&quot;207&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kid&lt;/b&gt; 또한 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개키가 하나인것이아니라 &lt;b&gt;IdToken&lt;/b&gt;의 헤더정보를 보면 &lt;b&gt;kid&lt;/b&gt; 정보가 있으며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 &lt;b&gt;kid와 동일한 키값을 가진 공개키로 ID 토큰 유효성 검증&lt;/b&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;//KakaoOauthClient
@FeignClient(
        name = &quot;KakaoAuthClient&quot;,
        url = &quot;https://kauth.kakao.com&quot;,
        configuration = KakaoKauthConfig.class)
public interface KakaoOauthClient {
	@Cacheable(cacheNames = &quot;KakaoOICD&quot;, cacheManager = &quot;oidcCacheManager&quot;)
	@GetMapping(&quot;/.well-known/jwks.json&quot;)
	OIDCPublicKeysResponse getKakaoOIDCOpenKeys();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;//RedisCacheConfig
@Bean
public CacheManager oidcCacheManager(RedisConnectionFactory cf) {
    RedisCacheConfiguration redisCacheConfiguration =
            RedisCacheConfiguration.defaultCacheConfig()
                    .serializeKeysWith(
                            RedisSerializationContext.SerializationPair.fromSerializer(
                                    new StringRedisSerializer()))
                    .serializeValuesWith(
                            RedisSerializationContext.SerializationPair.fromSerializer(
                                    new GenericJackson2JsonRedisSerializer()))
                    // TTL 일주일로 설정               
                    .entryTtl(Duration.ofDays(7L));

    return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf)
            .cacheDefaults(redisCacheConfiguration)
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 &lt;b&gt;feign 클라이언트요청에 캐싱을 해버릴 수 있다&lt;/b&gt;. 얼마나 간편한가? 한번에 끝나버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;위와같은 방식으로 공개키 목록을 로그인 요청시에 레디시 캐시저장소에서 꺼내올 수 있는 환경을 구성했다.&lt;/b&gt;&lt;/p&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;3.2. ID 토큰 유효성 검증하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.2.1 서명 검증전 페이로드 검증&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;카카오 토큰 유효성 검증하기에서 안내하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1678197233439&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. ID 토큰의 영역 구분자인 온점(.)을 기준으로 헤더, 페이로드, 서명을 분리
2. 페이로드를 Base64 방식으로 디코딩
3. 페이로드의 iss 값이 https://kauth.kakao.com와 일치하는지 확인
4. 페이로드의 aud 값이 서비스 앱 키와 일치하는지 확인
5. 페이로드의 exp 값이 현재 UNIX 타임스탬프(Timestamp)보다 큰 값인지 확인(ID 토큰이 만료되지 않았는지 확인)
6. 페이로드의 nonce 값이 카카오 로그인 요청 시 전달한 값과 일치하는지 확인
7. 서명 검증

# 서명 검증은 다음 순서로 진행합니다.

1. 헤더를 Base64 방식으로 디코딩
2. OIDC: 공개키 목록 조회하기를 통해 카카오 인증 서버가 서명 시 사용하는 공개키 목록 조회
3. 공개키 목록에서 헤더의 kid에 해당하는 공개키 값 확인
# 공개키는 일정 기간 캐싱(Caching)하여 사용할 것을 권장하며, 지나치게 빈번한 요청 시 요청이 차단될 수 있으므로 유의
4. JWT 서명 검증을 지원하는 라이브러리를 사용해 공개키로 서명 검증&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이중에서 6번 nonce 관련은 구현하지 않았다. 참고바란다.&lt;/b&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;1~5번은 한번에 처리가 가능하다.&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;style2&quot;&gt;&lt;b&gt;1. 우선은 인증전에 누구나 다 얻을 수 있는 payload를 가져온다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1678197357768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//JwtOICDProvider
private String getUnsignedToken(String token) {
    String[] splitToken = token.split(&quot;\\.&quot;);
    if (splitToken.length != 3) throw InvalidTokenException.EXCEPTION;
    return splitToken[0] + &quot;.&quot; + splitToken[1] + &quot;.&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure id=&quot;og_1678197393198&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;Allow parsing signed JWTs without the key &amp;middot; Issue #280 &amp;middot; jwtk/jjwt&quot; data-og-description=&quot;As a client I want to parse a JWT received from a server to inspect the contents. The JWT is signed by the server and obviously I don't have the secret signing key. Currently it's not possi...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jwtk/jjwt/issues/280&quot; data-og-url=&quot;https://github.com/jwtk/jjwt/issues/280&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zHrZ9/hyRRB732sD/pjKVHYkV8H6aYfd3kYi29K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jwtk/jjwt/issues/280&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jwtk/jjwt/issues/280&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zHrZ9/hyRRB732sD/pjKVHYkV8H6aYfd3kYi29K/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;Allow parsing signed JWTs without the key &amp;middot; Issue #280 &amp;middot; jwtk/jjwt&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;As a client I want to parse a JWT received from a server to inspect the contents. The JWT is signed by the server and obviously I don't have the secret signing key. Currently it's not possi...&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;Header.body.VerifySignature&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&amp;nbsp;- 2,3,4,5 번&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// JwtOIDCProvider
private Jwt&amp;lt;Header, Claims&amp;gt; getUnsignedTokenClaims(String token, String iss, String aud) {
    try {
        return Jwts.parserBuilder()
                .requireAudience(aud) //aud(두둥 카카오톡 어플리케이션 아이디) 가 같은지 확인
                .requireIssuer(iss)//iss(이슈어)가 카카오인지 확인
                .build()
                .parseClaimsJwt(getUnsignedToken(token));
    } catch (ExpiredJwtException e) { //파싱하면서 만료된 토큰인지 확인.
        throw ExpiredTokenException.EXCEPTION; 
    } catch (Exception e) {
        log.error(e.toString());
        throw InvalidTokenException.EXCEPTION;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;&gt;
&lt;li&gt;&lt;b&gt;iss(이슈어)가 카카오인지 확인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;aud(두둥 카카오톡 어플리케이션 아이디) 가 같은지 확인&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;2,3,4,5번이 한번에 끝나버렸다.&lt;/p&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;3.2.2 서명 검증하기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b07PAx/btr2Fg0qGjh/t4qJtOUB2KfWatdcMnFdL1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b07PAx/btr2Fg0qGjh/t4qJtOUB2KfWatdcMnFdL1/img.jpg&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://developers.kakao.com/docs/latest/ko/kakaologin/common#oidc-id-token&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b07PAx/btr2Fg0qGjh/t4qJtOUB2KfWatdcMnFdL1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb07PAx%2Fbtr2Fg0qGjh%2Ft4qJtOUB2KfWatdcMnFdL1%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;619&quot; height=&quot;515&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1328&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;amp;nbsp;https://developers.kakao.com/docs/latest/ko/kakaologin/common#oidc-id-token&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우린 이제 &lt;b&gt;Header&lt;/b&gt;와 &lt;b&gt;payload&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;공개키 목록 에서 쓸 kid를 가져오는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// JwtOIDCProvider
private final String KID = &quot;kid&quot;;

public String getKidFromUnsignedTokenHeader(String token, String iss, String aud) {
    return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get(KID);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kid를 가져오게되면 서명 검증에 어떤 공개키를 써야할지 결정할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// kakao Oauth helper
public OIDCDecodePayload getOIDCDecodePayload(String token) {
	// 공개키 목록을 조회한다. 캐싱이 되어있다.
    OIDCPublicKeysResponse oidcPublicKeysResponse = kakaoOauthClient.getKakaoOIDCOpenKeys();
    return oauthOIDCHelper.getPayloadFromIdToken(
    		//idToken
            token,
            // iss 와 대응되는 값
            oauthProperties.getKakaoBaseUrl(),
            // aud 와 대응되는값
            oauthProperties.getKakaoAppId(),
            // 공개키 목록
            oidcPublicKeysResponse);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Helper
@RequiredArgsConstructor
public class OauthOIDCHelper {
    private final JwtOIDCProvider jwtOIDCProvider;
	// OauthOIDC는 스펙이기때문에 OauthOIDCHelper 하나로 카카오,구글 다 대응 가능하다.
    // KakaoOauthHelper 등에서 아래 소스들을 사용한다.
    // kid를 토큰에서 가져온다.
    private String getKidFromUnsignedIdToken(String token, String iss, String aud) {
        return jwtOIDCProvider.getKidFromUnsignedTokenHeader(token, iss, aud);
    }
	
    public OIDCDecodePayload getPayloadFromIdToken(
            String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse) {
        String kid = getKidFromUnsignedIdToken(token, iss, aud);
		// KakaoOauthHelper 에서 공개키를 조회했고 해당 디티오를 넘겨준다.
        OIDCPublicKeyDto oidcPublicKeyDto =
                oidcPublicKeysResponse.getKeys().stream()
                		// 같은 kid를 가져온다.
                        .filter(o -&amp;gt; o.getKid().equals(kid))
                        .findFirst()
                        .orElseThrow();
		// 검증이 된 토큰에서 바디를 꺼내온다.
        return jwtOIDCProvider.getOIDCTokenBody(
                token, oidcPublicKeyDto.getN(), oidcPublicKeyDto.getE());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// JwtOIDCProvider
// 공개키로 토큰 검증을 시도한다.
public Jws&amp;lt;Claims&amp;gt; getOIDCTokenJws(String token, String modulus, String exponent) {
    try {
        return Jwts.parserBuilder()
                .setSigningKey(getRSAPublicKey(modulus, exponent))
                .build()
                .parseClaimsJws(token);
    } catch (ExpiredJwtException e) {
        throw ExpiredTokenException.EXCEPTION;
    } catch (Exception e) {
        log.error(e.toString());
        throw InvalidTokenException.EXCEPTION;
    }
}
// OIDCDecodePayload 를 가져온다. 스펙이라 공통으로 사용할 수 있다.
public OIDCDecodePayload getOIDCTokenBody(String token, String modulus, String exponent) {
    Claims body = getOIDCTokenJws(token, modulus, exponent).getBody();
    return new OIDCDecodePayload(
            body.getIssuer(),
            body.getAudience(),
            body.getSubject(),
            body.get(&quot;email&quot;, String.class));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앞써 비검증된 토큰에서 header정보와 payload 정보를 가져올 수 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Header 정보에서 kid 를 가져와 공개키를 어느키에 매칭시킬지 구한다.&lt;/b&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;&lt;b&gt;n, e를 조합해서 공개키를 직접만들어야한다.&lt;/b&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// JwtOIDCProvider
// 제일 핵심이 되는 소스이다 n ,e 값으로 Rsa 퍼블릭 키를 연산 할 수 있다.
// 진짜.. 힘들게 만든 소스다..잘 쓰시길 바란다..
private Key getRSAPublicKey(String modulus, String exponent)
        throws NoSuchAlgorithmException, InvalidKeySpecException {
    KeyFactory keyFactory = KeyFactory.getInstance(&quot;RSA&quot;);
    byte[] decodeN = Base64.getUrlDecoder().decode(modulus);
    byte[] decodeE = Base64.getUrlDecoder().decode(exponent);
    BigInteger n = new BigInteger(1, decodeN);
    BigInteger e = new BigInteger(1, decodeE);

    RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
    return keyFactory.generatePublic(keySpec);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;위소스는 넘겨온 n , e 로 공개키를 구하는 방식이다.&lt;/b&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;무제 2.jpg&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMxYcF/btr2DEAPXzv/SQ6jcyydpeXkyteiqypcr0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMxYcF/btr2DEAPXzv/SQ6jcyydpeXkyteiqypcr0/img.jpg&quot; data-alt=&quot;n,e를 구하는방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMxYcF/btr2DEAPXzv/SQ6jcyydpeXkyteiqypcr0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMxYcF%2Fbtr2DEAPXzv%2FSQ6jcyydpeXkyteiqypcr0%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;630&quot; height=&quot;184&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;n,e를 구하는방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;base64로 인코딩 된 값이 넘어오기때문에 디코드를 하면서 byte 어레이를 구하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이트에서 정수로 변환하는 작업을 거쳐야한다. n 숫자가 큰편이니 BigInteger를 써야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;signum 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;이렇게 해서 &lt;b&gt;인증된 idToken에서 바디 정보를 빼올 수 있다.&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;이렇게 IdToken을 활용해서 로그인 세션 유지를 하는 방법을 알아보았다.&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;Oauth 의 accessToken은 리소스서버에 인가를 위한 것이고,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IdToken은 인증을 위한것이며,&lt;/b&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;&lt;b&gt;Rsa 공개키방식으로 토큰 검증을 할 수 있는 idToken을 활용하여&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실 회원가입전 id token을 활용해서 oauth에 인증된 사용자임을 검증 하고 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 소스들은 두둥 프로젝트에서 참고 가능하다.&lt;/p&gt;
&lt;figure id=&quot;og_1678201110165&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bs5etF/hyRQsxCvr8/Au09XQm4S0V9EOiHijbL1k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bs5etF/hyRQsxCvr8/Au09XQm4S0V9EOiHijbL1k/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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;- &lt;a title=&quot;JwtOIDCProvider&quot; href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Common/src/main/java/band/gosrock/common/jwt/JwtOIDCProvider.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JwtOIDCProvider&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a title=&quot;KakaoOauthHelper&quot; href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/auth/service/helper/KakaoOauthHelper.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;KakaoOauthHelper&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a title=&quot;OauthOIDCHelper&quot; href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/auth/service/helper/OauthOIDCHelper.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OauthOIDCHelper&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a title=&quot;KakaoOauthClient&quot; href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/java/band/gosrock/infrastructure/outer/api/oauth/client/KakaoOauthClient.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;KakaoOauthClient&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a title=&quot;RedisCacheConfig&quot; href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/java/band/gosrock/infrastructure/config/redis/RedisCacheConfig.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RedisCacheConfig&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;아마 위 예시 소스로는 복잡해서, 구현하기 힘드실것같다..&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;찾아봐도 OIDC 를 직접 구현한 자료가 없어서&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;다행이도 캐싱과같은건 feign클라이언트를 쓰고있어서 편하게 했고,&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;카카오의 경우 aud(앱키)가 앱환경에서는 해당 네이티브 앱 키로 나온다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/16Tr6/btr2D4svQWV/m2YqaKdLbGakLT4Z2DeJBk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/16Tr6/btr2D4svQWV/m2YqaKdLbGakLT4Z2DeJBk/img.jpg&quot; data-alt=&quot;내 어플리케이션 &amp;amp;gt; 앱설정 &amp;amp;gt; 요약정보&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/16Tr6/btr2D4svQWV/m2YqaKdLbGakLT4Z2DeJBk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F16Tr6%2Fbtr2D4svQWV%2Fm2YqaKdLbGakLT4Z2DeJBk%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;200&quot; height=&quot;279&quot; data-filename=&quot;무제 2.jpg&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내 어플리케이션 &amp;gt; 앱설정 &amp;gt; 요약정보&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>스프링</category>
      <category>JWT</category>
      <category>oauth</category>
      <category>OIDC</category>
      <category>두둥</category>
      <category>스프링</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/35</guid>
      <comments>https://devnm.tistory.com/35#entry35comment</comments>
      <pubDate>Wed, 8 Mar 2023 00:03:07 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] spring feign client wiremock test</title>
      <link>https://devnm.tistory.com/34</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUxJiP/btr2r50GU2n/G3FXhpeHoStxxtFSAnyET0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUxJiP/btr2r50GU2n/G3FXhpeHoStxxtFSAnyET0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUxJiP/btr2r50GU2n/G3FXhpeHoStxxtFSAnyET0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUxJiP%2Fbtr2r50GU2n%2FG3FXhpeHoStxxtFSAnyET0%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;629&quot; height=&quot;291&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;580&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;두둥 서비스에서 api client로 &lt;b&gt;feign&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;mocking&lt;/b&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;figure id=&quot;og_1678170177049&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코어 API | 토스페이먼츠 개발자센터&quot; data-og-description=&quot;토스페이먼츠 API 엔드포인트(Endpoint)와 객체 정보, 파라미터, 요청 및 응답 예제를 살펴보세요.&quot; data-og-host=&quot;docs.tosspayments.com&quot; data-og-source-url=&quot;https://docs.tosspayments.com/reference#%EC%A0%95%EC%82%B0-%EC%A1%B0%ED%9A%8C&quot; data-og-url=&quot;https://docs.tosspayments.com/reference&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c3AeRs/hyRQrMg7lD/M9YHA5WW8E82dl0s2QnBqk/img.png?width=780&amp;amp;height=390&amp;amp;face=0_0_780_390&quot;&gt;&lt;a href=&quot;https://docs.tosspayments.com/reference#%EC%A0%95%EC%82%B0-%EC%A1%B0%ED%9A%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.tosspayments.com/reference#%EC%A0%95%EC%82%B0-%EC%A1%B0%ED%9A%8C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c3AeRs/hyRQrMg7lD/M9YHA5WW8E82dl0s2QnBqk/img.png?width=780&amp;amp;height=390&amp;amp;face=0_0_780_390');&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;코어 API | 토스페이먼츠 개발자센터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;토스페이먼츠 API 엔드포인트(Endpoint)와 객체 정보, 파라미터, 요청 및 응답 예제를 살펴보세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.tosspayments.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정산 조회 작업을 거쳐서 발생 주문 건에 대해서 &lt;b&gt;PG 수수료&lt;/b&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;응답 dto로 알맞게 파싱되는지, 테스트를 해보고 싶었다.&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;/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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. wiremock&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;&amp;nbsp; 2.1. feign client&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 2.2. 테스트 코드 작성하기&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. wiremock&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;wiremock&lt;/b&gt; 은 테스트 목적으로 사용할 수 있는 웹 서버이다. 즉 테스트 환경에서 요청이 들어오면 정해진 응답을 반환 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;wiremock&lt;/b&gt; 자체로도 서버를 껏다 킬 수 있지만 스프링과도 연동이 가능하다.&lt;/p&gt;
&lt;figure id=&quot;og_1678178812424&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;10.&amp;nbsp;Spring Cloud Contract WireMock&quot; data-og-description=&quot;Modules giving you the possibility to use WireMock with different servers by using the &amp;quot;ambient&amp;quot; server embedded in a Spring Boot application. Check out the samples for more details. If you have a Spring Boot application that uses Tomcat as an embedded ser&quot; data-og-host=&quot;cloud.spring.io&quot; data-og-source-url=&quot;https://cloud.spring.io/spring-cloud-contract/1.1.x/multi/multi__spring_cloud_contract_wiremock.html&quot; data-og-url=&quot;https://cloud.spring.io/spring-cloud-contract/1.1.x/multi/multi__spring_cloud_contract_wiremock.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://cloud.spring.io/spring-cloud-contract/1.1.x/multi/multi__spring_cloud_contract_wiremock.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cloud.spring.io/spring-cloud-contract/1.1.x/multi/multi__spring_cloud_contract_wiremock.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;10.&amp;nbsp;Spring Cloud Contract WireMock&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Modules giving you the possibility to use WireMock with different servers by using the &quot;ambient&quot; server embedded in a Spring Boot application. Check out the samples for more details. If you have a Spring Boot application that uses Tomcat as an embedded ser&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cloud.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;testImplementation &quot;org.springframework.cloud:spring-cloud-contract-wiremock:3.1.5&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 모듈을 추가하게 되면 스프링과 쉽게 연동할 수 있다. ( 공식문서에 있는 내용 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우린 standalone 모드 대신에 테스트 과정에 포함시킬거다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[
  {
    &quot;mId&quot;: &quot;tosspayments&quot;,
    &quot;paymentKey&quot;: &quot;5zJ4xY7m0kODnyRpQWGrN2xqGlNvLrKwv1M9ENjbeoPaZdL6&quot;,
    &quot;transactionKey&quot;: &quot;8B4F646A829571D870A3011A4E13D640&quot;,
    &quot;orderId&quot;: &quot;a4CWyWY5m89PNh7xJwhk1&quot;,
    &quot;currency&quot;: &quot;KRW&quot;,
    &quot;method&quot;: &quot;카드&quot;,
    &quot;amount&quot;: 34000
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 json 파일을 위와 같이 만들어두면,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mCjaB/btr2vUqBGYx/avTdQuq7fBzDVHPuPKz2Kk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mCjaB/btr2vUqBGYx/avTdQuq7fBzDVHPuPKz2Kk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mCjaB/btr2vUqBGYx/avTdQuq7fBzDVHPuPKz2Kk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmCjaB%2Fbtr2vUqBGYx%2FavTdQuq7fBzDVHPuPKz2Kk%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;830&quot; height=&quot;206&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Path file = ResourceUtils.getFile(&quot;classpath:payload/settlement-response.json&quot;).toPath();
// 응답예시 생성하기
.withBody(Files.readAllBytes(file))));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드 작성시에 미리 만들어둔 json 응답을 줄 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 적용하기&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;2.1. feign client&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@FeignClient(
        name = &quot;SettlementClient&quot;,
        // url 자체를 환경변수로 세팅한다.
        url = &quot;${feign.toss.url}&quot;,
        configuration = {TransactionGetConfig.class})
public interface SettlementClient {
    @GetMapping(&quot;/v1/settlements&quot;)
    List&amp;lt;SettlementResponse&amp;gt; execute(
            @RequestParam(value = &quot;startDate&quot;) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
                    LocalDate startDate,
            @RequestParam(value = &quot;endDate&quot;) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
                    LocalDate endDate,
            @RequestParam(value = &quot;dateType&quot;) String dateType,
            @RequestParam(value = &quot;page&quot;) int page,
            @RequestParam(value = &quot;size&quot;) int size);
}
// application-infrastructure.yml
feign:
  toss:
    url : https://api.tosspayments.com&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 요청인데 중요한 부분은 url 자체를 환경변수로 넣어야한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 요청 주소를 default 로 세팅 해놓고 테스트를 돌릴 때 &lt;b&gt;@TestPropertySource&lt;/b&gt; 로 환경변수를 &lt;b&gt;localhost&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;여담이지만 &lt;b&gt;@TestPropertySource 는 엄청 유용하게 쓸데가 많은것 같다.&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678181433784&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[스프링] Spring disable Aop in test&quot; data-og-description=&quot;오랜만에... 글을 씁니다.! 디프만 12기 끝나고 ( 13기 운영진도 할 예정입니다. ㅎㅎ ) 고스락 티켓예매 세번째 프로젝트로 두둥이라는 프로젝트를 시작하게 되었다. 기존엔 고스락만을 위한 예매&quot; data-og-host=&quot;devnm.tistory.com&quot; data-og-source-url=&quot;https://devnm.tistory.com/24&quot; data-og-url=&quot;https://devnm.tistory.com/24&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/PfmRI/hyRRG2hdQX/YRUx802yqawBisYxSaX3Bk/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411,https://scrap.kakaocdn.net/dn/ctXPdX/hyRRI6RdXN/gjdGsXo76l3zhhLx49GP3K/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411,https://scrap.kakaocdn.net/dn/bpFPTR/hyRRDkbeeV/fUVU3FeIC6xpNg4RtDdcAk/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411&quot;&gt;&lt;a href=&quot;https://devnm.tistory.com/24&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devnm.tistory.com/24&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/PfmRI/hyRRG2hdQX/YRUx802yqawBisYxSaX3Bk/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411,https://scrap.kakaocdn.net/dn/ctXPdX/hyRRI6RdXN/gjdGsXo76l3zhhLx49GP3K/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411,https://scrap.kakaocdn.net/dn/bpFPTR/hyRRDkbeeV/fUVU3FeIC6xpNg4RtDdcAk/img.png?width=731&amp;amp;height=411&amp;amp;face=0_0_731_411');&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;[스프링] Spring disable Aop in test&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오랜만에... 글을 씁니다.! 디프만 12기 끝나고 ( 13기 운영진도 할 예정입니다. ㅎㅎ ) 고스락 티켓예매 세번째 프로젝트로 두둥이라는 프로젝트를 시작하게 되었다. 기존엔 고스락만을 위한 예매&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devnm.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 쓴글인 spring disable aop in test 도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 돌 때 어느 한 aop 를 끄고싶을 때도 설정할 수 있다.!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위글에서는 분산락을 aop 통해서 적용하고 있었는데,&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;이 외엔 일반적인 feign client 모습과 동일하다.&lt;/p&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;2.2. 테스트 코드 작성하기&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@SpringBootTest(classes = InfraIntegrateTestConfig.class)
@AutoConfigureWireMock(port = 0)
@ActiveProfiles(resolver = InfraIntegrateProfileResolver.class)
@TestPropertySource(
        properties = {
            &quot;feign.toss.url=http://localhost:${wiremock.server.port}&quot;,
            // 타임리프때문에 테스트 깨져서 넣음
            &quot;spring.thymeleaf.enabled=false&quot;
        })
public class TossSettlementClientTest {&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;@AutoConfigureWireMock(port = 0)&lt;/b&gt; 로설정해서 &lt;b&gt;randomport&lt;/b&gt; 상에서 실행되게끔 하는것이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feign 클라이언트가 요청 보낼 주소를 localhost 에 어느&lt;b&gt; random port&lt;/b&gt; 인지는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;wiremock.server.port&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;이렇게 요청보낼 주소를 정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan(basePackageClasses = {DuDoongInfraApplication.class, DuDoongCommonApplication.class})
public class InfraIntegrateTestConfig {}

public class InfraIntegrateProfileResolver implements ActiveProfilesResolver {

    @Override
    public String[] resolve(Class&amp;lt;?&amp;gt; testClass) {
        // some code to find out your active profiles
        return new String[] {&quot;common&quot;, &quot;infrastructure&quot;};
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;InfraIntegrateTestConfig , InfraIntegrateProfileResolver&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 스캔 범위를 지정하고, 필요한 profile을 편하게 설정하기 위해서 만든 편의 클래스이다.&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;color: #666666;&quot;&gt;각 모듈 ( 최종 어플리케이션 모듈이 아닌 의존성이있는 모듈등인 경우 )&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;application-{모듈이름}.yml 형태로 환경변수들을 세팅해 줄 수 있고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;profile에 지정을 해줘야 해당 모듈의 환경변수들을 불러올 수 있다.&lt;/span&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;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Autowired private SettlementClient settlementClient;

@Test
public void 정산요청_올바르게_파싱되어야한다() throws IOException {
    // 만들어둔 json 파일을 불러온다.
    Path file = ResourceUtils.getFile(&quot;classpath:payload/settlement-response.json&quot;).toPath();
	// import static com.github.tomakehurst.wiremock.client.WireMock.*
    stubFor(
    		// localhost:${wiremock.server.port}/v1/settlements 요청은 willReturn을 한다.
            get(urlPathEqualTo(&quot;/v1/settlements&quot;))
                    .willReturn(
                            aResponse()
                                    .withStatus(HttpStatus.OK.value())
                                    .withHeader(
                                            &quot;Content-Type&quot;, MediaType.APPLICATION_JSON_VALUE)
                                    // 바디 지정
                                    .withBody(Files.readAllBytes(file))));
    LocalDate now = LocalDate.now();
    // 실제 요청이 List&amp;lt;SettlementResponse&amp;gt;에 담겨서온다 
    List&amp;lt;SettlementResponse&amp;gt; test = settlementClient.execute(now, now, &quot;test&quot;, 1, 10);
	
    SettlementResponse settlementResponse = test.get(0);
    // 파싱이 제대로 되었는지 확인.. 다 확인은 안하고 디버거로 적당히 했다.!
    assertEquals(settlementResponse.getFees().size(), 2);
}&lt;/code&gt;&lt;/pre&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;무제.jpg&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;970&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5wORi/btr2DDhjKoG/uCOVmD1gYGEbfd3HEBPZ01/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5wORi/btr2DDhjKoG/uCOVmD1gYGEbfd3HEBPZ01/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5wORi/btr2DDhjKoG/uCOVmD1gYGEbfd3HEBPZ01/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5wORi%2Fbtr2DDhjKoG%2FuCOVmD1gYGEbfd3HEBPZ01%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;343&quot; height=&quot;322&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;970&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;Enum 값이나 iso 형식으로 넘어오는 날짜 데이타들이 잘 파싱되는것을 볼 수 있다.&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;이렇게 wiremock 을 활용해서 테스트 코드간에 응답을 미리 지정해서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 보낸뒤에 파싱이 잘되는지 확인해 보았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;figure id=&quot;og_1678181216902&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/outer/api/tossPayments/client/TossSettlementClientTest.java&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vuvK1/hyRRD5yAv0/1dRL3XPyPh0EcjDLD3lhe0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/outer/api/tossPayments/client/TossSettlementClientTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/outer/api/tossPayments/client/TossSettlementClientTest.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vuvK1/hyRRD5yAv0/1dRL3XPyPh0EcjDLD3lhe0/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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;소스 코드는 레포지토리에서 확인 가능하다.&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;</description>
      <category>스프링</category>
      <category>feign</category>
      <category>wiremock</category>
      <category>스프링</category>
      <category>테스트</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/34</guid>
      <comments>https://devnm.tistory.com/34#entry34comment</comments>
      <pubDate>Tue, 7 Mar 2023 18:32:32 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] spring batch 도커로 세팅하기 with jenkins</title>
      <link>https://devnm.tistory.com/33</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LF6Em/btr2wtsghaL/rmrWVIha9x3xfIgWKRuOx1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LF6Em/btr2wtsghaL/rmrWVIha9x3xfIgWKRuOx1/img.jpg&quot; data-alt=&quot;두둥 이벤트 거래정산 파이프라인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LF6Em/btr2wtsghaL/rmrWVIha9x3xfIgWKRuOx1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLF6Em%2Fbtr2wtsghaL%2FrmrWVIha9x3xfIgWKRuOx1%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;1860&quot; height=&quot;456&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;두둥 이벤트 거래정산 파이프라인&lt;/figcaption&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;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=_nkJkWVH-mo&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/c3yBxF/hyRRFhJzna/2gaWxCbFYAMFcXMSZ5hEaK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/_nkJkWVH-mo&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;&lt;/figcaption&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;&lt;b&gt;스프링 배치에서 스텝&lt;/b&gt;으로 나눈것이 아닌&lt;b&gt; 잡&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;강의 내용중에 스프링 배치 어플리케이션의 버젼이 바뀐경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 최신본을 실행시키기위해 app.jar 를 바라보게 한후에 &lt;b&gt;readlink&lt;/b&gt; 로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;v1 -&amp;gt; v2.jar&lt;/b&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;필자는 Api 어플리케이션도 도커로 말아서 배포 중이므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 어플리케이션도 도커로 말아서 항상 최신본을 실행시킬수 있는 구성을 공유하고자한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 배치 어플리케이션 도커로 세팅&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;3. sh 커맨드로 젠킨스 잡 구성하기&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 배치 어플리케이션 도커로 세팅&lt;/h2&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;도메인 관련 오브젝트들을 API 어플리케이션에도 사용하고, Batch 어플리케이션 둘다 사용가능하다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 배치 어플리케이션 그래들
implementation project(':DuDoong-Domain')
implementation project(':DuDoong-Common')
implementation project(':DuDoong-Infrastructure')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 구성을했고, 배치 어플리케이션을 항상 실행시키는것이아니라,&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  profiles:
    include:
      - infrastructure
      - domain
      - common
      # --job.name=&amp;lt;잡이름&amp;gt; 형태로 파라미터를 넘겨줘서 잡을 실행시킨다.
  batch.job.names: ${job.name:NONE}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잡 단위로 실행시키기 때문에 application.yml을 위와 같이 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;FROM openjdk:17-alpine

EXPOSE 8080

COPY ./build/libs/*.jar app.jar
ARG PROFILE=dev
ENV PROFILE=${PROFILE}

ENTRYPOINT [&quot;java&quot;,&quot;-Dspring.profiles.active=${PROFILE}&quot;, &quot;-Djava.security.egd=file:/dev/./urandom&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile은 위와같이 세팅했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Api 어플리케이션 도커파일과 같다.&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;도커를 말아올리는 ci는 github action을 사용중인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그기반으로 액션을 트리거 시키고있다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;on:
  push:
    tags:
      - Api-v*.*.*&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;on:
  push:
    tags:
      - Batch-v*.*.*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Api,Batch 두가지로 시작하는 태그에 반응하도록 하고있고,&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;RELEASE_VERSION_WITHOUT_V=&quot;$(cut -d'v' -f2 &amp;lt;&amp;lt;&amp;lt; ${GITHUB_REF#refs/*/})&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 Batch-v1.0.0 이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커태그는 {user}/{project}:1.0.0 형식의 도커가 만들어지도록 하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rKmKU/btr2vnzlPc2/A2uztlbYN7EMBkuacEYrV0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rKmKU/btr2vnzlPc2/A2uztlbYN7EMBkuacEYrV0/img.jpg&quot; data-alt=&quot;태그 릴리즈&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rKmKU/btr2vnzlPc2/A2uztlbYN7EMBkuacEYrV0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrKmKU%2Fbtr2vnzlPc2%2FA2uztlbYN7EMBkuacEYrV0%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;470&quot; height=&quot;223&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;태그 릴리즈&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃헙의 릴리즈를 사용해서 태그를 달아주고있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 젠킨스 글로벌 환경변수 세팅&lt;/h2&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;무제.jpg&quot; data-origin-width=&quot;2900&quot; data-origin-height=&quot;1452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfG1Wz/btr2xSeDsk2/X9IJ70C3iMuB3DWqpY9KPk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfG1Wz/btr2xSeDsk2/X9IJ70C3iMuB3DWqpY9KPk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfG1Wz/btr2xSeDsk2/X9IJ70C3iMuB3DWqpY9KPk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfG1Wz%2Fbtr2xSeDsk2%2FX9IJ70C3iMuB3DWqpY9KPk%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;708&quot; height=&quot;354&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2900&quot; data-origin-height=&quot;1452&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;실행시키고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;latest 태그로 항상 최신버젼을 가져오고있는데 이는 깃헙 action ci 단에서 버젼이 붙은 태그로도 배포하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;latest 태그도 항상붙여서 올리고있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678168751829&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker run --pull=always --env-file {환경변수 파일 위치} --net=host {도커이미지}:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sudo 를 젠킨스에서 붙일려면&lt;/p&gt;
&lt;figure id=&quot;og_1678168373492&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Authentication error in jenkins on using sudo&quot; data-og-description=&quot;I have sh script in jenkins which has sudo ssh command and I am getting this error Warning: Identity file key.pem not accessible: Permission denied. Host key verification failed. sudo: no tty pres...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/17940612/authentication-error-in-jenkins-on-using-sudo&quot; data-og-url=&quot;https://stackoverflow.com/questions/17940612/authentication-error-in-jenkins-on-using-sudo&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CAFRP/hyRRCSVJs9/bmtU0xMS72eksK414KQVD0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/17940612/authentication-error-in-jenkins-on-using-sudo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/17940612/authentication-error-in-jenkins-on-using-sudo&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CAFRP/hyRRCSVJs9/bmtU0xMS72eksK414KQVD0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;Authentication error in jenkins on using sudo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I have sh script in jenkins which has sudo ssh command and I am getting this error Warning: Identity file key.pem not accessible: Permission denied. Host key verification failed. sudo: no tty pres...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.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;위 내용을 참고하기 바란다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. sh 커맨드로 젠킨스 잡 구성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678168467193&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;${BATCH_PROD_APP} --job.name=슬랙유저통계 date=${TODAY}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2296&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4BLYz/btr2r4tSwRv/9vcOA5wvMHvOHAwfRvBI40/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4BLYz/btr2r4tSwRv/9vcOA5wvMHvOHAwfRvBI40/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4BLYz/btr2r4tSwRv/9vcOA5wvMHvOHAwfRvBI40/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4BLYz%2Fbtr2r4tSwRv%2F9vcOA5wvMHvOHAwfRvBI40%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;2296&quot; height=&quot;1006&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;2296&quot; data-origin-height=&quot;1006&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;추가로 넣어야할 매개변수가 있는경우,&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&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;
&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;/p&gt;
&lt;figure id=&quot;og_1678168639637&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bs5etF/hyRQsxCvr8/Au09XQm4S0V9EOiHijbL1k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bs5etF/hyRQsxCvr8/Au09XQm4S0V9EOiHijbL1k/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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>스프링</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/33</guid>
      <comments>https://devnm.tistory.com/33#entry33comment</comments>
      <pubDate>Tue, 7 Mar 2023 15:00:25 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] spring thymeleaf to pdf  이미지,한글 적용하기</title>
      <link>https://devnm.tistory.com/32</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;1666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCZ7KF/btr115T4HOj/LCSRtzuFdzuvgGVtuWlMyK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCZ7KF/btr115T4HOj/LCSRtzuFdzuvgGVtuWlMyK/img.jpg&quot; data-alt=&quot;두둥의 정산서 pdf&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCZ7KF/btr115T4HOj/LCSRtzuFdzuvgGVtuWlMyK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCZ7KF%2Fbtr115T4HOj%2FLCSRtzuFdzuvgGVtuWlMyK%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;277&quot; height=&quot;352&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;1666&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;두둥의 정산서 pdf&lt;/figcaption&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;통신 판매 중개업종이므로 , 호스트에게 공연 카드결제 대금&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정산 작업도 배치로 돌려야 했는데, 이때 pdf로 정산서를 보내줘야할 일이 생겼다.&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;두둥에서는 메일도 thymeleaf를 사용해서 보내고 있으므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pdf 도 thymeleaf 를 사용해서 보내는 방법으로 결정했다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp; flying-saucer-pdf&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;3. 이미지 가능하게 하기&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. flying-saucer-pdf&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1678084372584&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 - flyingsaucerproject/flyingsaucer: XML/XHTML and CSS 2.1 renderer in pure Java&quot; data-og-description=&quot;XML/XHTML and CSS 2.1 renderer in pure Java. Contribute to flyingsaucerproject/flyingsaucer development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/flyingsaucerproject/flyingsaucer&quot; data-og-url=&quot;https://github.com/flyingsaucerproject/flyingsaucer&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/khaON/hyRQsX2NOO/BD85SQRkbDn5SYUhMWnaSk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/flyingsaucerproject/flyingsaucer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/flyingsaucerproject/flyingsaucer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/khaON/hyRQsX2NOO/BD85SQRkbDn5SYUhMWnaSk/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 - flyingsaucerproject/flyingsaucer: XML/XHTML and CSS 2.1 renderer in pure Java&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;XML/XHTML and CSS 2.1 renderer in pure Java. Contribute to flyingsaucerproject/flyingsaucer 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;baeldung 에서도 자세히는 아니지만 대충은 확인 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/thymeleaf-generate-pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/thymeleaf-generate-pdf&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;p data-ke-size=&quot;size16&quot;&gt;간단하다. 해당 모듈을 이용해서 thymeleaf 로 html 스트링을 만든뒤에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;render 을 통해서 pdf를 만드는것이 가능하다.&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;두둥 서버스에서는 outputStream 을 파일로 내려주는게 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ByteStream 으로 만든뒤에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;private S3 에 올리고 나서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 전송용 잡에서 private S3에서 다운로드 받아 이메일로 전송하는 구조를 취하고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 타임리프 템플릿 엔진에서 settlement.html 파일을 context와 내려준다.
String html = templateEngine.process(&quot;settlement&quot;, context);
// PdfRender 클래스
@Component
@RequiredArgsConstructor
@Slf4j
public class PdfRender {
    
    public ByteArrayOutputStream generatePdfFromHtml(String html)
            throws DocumentException, IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        //renderer 설정
        ITextRenderer renderer = new ITextRenderer();

        renderer.getFontResolver();
        renderer.setDocumentFromString(html);
        renderer.layout();
		// PDF 만들어준다.
        renderer.createPDF(outputStream);

        outputStream.close();
        // outputStream 으로 리턴후 S3로 파일업로드를 stream 형태로 올린다.
        return outputStream;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임리프 템플릿 엔진으로 html으로 변환을 한후에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pdf로 렌더링 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1678089826821&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;FileOutputStream (Java Platform SE 7 )&quot; data-og-description=&quot;Returns the unique FileChannel object associated with this file output stream. The initial position of the returned channel will be equal to the number of bytes written to the file so far unless this stream is in append mode, in which case it will be equal&quot; data-og-host=&quot;docs.oracle.com&quot; data-og-source-url=&quot;https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html&quot; data-og-url=&quot;https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;FileOutputStream (Java Platform SE 7 )&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Returns the unique FileChannel object associated with this file output stream. The initial position of the returned channel will be equal to the number of bytes written to the file so far unless this stream is in append mode, in which case it will be equal&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.oracle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;pre id=&quot;code_1678089967582&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String outputFolder = System.getProperty(&quot;user.home&quot;) + File.separator + &quot;thymeleaf.pdf&quot;;
OutputStream outputStream = new FileOutputStream(outputFolder);
 // 중간 생략
renderer.createPDF(outputStream);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PdfRender 클래스를 만든후에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 작업간에 실행시켰다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 한글 적용하기&lt;/h2&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;따라서 한글이 되는 폰트를 리소스에 넣고 , 해당 한글 폰트를 html에 넣은뒤에&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;pre class=&quot;css&quot;&gt;&lt;code&gt;body {
  width: 100%;
  font-size: 12px;
  font-weight: lighter;
  color:#000;
  line-height:180%;
  font-family: NanumBarunGothic,serif;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선은 폰트는 NanumBarunGothic 으로 정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글 되는 폰트면 다된다.!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나눔 바른 고딕 치면 다운로드 가능하다 NanumBarunGothic.ttf 로 하면 된다.&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;그뒤에 render 과정에서 폰트를 정해줘야한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;renderer.getFontResolver()
	//폰트를 설정한다.
        .addFont(
                new ClassPathResource(&quot;/templates/NanumBarunGothic.ttf&quot;)
                        .getURL()
                        .toString(),
                BaseFont.IDENTITY_H,
                BaseFont.EMBEDDED);
renderer.setDocumentFromString(html);
renderer.layout();&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 이미지 가능하게 하기&lt;/h2&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;b&gt;1. 정적 이미지를 리소스에 포함해서 하는 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. html src url 을 pdf 그리는 과정에서 감지 되는 스트림으로 받아와서 base64로 인코딩한후&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;html에 직접 포함하는 방법.&lt;/b&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;1번의 형식은 로컬에서 잘됐는데 도커환경에서 배포를 해보니 pdf 에 이미지가 제대로 안담기는 현상이 일어났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 파일로 포함해서 올리기는 .. 패키징 사이즈도 커지니 부담스러워서&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;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;img
    width=&quot;244&quot;
    alt=&quot;en-banner-black&quot;
    src=&quot;https://asset.dudoong.com/common/en-banner-black.png&quot;
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;html 템플릿 안에는 원래대로 src를 적어둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class B64ImgReplacedElementFactory implements ReplacedElementFactory {

    public ReplacedElement createReplacedElement(
            LayoutContext c, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) {
        Element e = box.getElement();
        if (e == null) {
            return null;
        }
        String nodeName = e.getNodeName();
        if (nodeName.equals(&quot;img&quot;)) {
            String attribute = e.getAttribute(&quot;src&quot;);
            FSImage fsImage;
            try {
                fsImage = buildImage(attribute, uac);
            } catch (BadElementException e1) {
                fsImage = null;
            } catch (IOException e1) {
                fsImage = null;
            }
            if (fsImage != null) {
                if (cssWidth != -1 || cssHeight != -1) {
                    fsImage.scale(cssWidth, cssHeight);
                }
                return new ITextImageElement(fsImage);
            }
        }
        return null;
    }

    protected FSImage buildImage(String srcAttr, UserAgentCallback uac)
            throws IOException, BadElementException {
        FSImage fsImage;
        if (srcAttr.startsWith(&quot;data:image/&quot;)) {
            String b64encoded =
                    srcAttr.substring(
                            srcAttr.indexOf(&quot;base64,&quot;) + &quot;base64,&quot;.length(), srcAttr.length());

            byte[] decodedBytes = Base64.getDecoder().decode(b64encoded);
            fsImage = new ITextFSImage(Image.getInstance(decodedBytes));
        } else {
            fsImage = uac.getImageResource(srcAttr).getImage();
        }
        return fsImage;
    }

    public void remove(Element e) {}

    public void reset() {}

    @Override
    public void setFormSubmissionListener(FormSubmissionListener listener) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B64ImgReplacedElementFactory 클래스만든뒤에 img 태그가 발견되면 이미지를 다운로드 받아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;base64 형식으로 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class PdfRender {

    private final B64ImgReplacedElementFactory b64ImgReplacedElementFactory;

    public ByteArrayOutputStream generatePdfFromHtml(String html)
            throws DocumentException, IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ITextRenderer renderer = new ITextRenderer();
        SharedContext sharedContext = renderer.getSharedContext();
        sharedContext.setPrint(true);
        sharedContext.setInteractive(false);
        sharedContext.setReplacedElementFactory(b64ImgReplacedElementFactory);
        sharedContext.getTextRenderer().setSmoothingThreshold(0);

        renderer.getFontResolver()
                .addFont(
                        new ClassPathResource(&quot;/templates/NanumBarunGothic.ttf&quot;)
                                .getURL()
                                .toString(),
                        BaseFont.IDENTITY_H,
                        BaseFont.EMBEDDED);
        renderer.setDocumentFromString(html);
        renderer.layout();

        renderer.createPDF(outputStream);

        outputStream.close();
        return outputStream;
    }
}
&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;B64ImgReplacedElementFactory 를 적용하면 이미지도 포함해서 pdf를 랜더링 할 수 있다.&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;pre id=&quot;code_1678092551264&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Batch App 이벤트 pdf 정산 소스
String html = templateEngine.process(&quot;settlement&quot;, context);
// html
ByteArrayOutputStream outputStream =
	pdfRender.generatePdfFromHtml(html);

String fileKey =
	s3PrivateFileUploadService.eventSettlementPdfUpload(event.getId(), outputStream);&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&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;&amp;nbsp;- pdfRender&lt;/p&gt;
&lt;figure id=&quot;og_1678092278204&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/java/band/gosrock/infrastructure/config/pdf/PdfRender.java&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2yQBj/hyRRJYgYKn/cyG8KrTrTarz5XC93EmBb1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/java/band/gosrock/infrastructure/config/pdf/PdfRender.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/java/band/gosrock/infrastructure/config/pdf/PdfRender.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2yQBj/hyRRJYgYKn/cyG8KrTrTarz5XC93EmBb1/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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 data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;B64ImgReplacedElementFactory&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1678092287290&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/java/band/gosrock/infrastructure/config/pdf/B64ImgReplacedElementFactory.java&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ckWLRx/hyRQnoZBZP/unmRxRC58UMX5yYQ5R1Ao1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/java/band/gosrock/infrastructure/config/pdf/B64ImgReplacedElementFactory.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/java/band/gosrock/infrastructure/config/pdf/B64ImgReplacedElementFactory.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ckWLRx/hyRQnoZBZP/unmRxRC58UMX5yYQ5R1Ao1/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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 data-ke-size=&quot;size16&quot;&gt;- settlement.html&lt;/p&gt;
&lt;figure id=&quot;og_1678092293583&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/resources/templates/settlement.html&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Co8dl/hyRQksgj9f/bZFEN8kN6OUdLdbIJHdJ70/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/resources/templates/settlement.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Infrastructure/src/main/resources/templates/settlement.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Co8dl/hyRQksgj9f/bZFEN8kN6OUdLdbIJHdJ70/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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 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;</description>
      <category>스프링</category>
      <category>스프링</category>
      <category>타임리프</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/32</guid>
      <comments>https://devnm.tistory.com/32#entry32comment</comments>
      <pubDate>Mon, 6 Mar 2023 17:49:48 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] spring rate limit 적용히기 bucket4j</title>
      <link>https://devnm.tistory.com/31</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1304&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/elfBHz/btr2nGSJMUn/2uMOAhzy9lrKqVS26fpGYk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/elfBHz/btr2nGSJMUn/2uMOAhzy9lrKqVS26fpGYk/img.jpg&quot; data-alt=&quot;슬랙으로 알림오는 Rate Limit Error&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/elfBHz/btr2nGSJMUn/2uMOAhzy9lrKqVS26fpGYk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FelfBHz%2Fbtr2nGSJMUn%2F2uMOAhzy9lrKqVS26fpGYk%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;668&quot; height=&quot;195&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1304&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;슬랙으로 알림오는 Rate Limit Error&lt;/figcaption&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;api를 공개하게 되면 유저가 마음대로 요청을 할 수 있다.&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;요청량을 제한하는건 ip기반으로 하게되면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서에서 막아줄수도있고 , nginx 에서도 막아 줄 수 있다.&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;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 문제점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. buket4j&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 적용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3.1. Bucket4j 와 레디스 jcache 로 연결하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3.2. ProxyManager 로 버킷 만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 3.3 인터셉터에 적용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 3.3.1. 인터셉터에서 유저정보를 불러올려면? SecurityContextHodler&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 3.3.2 인터셉터에 적용해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제점&lt;/h2&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;b&gt;Redisson&lt;/b&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;Redisson 기반 분산락에는 &lt;b&gt;타임아웃이&lt;/b&gt; 존재하는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;waitTime 이 지나게 되면 락으로 진입 자체를 못하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1678080230974&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;boolean available = rLock.tryLock(waitTime, leaseTime, timeUnit);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락이라는건 공유자원에 접근 할때 한번에 하나 씩 집어넣는것이므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;악성유저가 락을 쓰는 api를 계속 요청할 경우 처리하는데 시간이걸리므로 서버 에러가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;클라이언트가&amp;nbsp; 작업중이다가... 실제로 계속 api 콜하면서 500번대 응답이 엄청 생긴적이있다.&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;그래서 쓰로틀링을 유저 아이디 기반으로 서버 쪽에도 넣어야 할것 같아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Rate Limit&lt;/b&gt; 을 적용하기로 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.&amp;nbsp; bucket4j&lt;/h2&gt;
&lt;figure id=&quot;og_1678080385506&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 - bucket4j/bucket4j: Java rate limiting library based on token-bucket algorithm.&quot; data-og-description=&quot;Java rate limiting library based on token-bucket algorithm. - GitHub - bucket4j/bucket4j: Java rate limiting library based on token-bucket algorithm.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/bucket4j/bucket4j&quot; data-og-url=&quot;https://github.com/bucket4j/bucket4j&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bhW53u/hyRQjUgrAS/B2i4IrDE0n3iLFfhBdEss1/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://github.com/bucket4j/bucket4j&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/bucket4j/bucket4j&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bhW53u/hyRQjUgrAS/B2i4IrDE0n3iLFfhBdEss1/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&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 - bucket4j/bucket4j: Java rate limiting library based on token-bucket algorithm.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Java rate limiting library based on token-bucket algorithm. - GitHub - bucket4j/bucket4j: Java rate limiting library based on token-bucket algorithm.&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;구현 방식에는 여러 방법이 있을것 같은데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 알고리즘 을 이용해서 적용하는 방식이다.&lt;/p&gt;
&lt;figure id=&quot;og_1678080513407&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Rate Limit - step 3] Token Bucket 알고리즘 구현 (rate limiting)&quot; data-og-description=&quot;필자는 현재 API서버를 열심히 개발 하고 있다. 개발한 서비스가 트래픽을 많이 받다보니, 트래픽을 적절히 제한하지 않았을 때 장애가 발생했고, 그에 따라 API의 Rate Limit을 구현해야 했다. 공부&quot; data-og-host=&quot;etloveguitar.tistory.com&quot; data-og-source-url=&quot;https://etloveguitar.tistory.com/128&quot; data-og-url=&quot;https://etloveguitar.tistory.com/128&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cG3zl5/hyRQvf6HGP/TYo4BlO97wbS6T8UjcdQE0/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/co1rqz/hyRQqy4zNz/g4m2nNhLpgQvENzFWQhf61/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/gb4OU/hyRQxkLiie/6aqrjCKIo7g9wHgON1Ptak/img.jpg?width=743&amp;amp;height=682&amp;amp;face=270_154_439_338&quot;&gt;&lt;a href=&quot;https://etloveguitar.tistory.com/128&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://etloveguitar.tistory.com/128&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cG3zl5/hyRQvf6HGP/TYo4BlO97wbS6T8UjcdQE0/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/co1rqz/hyRQqy4zNz/g4m2nNhLpgQvENzFWQhf61/img.png?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512,https://scrap.kakaocdn.net/dn/gb4OU/hyRQxkLiie/6aqrjCKIo7g9wHgON1Ptak/img.jpg?width=743&amp;amp;height=682&amp;amp;face=270_154_439_338');&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;[Rate Limit - step 3] Token Bucket 알고리즘 구현 (rate limiting)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;필자는 현재 API서버를 열심히 개발 하고 있다. 개발한 서비스가 트래픽을 많이 받다보니, 트래픽을 적절히 제한하지 않았을 때 장애가 발생했고, 그에 따라 API의 Rate Limit을 구현해야 했다. 공부&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;etloveguitar.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;figure id=&quot;og_1678080638206&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;High-throughput distributed rate limiter&quot; data-og-description=&quot;Production-grade systems usually consist of multiple interconnected components that depend on each other. Popularization of the microservice architect...&quot; data-og-host=&quot;engineering.linecorp.com&quot; data-og-source-url=&quot;https://engineering.linecorp.com/en/blog/high-throughput-distributed-rate-limiter&quot; data-og-url=&quot;https://engineering.linecorp.com/en/blog/high-throughput-distributed-rate-limiter&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bo7zct/hyRQigNayv/lFf3I2VvfOKM3eb42kjGk0/img.png?width=1024&amp;amp;height=419&amp;amp;face=0_0_1024_419&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/en/blog/high-throughput-distributed-rate-limiter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://engineering.linecorp.com/en/blog/high-throughput-distributed-rate-limiter&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bo7zct/hyRQigNayv/lFf3I2VvfOKM3eb42kjGk0/img.png?width=1024&amp;amp;height=419&amp;amp;face=0_0_1024_419');&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;High-throughput distributed rate limiter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Production-grade systems usually consist of multiple interconnected components that depend on each other. Popularization of the microservice architect...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;engineering.linecorp.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;토큰이 얼마나남아있는지 &lt;b&gt;서버 어플리케이션간에 공유&lt;/b&gt;할 수 있는 구성이였으면 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fygyl/btr1YJRbLl5/P0mb0KYN43QRDTJzlypg7K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fygyl/btr1YJRbLl5/P0mb0KYN43QRDTJzlypg7K/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;380&quot; data-filename=&quot;무제.jpg&quot; width=&quot;657&quot; height=&quot;230&quot; style=&quot;width: 42.6356%; margin-right: 10px;&quot; data-widthpercent=&quot;43.14&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fygyl/btr1YJRbLl5/P0mb0KYN43QRDTJzlypg7K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFygyl%2Fbtr1YJRbLl5%2FP0mb0KYN43QRDTJzlypg7K%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;1086&quot; height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf0gLf/btr114U4H1H/eEeSdsboOzFaBmXf97ON61/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf0gLf/btr114U4H1H/eEeSdsboOzFaBmXf97ON61/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;464&quot; data-filename=&quot;무제.jpg&quot; style=&quot;width: 56.2017%;&quot; data-widthpercent=&quot;56.86&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf0gLf/btr114U4H1H/eEeSdsboOzFaBmXf97ON61/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf0gLf%2Fbtr114U4H1H%2FeEeSdsboOzFaBmXf97ON61%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;1748&quot; height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(https://github.com/redisson/redisson) /&amp;nbsp; (https://github.com/bucket4j/bucket4j)&lt;/figcaption&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;그와중에 &lt;b&gt;jcache api&lt;/b&gt; 사용가능하고 , &lt;b&gt;redis&lt;/b&gt; 연동이 가능한 ( Redisson 이 jcache 를 지원함 ) , &lt;b&gt;bucket4j&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;style2&quot;&gt;&lt;b&gt;즉 우리는 jcache api 를 사용해서 레디스를 토큰을 여러 서버에서 공유할 수 있는 메모리 저장소로 사용할 예정이다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 적용하기&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;3.1. Bucket4j 와 레디스 jcache 로 연결하기&lt;/h3&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;figure id=&quot;og_1678081000830&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;14. Integration with frameworks&quot; data-og-description=&quot;Redisson - Redis Java client with features of In-Memory Data Grid. Over 50 Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/redisson/redisson/wiki/14.-Integration-with-frameworks/#144-jcache-api-jsr-107-implementation&quot; data-og-url=&quot;https://github.com/redisson/redisson/wiki/14.-Integration-with-frameworks&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAHfZ8/hyRQs4MPHN/OiJs5RcHOWECqwVWLlml00/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/redisson/redisson/wiki/14.-Integration-with-frameworks/#144-jcache-api-jsr-107-implementation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/redisson/redisson/wiki/14.-Integration-with-frameworks/#144-jcache-api-jsr-107-implementation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAHfZ8/hyRQs4MPHN/OiJs5RcHOWECqwVWLlml00/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;14. Integration with frameworks&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Redisson - Redis Java client with features of In-Memory Data Grid. Over 50 Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map...&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_1678081023862&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Bucket4j 8.1.1 Reference&quot; data-og-description=&quot;Question: Why does bucket invoke the listener on the client-side instead of the server-side in case of distributed scenario? What do I need to do if I need an aggregated stat across the whole cluster? Answer: Because of a planned expansion to non-JVM back-&quot; data-og-host=&quot;bucket4j.com&quot; data-og-source-url=&quot;https://bucket4j.com/8.1.1/toc.html#bucket4j-jcache&quot; data-og-url=&quot;https://bucket4j.com/8.1.1/toc.html#bucket4j-jcache&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://bucket4j.com/8.1.1/toc.html#bucket4j-jcache&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bucket4j.com/8.1.1/toc.html#bucket4j-jcache&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Bucket4j 8.1.1 Reference&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Question: Why does bucket invoke the listener on the client-side instead of the server-side in case of distributed scenario? What do I need to do if I need an aggregated stat across the whole cluster? Answer: Because of a planned expansion to non-JVM back-&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bucket4j.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/** for bucket4j */
// JCache 의 CacheManager를 생성한다.
@Bean
public CacheManager cacheManager(RedissonClient redissonClient) {
    CacheManager manager = Caching.getCachingProvider().getCacheManager();
    Cache&amp;lt;Object, Object&amp;gt; bucket4j = manager.getCache(&quot;bucket4j&quot;);
    // 테스트 코드에서 깨져서 null check 하고 createCache 를해야함
    if (bucket4j == null) {
        manager.createCache(&quot;bucket4j&quot;, RedissonConfiguration.fromInstance(redissonClient));
    }
    return manager;
}

/** for bucket4j */
@Bean
ProxyManager&amp;lt;String&amp;gt; proxyManager(CacheManager cacheManager) {
    // JCacheProxyManager 를 생성한다.
    return new JCacheProxyManager&amp;lt;&amp;gt;(cacheManager.getCache(&quot;bucket4j&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redisson&lt;/b&gt; 클라이언트가 &lt;b&gt;JCache api&lt;/b&gt; 를 지원하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JCache&lt;/b&gt; 의 &lt;b&gt;ChaceManager&lt;/b&gt;를 만든뒤에 해당 캐시 매니저로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Bucket4j&lt;/b&gt; 의 &lt;b&gt;JCacheProxyManager&lt;/b&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;ProxyManager&amp;lt;String&amp;gt; proxyManger&lt;/b&gt;을 빈으로 등록하고 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스를 캐시 저장소로 활용 하였으니, &lt;b&gt;다중 서버에서도 토큰 여부&lt;/b&gt;를 같이 판단 할 수 있게 되었다.&lt;/p&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;3.2. ProxyManager 로 버킷 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class UserRateLimiter {
    // autowiring dependencies
    private final ProxyManager&amp;lt;String&amp;gt; buckets;

    @Value(&quot;${throttle.overdraft}&quot;)
    private long overdraft;

    @Value(&quot;${throttle.greedyRefill}&quot;)
    private long greedyRefill;

    public Bucket resolveBucket(String key) {
        Supplier&amp;lt;BucketConfiguration&amp;gt; configSupplier = getConfigSupplierForUser();
        return buckets.builder().build(key, configSupplier);  // 1
    }

    private Supplier&amp;lt;BucketConfiguration&amp;gt; getConfigSupplierForUser() {
        Refill refill = Refill.greedy(greedyRefill, Duration.ofMinutes(1));
        Bandwidth limit = Bandwidth.classic(overdraft, refill);
        return () -&amp;gt; (BucketConfiguration.builder().addLimit(limit).build()); //2
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 우린 유저 아이디 기반 &lt;b&gt;Rate Limit&lt;/b&gt; 를 걸길 원하고 있으니 ( 두둥 서버에서는 익명 유저일경우에는 아이피 기반으로 걸어버렸다 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//1&amp;nbsp; 버킷을 유저아이디를 키값으로 만들고, 있으면 리턴을 해준다 build 라고 해서 &lt;b&gt;매번 새로운 버킷&lt;/b&gt;을 만들지 않느다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//2 &lt;b&gt;BucketConfiguration&lt;/b&gt;으로 &lt;b&gt;Bandwith&lt;/b&gt; 를설정한다. 전략이 여러가지가 있다.&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;전략이 더많으니 공식문서에서 확인하길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 인터셉터에 적용하기&lt;/h3&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;3.3.1. 인터셉터에서 유저정보를 불러올려면? SecurityContextHodler&lt;/h4&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;Rate Limit&lt;/b&gt; 을 적용하는데는 정말 자유로울 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Aop&lt;/b&gt; 에 적용해도 되고 , 필터 단에 넣어도 되고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두둥 프로젝트에서는 우선 글로벌하게 &lt;b&gt;rate limit을 걸고 싶고, 유저 아이디 기반으로 걸고 싶어서&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;요청이 들어올때에 &lt;b&gt;시큐리티 context에서&lt;/b&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 두둥의 시큐리티 유틸 클래스 
public class SecurityUtils {
    private static SimpleGrantedAuthority anonymous = new SimpleGrantedAuthority(&quot;ROLE_ANONYMOUS&quot;);
    private static SimpleGrantedAuthority swagger = new SimpleGrantedAuthority(&quot;ROLE_SWAGGER&quot;);

    private static List&amp;lt;SimpleGrantedAuthority&amp;gt; notUserAuthority = List.of(anonymous, swagger);

    public static Long getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            throw SecurityContextNotFoundException.EXCEPTION;
        }
        if (authentication.isAuthenticated()
                &amp;amp;&amp;amp; !CollectionUtils.containsAny(
                        authentication.getAuthorities(), notUserAuthority)) {
            return Long.valueOf(authentication.getName());
        }
        // 스웨거 유저일시 익명 유저 취급
        // 익명유저시 userId 0 반환
        return 0L;
    }
}&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;쓰레드 로컬&lt;/b&gt;에 인증 관련정보가 저장되어서 요청마다 &lt;b&gt;SecurityContextHodler&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(익명 유저는 비 인증 api 인 경우 시큐리티 필터를 통과하면 익명 유저이다 )&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익명유저와 스웨거 유저( 스웨거에 비밀번호를 걸기위해서 Basic auth 를 설정한 상태임 ) 이면 유저아이디 0을 리턴하도록 했다.&lt;/p&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;3.3.2 인터셉터에 적용해보자.&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class ThrottlingInterceptor implements HandlerInterceptor {

    private final UserRateLimiter userRateLimiter;
    private final IPRateLimiter ipRateLimiter;
    private final ObjectMapper objectMapper;

    private final SlackThrottleErrorSender slackThrottleErrorSender;

    @Override
    public boolean preHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler)
            throws IOException {
        Long userId = SecurityUtils.getCurrentUserId();
        Bucket bucket;
        if (userId == 0L) {
            // 익명 유저 ip 기반처리
            String remoteAddr = request.getRemoteAddr();
            bucket = ipRateLimiter.resolveBucket(remoteAddr);
        } else {
            // 비 익명 유저 유저 아이디 기반 처리
            bucket = userRateLimiter.resolveBucket(userId.toString());
        }

        if (bucket.tryConsume(1)) {
            return true;
        }
        // 슬랙 알림 메시지 발송.
        // limit is exceeded
        ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
        slackThrottleErrorSender.execute(cachingRequest, userId);
        responseTooManyRequestError(request, response);

        return false;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익명 유저와 스웨거 유저일 경우 ip 기반으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비 익명 유저일 경우 유저 아이디 기반으로 Rate Limit 을 적용했다.&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;익명 유저는 비인증 api 일경우 시큐리티는 익명유저로판단한다.&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;이글에서 중요한점은 redis client인 redisson 이 jcahce api를 지원한다는점,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이를 이용해서 bucket4j 를 연동 할 수 있다는점.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;redis 를 활용해서 토큰의 공유 저장소로 활용해서 분산 서버에 적용가능하다는 점.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1678082488348&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/rateLimit/ThrottlingInterceptor.java&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bwfMGf/hyRQwsAjsl/Wv7rR4xuddPJtOoFXwOOf0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/rateLimit/ThrottlingInterceptor.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/rateLimit/ThrottlingInterceptor.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bwfMGf/hyRQwsAjsl/Wv7rR4xuddPJtOoFXwOOf0/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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;이글의 소스는 위 레포지토레이서 참고 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 서버에서도 Rate Limit 처리를 안해도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;aws로드밸런서나 nginx에도 처리가 가능하니,&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;</description>
      <category>스프링</category>
      <category>RateLimit</category>
      <category>스프링</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/31</guid>
      <comments>https://devnm.tistory.com/31#entry31comment</comments>
      <pubDate>Mon, 6 Mar 2023 15:06:19 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] spring 프록시 환경에서 HttpContentCache 적용</title>
      <link>https://devnm.tistory.com/30</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c42cw1/btr1URnVe8f/9KGlmqYLzEuwmfYS8r6RA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c42cw1/btr1URnVe8f/9KGlmqYLzEuwmfYS8r6RA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c42cw1/btr1URnVe8f/9KGlmqYLzEuwmfYS8r6RA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc42cw1%2Fbtr1URnVe8f%2F9KGlmqYLzEuwmfYS8r6RA0%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;520&quot; height=&quot;307&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;756&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;두둥 백엔드에서는 에러로그를 &lt;b&gt;cloudWatch&lt;/b&gt; 로도 전송하면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간으로 500번대 알림을 받아보기 위해서 슬랙으로 비동기적으로 에러를 전송하고 있다.&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;body&lt;/b&gt;의 값을 볼 수 없어서&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1678032140640&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ContentCachingRequestWrapper (Spring Framework 6.0.6 API)&quot; data-og-description=&quot;handleContentOverflow protected&amp;nbsp;void&amp;nbsp;handleContentOverflow(int&amp;nbsp;contentCacheLimit) Template method for handling a content overflow: specifically, a request body being read that exceeds the specified content cache limit. The default implementation is empt&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingRequestWrapper.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingRequestWrapper.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingRequestWrapper.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingRequestWrapper.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;ContentCachingRequestWrapper (Spring Framework 6.0.6 API)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;handleContentOverflow protected&amp;nbsp;void&amp;nbsp;handleContentOverflow(int&amp;nbsp;contentCacheLimit) Template method for handling a content overflow: specifically, a request body being read that exceeds the specified content cache limit. The default implementation is empt&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 &lt;b&gt;ContentCachingRequest/ResponseWrapper&lt;/b&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 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;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Component
public class HttpContentCacheFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper wrappingResponse =
                new ContentCachingResponseWrapper(response);

        chain.doFilter(wrappingRequest, wrappingResponse);

        wrappingResponse.copyBodyToResponse();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 프록시 환경에서는 필터의 순서 때문에 필요한곳에서 &lt;b&gt;ContentCachingRequestWrapper&lt;/b&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;figure id=&quot;og_1678032661990&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;fix : x-forward 필터로 인한 ContentCachingRequestWrapper 캐스팅 오류 해결 by ImNM &amp;middot; Pull Request #267 &amp;middot; Gosrock/Du&quot; data-og-description=&quot;개요 close #266 작업사항 500 번대 오류시 슬랙 알림이 서버에서 안오더라고요? 보니깐 proxy 로 nginx 달려있으니 x-Forward 헤더 가 넘어오는데 이때 ForwardedHeaderFilter가 동작해 버리더라고요 위 필터가 &quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/pull/267&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend/pull/267&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cHrkdJ/hyRQrc7kPF/cEp5XTF7K0FVzhyz8f3H8K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/pull/267&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/pull/267&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cHrkdJ/hyRQrc7kPF/cEp5XTF7K0FVzhyz8f3H8K/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;fix : x-forward 필터로 인한 ContentCachingRequestWrapper 캐스팅 오류 해결 by ImNM &amp;middot; Pull Request #267 &amp;middot; Gosrock/Du&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 close #266 작업사항 500 번대 오류시 슬랙 알림이 서버에서 안오더라고요? 보니깐 proxy 로 nginx 달려있으니 x-Forward 헤더 가 넘어오는데 이때 ForwardedHeaderFilter가 동작해 버리더라고요 위 필터가&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;이 글 내용은 위 &lt;b&gt;pr&lt;/b&gt;을 기반으로 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 문제점&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;&amp;nbsp; 2.1. 스프링 시큐리티 처럼 필터 조정하면되는거아니야?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 2.2. 오더로 조정하기&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제점&lt;/h2&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러를 슬랙으로 바디 정보를 함께 전송할려면 &lt;b&gt;GlobalExceptionHandler&lt;/b&gt; 에서&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@ExceptionHandler(Exception.class)
protected ResponseEntity&amp;lt;ErrorResponse&amp;gt; handleException(Exception e, HttpServletRequest request)
        throws IOException {
    final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스적으로 처리하지못한 500번대인 &lt;b&gt;Exception&lt;/b&gt; 을 잡아서 처리해야할때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HttpServletRequest&lt;/b&gt; 를 다시 &lt;b&gt;ContentCachingRequestWrapper&lt;/b&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;하지만 두둥서버는 &lt;b&gt;nginx&lt;/b&gt; 안에서 &lt;b&gt;프록시&lt;/b&gt; 되어있는 환경이였는데, 어느날&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;java.lang.ClassCastException: 
class org.springframework.web.filter.ForwardedHeaderFilter$ForwardedHeaderExtractingRequest 
cannot be cast to class org.springframework.web.util.ContentCachingRequestWrapper&lt;/code&gt;&lt;/pre&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;ForwardedHeaderExtractionRequest&lt;/b&gt; 로 &lt;b&gt;request&lt;/b&gt;가 넘어와서 캐스팅 할 수 없었던 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ForwardedHeaderFilter&lt;/b&gt;를 찾아가서 발동 조건을 살펴보았다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// FORWARDED_HEADER_NAMES
FORWARDED_HEADER_NAMES.add(&quot;Forwarded&quot;);
FORWARDED_HEADER_NAMES.add(&quot;X-Forwarded-Host&quot;);
FORWARDED_HEADER_NAMES.add(&quot;X-Forwarded-Port&quot;);
FORWARDED_HEADER_NAMES.add(&quot;X-Forwarded-Proto&quot;);
FORWARDED_HEADER_NAMES.add(&quot;X-Forwarded-Prefix&quot;);
FORWARDED_HEADER_NAMES.add(&quot;X-Forwarded-Ssl&quot;);
FORWARDED_HEADER_NAMES.add(&quot;X-Forwarded-For&quot;);

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
   for (String headerName : FORWARDED_HEADER_NAMES) {
      if (request.getHeader(headerName) != null) {
         return false;
      }
   }
   return true;
}&lt;/code&gt;&lt;/pre&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;그럼 nginx는? 요청을 &lt;b&gt;upstream server&lt;/b&gt;로 넘겨줄 때 어떤 헤더를 넘겨주길래 저렇게 작동할까. 봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1788&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EJR8J/btr1Ut8VqMg/wuMGOMWasdJRgtSzRRNBPK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EJR8J/btr1Ut8VqMg/wuMGOMWasdJRgtSzRRNBPK/img.jpg&quot; data-alt=&quot;(https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EJR8J/btr1Ut8VqMg/wuMGOMWasdJRgtSzRRNBPK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEJR8J%2Fbtr1Ut8VqMg%2FwuMGOMWasdJRgtSzRRNBPK%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;1788&quot; height=&quot;502&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1788&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;X- 종류말고도 , remote_addr 등의 정보를 헤더에 담아 upstream으로 넘겨주는것을&amp;nbsp; 알 수 있다.&lt;/b&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;위와같은 조건들로 인해서. ForwardedHeaderFilter 가 작동하게 된것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서는 작동하지 않았던 ForwardedHeaderFilter 가 작동하게되면서 캐스팅오류가 난것을 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;그럼 , 캐스팅 오류는 왜 일어난게 된걸까?&lt;/b&gt;&lt;/blockquote&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;서블릿필터정보.jpg&quot; data-origin-width=&quot;2438&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdhX2E/btr16kP4zVg/0I0ePn2uShpivUlepjmGAk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdhX2E/btr16kP4zVg/0I0ePn2uShpivUlepjmGAk/img.jpg&quot; data-alt=&quot;서블릿 필터체인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdhX2E/btr16kP4zVg/0I0ePn2uShpivUlepjmGAk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdhX2E%2Fbtr16kP4zVg%2F0I0ePn2uShpivUlepjmGAk%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;892&quot; height=&quot;199&quot; data-filename=&quot;서블릿필터정보.jpg&quot; data-origin-width=&quot;2438&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서블릿 필터체인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만들었던 &lt;b&gt;HttpContentCacheFilter&lt;/b&gt;가 &lt;b&gt;ForwardedHeaderFilter&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;ForwardedHeaderFilter&lt;/b&gt; 필터가 &lt;b&gt;HttpContentCacheFilter&lt;/b&gt; 보다 앞에오게하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 에러 핸들어에서 넘어오는 HttpRequest는 &lt;b&gt;ContentCachingRequestWrapper&lt;/b&gt; 타입일것이 명확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 개선하기&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;2.1. 스프링 시큐리티 처럼 필터 조정하면되는거아니야?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연스럽게... 생각할수 있을것같다. 나도 그랬다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Component
public class FilterConfig
        extends SecurityConfigurerAdapter&amp;lt;DefaultSecurityFilterChain, HttpSecurity&amp;gt; {

    private final JwtTokenFilter jwtTokenFilter;
    private final JwtExceptionFilter jwtExceptionFilter;
    private final HttpContentCacheFilter httpContentCacheFilter;

    @Override
    public void configure(HttpSecurity builder) {
        builder.addFilterBefore(jwtTokenFilter, BasicAuthenticationFilter.class);
        builder.addFilterBefore(jwtExceptionFilter, JwtTokenFilter.class);
        // ForwardedHeaderFilter뒤에 놓으면되네!
        builder.addFilterBefore(httpContentCacheFilter, ForwardedHeaderFilter.class);
    }
}&lt;/code&gt;&lt;/pre&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;서블릿필터정보.jpg&quot; data-origin-width=&quot;2438&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdhX2E/btr16kP4zVg/0I0ePn2uShpivUlepjmGAk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdhX2E/btr16kP4zVg/0I0ePn2uShpivUlepjmGAk/img.jpg&quot; data-alt=&quot;서블릿 필터체인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdhX2E/btr16kP4zVg/0I0ePn2uShpivUlepjmGAk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdhX2E%2Fbtr16kP4zVg%2F0I0ePn2uShpivUlepjmGAk%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;892&quot; height=&quot;199&quot; data-filename=&quot;서블릿필터정보.jpg&quot; data-origin-width=&quot;2438&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서블릿 필터체인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위 사진의 형광색 놓인 부분이 보이는가? &lt;b&gt;DelegatingFilterProxy&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4qH5w/btr2m2Vd2Nk/cw1riCmnurf1HGQxfYXYo0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4qH5w/btr2m2Vd2Nk/cw1riCmnurf1HGQxfYXYo0/img.jpg&quot; data-alt=&quot;FilterChainProxy$VirtualFilterChain&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4qH5w/btr2m2Vd2Nk/cw1riCmnurf1HGQxfYXYo0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4qH5w%2Fbtr2m2Vd2Nk%2Fcw1riCmnurf1HGQxfYXYo0%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;429&quot; height=&quot;330&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FilterChainProxy$VirtualFilterChain&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3번의 서블릿 필터를 거치면서 -&amp;gt; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;바로 위사진의 스프링 시큐리티의 로직이 수행되고 -&amp;gt; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4번의 필터로 돌아와서 수행된다.&lt;/b&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ForwardedHeaderFilter&lt;/b&gt;은 서블릿의 필터다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 우린.. 다른 방법을 알아봐야한다.&lt;/p&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;2.2 오더로 적용하기&lt;/h3&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;@Order&lt;/b&gt; 라는 어노테이션을 활용하는 방법이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Order(Integer.MAX_VALUE)
@Component
public class HttpContentCacheFilter extends OncePerRequestFilter {&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방식으로 Order을 지정하면 끝난다. 가아니라 이것도 안된다..&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;figure id=&quot;og_1678034384436&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Filter order in spring-boot&quot; data-og-description=&quot;How can I specify order of my Filter in spring-boot? I need to insert my MDC filter after Spring Security filter. I tried almost everything but my filter was always first. This didn't work: @Bean ...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/25957879/filter-order-in-spring-boot&quot; data-og-url=&quot;https://stackoverflow.com/questions/25957879/filter-order-in-spring-boot&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LAi7G/hyRQhPaONz/fQwHHsTPYvkTYI9kWJ1A8K/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/25957879/filter-order-in-spring-boot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/25957879/filter-order-in-spring-boot&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LAi7G/hyRQhPaONz/fQwHHsTPYvkTYI9kWJ1A8K/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;Filter order in spring-boot&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;How can I specify order of my Filter in spring-boot? I need to insert my MDC filter after Spring Security filter. I tried almost everything but my filter was always first. This didn't work: @Bean ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 위에서 찾아볼 수 있다.&lt;br /&gt;스프링 시큐리티도, ForwardedHeaderFilter도 아무 순서를 지정하지 않아서, 맨 뒤로 배정이 된다.&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;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
@Profile({&quot;prod&quot;, &quot;staging&quot;, &quot;dev&quot;})
public class ServletFilterConfig implements WebMvcConfigurer {

    private final HttpContentCacheFilter httpContentCacheFilter;
    private final ForwardedHeaderFilter forwardedHeaderFilter;

    @Bean
    public FilterRegistrationBean securityFilterChain(
            @Qualifier(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
                    Filter securityFilter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(securityFilter);
        registration.setOrder(Integer.MAX_VALUE - 3);
        registration.setName(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
        return registration;
    }

    @Bean
    public FilterRegistrationBean setResourceUrlEncodingFilter() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new ResourceUrlEncodingFilter());
        registrationBean.setOrder(Integer.MAX_VALUE - 2);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean setForwardedHeaderFilterOrder() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(forwardedHeaderFilter);
        registrationBean.setOrder(Integer.MAX_VALUE - 1);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean setHttpContentCacheFilterOrder() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(httpContentCacheFilter);
        registrationBean.setOrder(Integer.MAX_VALUE);
        return registrationBean;
    }
}&lt;/code&gt;&lt;/pre&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;무제.jpg&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIARnm/btr1QxcrBzm/EXAUvTS2Ua468q58HlIks1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIARnm/btr1QxcrBzm/EXAUvTS2Ua468q58HlIks1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIARnm/btr1QxcrBzm/EXAUvTS2Ua468q58HlIks1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIARnm%2Fbtr1QxcrBzm%2FEXAUvTS2Ua468q58HlIks1%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;620&quot; height=&quot;259&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;648&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 data-ke-size=&quot;size16&quot;&gt;이로써, 프록시 환경에서 &lt;b&gt;ContentCache&lt;/b&gt; 필터를 적용하는 방법을 찾아내서 적용시켰다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에 7,8,9번도 사실 내가 커스텀해서 적용한 필터들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Component 어노테이션으로 빈으로 등록해서 사용중인 컴포넌트들인데,&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;OnecPerRequestFilter&lt;/b&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;이걸 보니 시큐리티 FilterConfig 에서 di를 받을게 아니라 생성자로 생성해야하나? 의문점이 들긴하다.&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;figure id=&quot;og_1678035021435&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/ServletFilterConfig.java&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Mu1Oj/hyRQkkNITr/9uvW313DwKlXg0FcBOKEOk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/ServletFilterConfig.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/ServletFilterConfig.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Mu1Oj/hyRQkkNITr/9uvW313DwKlXg0FcBOKEOk/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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>스프링</category>
      <category>서블릿</category>
      <category>스프링</category>
      <category>시큐리티</category>
      <category>필터</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/30</guid>
      <comments>https://devnm.tistory.com/30#entry30comment</comments>
      <pubDate>Mon, 6 Mar 2023 01:54:38 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] spring swagger 같은 코드 여러 에러 응답 예시 만들기</title>
      <link>https://devnm.tistory.com/29</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 기록 2023-03-05 오후 10.16.51.gif&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbUxox/btr1XyuOVZX/ZQr6PD0qupFGRkDHtUgZf0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbUxox/btr1XyuOVZX/ZQr6PD0qupFGRkDHtUgZf0/img.gif&quot; data-alt=&quot;스웨거 같은코드 여러 에러응답 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbUxox/btr1XyuOVZX/ZQr6PD0qupFGRkDHtUgZf0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cbUxox/btr1XyuOVZX/ZQr6PD0qupFGRkDHtUgZf0/img.gif&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;689&quot; height=&quot;225&quot; data-filename=&quot;화면 기록 2023-03-05 오후 10.16.51.gif&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스웨거 같은코드 여러 에러응답 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1678022358175&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[스프링] error code 도메인 별 분리하기&quot; data-og-description=&quot;두둥 프로젝트에서는 처리중에 에러가 발생할경우 RuntimeException 을 상속받은 DuDoongException 에서 다시 상속받아서 코드별 에러클래스를 만들고 있다. @Getter @AllArgsConstructor public class DuDoongCodeExceptio&quot; data-og-host=&quot;devnm.tistory.com&quot; data-og-source-url=&quot;https://devnm.tistory.com/27&quot; data-og-url=&quot;https://devnm.tistory.com/27&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nUZsR/hyRQrYmxKj/NEGfm7aSoIzQ6bxc4iZfz1/img.jpg?width=956&amp;amp;height=674&amp;amp;face=0_0_956_674&quot;&gt;&lt;a href=&quot;https://devnm.tistory.com/27&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devnm.tistory.com/27&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nUZsR/hyRQrYmxKj/NEGfm7aSoIzQ6bxc4iZfz1/img.jpg?width=956&amp;amp;height=674&amp;amp;face=0_0_956_674');&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;[스프링] error code 도메인 별 분리하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;두둥 프로젝트에서는 처리중에 에러가 발생할경우 RuntimeException 을 상속받은 DuDoongException 에서 다시 상속받아서 코드별 에러클래스를 만들고 있다. @Getter @AllArgsConstructor public class DuDoongCodeExceptio&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devnm.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서는 error code를 도메인별로 분할하는 작업을 진행했었다.&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;무제.jpg&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctjOuj/btr1UuNst0w/1PRPNSWKdk4IwTdwyY76B0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctjOuj/btr1UuNst0w/1PRPNSWKdk4IwTdwyY76B0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctjOuj/btr1UuNst0w/1PRPNSWKdk4IwTdwyY76B0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctjOuj%2Fbtr1UuNst0w%2F1PRPNSWKdk4IwTdwyY76B0%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;705&quot; height=&quot;155&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;342&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;공유하고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 문제점&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;3. 스웨거 타입 분석&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 적용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 4.1. 커스텀 어노테이션 생성&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 4.2. 커스텀 어노테이션 정보를 가져오기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 4.3. 스웨거 예시 응답값 커스텀&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직 처리중에 에러를 내뱉게되어서 400번대로 응답을 해주게 되면, 클라이언트도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 api 를 사용할 때 어떤 때 해당 오류가 나는것인지 알아야 될 때가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678023220981&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ApiResponses(
      value = {
           @ApiResponse(
               responseCode = &quot;201&quot;,
               description = &quot;이전까지 회원가입을 하지 않았던 경우&quot;,
               content = @Content(
                   schema = @Schema(implementation = AfterOauthResponse.class)))
                     
           @ApiResponse(
               responseCode = &quot;200&quot;,
               description = &quot;이미 회원가입을 했던 유저인 경우&quot;,
               content = @Content(
                    schema = @Schema(implementation = AfterOauthResponse.class)))
})&lt;/code&gt;&lt;/pre&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;에러 코드를 enum 으로 기술을 했음에도,&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;/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;스웨거로 보여주는 방식을 고민해 보았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 에러 코드를 일일히 적지 않고 어떻게 옮길 수 있을까.&lt;/h2&gt;
&lt;figure id=&quot;og_1678023872046&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[스프링] spring swagger api 하나만 인증 풀기&quot; data-og-description=&quot;두둥 서비스 백엔드에서는 api 문서로 open api swagger를 사용중이다. 스웨거를 조금 커스텀하게 하면, 덕지덕지 붙는 어노테이션의 양을 줄일 수 있는데, 커스텀 어노테이션을 만들어서 리플랙션으&quot; data-og-host=&quot;devnm.tistory.com&quot; data-og-source-url=&quot;https://devnm.tistory.com/26&quot; data-og-url=&quot;https://devnm.tistory.com/26&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bTFieh/hyRQrqyKCf/I2arHO6OsFIoAgLwgyXHak/img.jpg?width=800&amp;amp;height=402&amp;amp;face=0_0_800_402,https://scrap.kakaocdn.net/dn/trrcq/hyRQtu8Br2/zkuRzxZHo5m1deSPvhgv00/img.jpg?width=800&amp;amp;height=402&amp;amp;face=0_0_800_402,https://scrap.kakaocdn.net/dn/l7rxA/hyRQrqyKBn/a0Z7oWHFGKPuY4DW7iuOU0/img.jpg?width=1454&amp;amp;height=240&amp;amp;face=0_0_1454_240&quot;&gt;&lt;a href=&quot;https://devnm.tistory.com/26&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devnm.tistory.com/26&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bTFieh/hyRQrqyKCf/I2arHO6OsFIoAgLwgyXHak/img.jpg?width=800&amp;amp;height=402&amp;amp;face=0_0_800_402,https://scrap.kakaocdn.net/dn/trrcq/hyRQtu8Br2/zkuRzxZHo5m1deSPvhgv00/img.jpg?width=800&amp;amp;height=402&amp;amp;face=0_0_800_402,https://scrap.kakaocdn.net/dn/l7rxA/hyRQrqyKBn/a0Z7oWHFGKPuY4DW7iuOU0/img.jpg?width=1454&amp;amp;height=240&amp;amp;face=0_0_1454_240');&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;[스프링] spring swagger api 하나만 인증 풀기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;두둥 서비스 백엔드에서는 api 문서로 open api swagger를 사용중이다. 스웨거를 조금 커스텀하게 하면, 덕지덕지 붙는 어노테이션의 양을 줄일 수 있는데, 커스텀 어노테이션을 만들어서 리플랙션으&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devnm.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 글에선, &lt;b&gt;어노테이션&lt;/b&gt;과 &lt;b&gt;리플렉션&lt;/b&gt;을 사용해서 스웨거로 보내질 정보인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Operation&lt;/b&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;별도의&lt;b&gt; example/{domain} 형식의 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OperationCustomizer&lt;/b&gt; 에서 해당 커스텀 어노테이션이 붙은 api라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Operation&lt;/b&gt; 정보에 응답 코드와 그 예시들을 보여줄 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 스웨거 타입 분석&lt;/h2&gt;
&lt;figure id=&quot;og_1678024414958&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 - OAI/OpenAPI-Specification: The OpenAPI Specification Repository&quot; data-og-description=&quot;The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#response-object&quot; data-og-url=&quot;https://github.com/OAI/OpenAPI-Specification&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#response-object&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#response-object&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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 - OAI/OpenAPI-Specification: The OpenAPI Specification Repository&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification 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;&lt;b&gt;Responses&lt;/b&gt; 안에 있는 &lt;b&gt;Response&lt;/b&gt; 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에 의하면 &lt;b&gt;Responses 클래스&lt;/b&gt; 안에는 &lt;b&gt;Response 클래스&lt;/b&gt; 형식을 가지는 필드들이있다.&lt;/p&gt;
&lt;pre id=&quot;code_1678024558046&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Operation 중 responses 필드
&quot;responses&quot;: {
    &quot;200&quot;: { // Response
      &quot;description&quot;: &quot;Pet updated.&quot;,
      &quot;content&quot;: {
        &quot;application/json&quot;: {},
        &quot;application/xml&quot;: {}
      }
    },
    &quot;405&quot;: { // Response
      &quot;description&quot;: &quot;Method Not Allowed&quot;,
      &quot;content&quot;: {
        &quot;application/json&quot;: {},
        &quot;application/xml&quot;: {}
      }
    }
  },&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;200&quot; : &amp;lt;Response&amp;gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;400&quot; : &amp;lt;Response&amp;gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 형식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;content 안에 application/json 형식안에 있는 객체는 &lt;b&gt;Media Type Object&lt;/b&gt; 이다&lt;/p&gt;
&lt;figure id=&quot;og_1678026131631&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 - OAI/OpenAPI-Specification: The OpenAPI Specification Repository&quot; data-og-description=&quot;The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#media-type-object&quot; data-og-url=&quot;https://github.com/OAI/OpenAPI-Specification&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cZUhoN/hyRQsptm8N/R1bYYmEHpbTOKeJxwifjaK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#media-type-object&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#media-type-object&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cZUhoN/hyRQsptm8N/R1bYYmEHpbTOKeJxwifjaK/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 - OAI/OpenAPI-Specification: The OpenAPI Specification Repository&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification 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;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// Media Type Object
{
  &quot;application/json&quot;: {
    &quot;schema&quot;: {
         &quot;$ref&quot;: &quot;#/components/schemas/Pet&quot;
    },
    &quot;examples&quot;: {
      &quot;cat&quot; : {
        &quot;summary&quot;: &quot;An example of a cat&quot;,
        &quot;value&quot;: 
          {
            &quot;name&quot;: &quot;Fluffy&quot;,
            &quot;petType&quot;: &quot;Cat&quot;,
            &quot;color&quot;: &quot;White&quot;,
            &quot;gender&quot;: &quot;male&quot;,
            &quot;breed&quot;: &quot;Persian&quot;
          }
      },
      &quot;dog&quot;: {
        &quot;summary&quot;: &quot;An example of a dog with a cat's name&quot;,
        &quot;value&quot; :  { 
          &quot;name&quot;: &quot;Puma&quot;,
          &quot;petType&quot;: &quot;Dog&quot;,
          &quot;color&quot;: &quot;Black&quot;,
          &quot;gender&quot;: &quot;Female&quot;,
          &quot;breed&quot;: &quot;Mixed&quot;
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Media Type Objec&lt;/b&gt;t 안에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;examplse&lt;/b&gt; 안에는 &lt;b&gt;Example Object&lt;/b&gt; 가 올수 있는데&lt;/p&gt;
&lt;figure id=&quot;og_1678026296406&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 - OAI/OpenAPI-Specification: The OpenAPI Specification Repository&quot; data-og-description=&quot;The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#example-object&quot; data-og-url=&quot;https://github.com/OAI/OpenAPI-Specification&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Btgca/hyRQpfeC2E/ysTkwCWhE5yhEQQGYO2EzK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#example-object&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#example-object&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Btgca/hyRQpfeC2E/ysTkwCWhE5yhEQQGYO2EzK/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 - OAI/OpenAPI-Specification: The OpenAPI Specification Repository&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification 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;우린 summary 와, value 값을 도메인별&amp;nbsp;&lt;b&gt;ErrorCode enum&lt;/b&gt; 에서 가져와서 예시로 적어줄 것이다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGTnTx/btr1QyCrSUj/ly6RUXaJXt0DcBufrSJEr1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGTnTx/btr1QyCrSUj/ly6RUXaJXt0DcBufrSJEr1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;720&quot; data-filename=&quot;무제.jpg&quot; style=&quot;width: 65.9062%; margin-right: 10px;&quot; data-widthpercent=&quot;66.68&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGTnTx/btr1QyCrSUj/ly6RUXaJXt0DcBufrSJEr1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGTnTx%2Fbtr1QyCrSUj%2Fly6RUXaJXt0DcBufrSJEr1%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;1686&quot; height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6lQba/btr1UtA3zQc/JtZrs3mJXUguaKtcewVm21/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6lQba/btr1UtA3zQc/JtZrs3mJXUguaKtcewVm21/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;788&quot; data-filename=&quot;무제 2.jpg&quot; style=&quot;width: 32.9311%;&quot; data-widthpercent=&quot;33.32&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6lQba/btr1UtA3zQc/JtZrs3mJXUguaKtcewVm21/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6lQba%2Fbtr1UtA3zQc%2FJtZrs3mJXUguaKtcewVm21%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;922&quot; height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진을 보면 좀더 이해하기에 쉬울것같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 적용하기&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;4.1. 커스텀 어노테이션 생성&lt;/h3&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 class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorCodeExample {
    Class&amp;lt;? extends BaseErrorCode&amp;gt; value();
}
//ex
@ApiErrorCodeExample(UserErrorCode.class)
public void getUserErrorCode() {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiErrorCodeExample&lt;/b&gt; 어노테이션을 만들고, 안에 value는 &lt;b&gt;BassErrorCode&lt;/b&gt; 를 확장시킨 타입의 Class만 받도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용시킨모습은 위의 예시 와 같다.&lt;/p&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;4.2. 커스텀 어노테이션 정보를 가져오기&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;//SwaggerConfig.java
@Bean
public OperationCustomizer customize() {
    return (Operation operation, HandlerMethod handlerMethod) -&amp;gt; {
        ApiErrorCodeExample apiErrorCodeExample =
                handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class);
        // ApiErrorCodeExample 어노테이션 단 메소드 적용
        if (apiErrorCodeExample != null) {
            generateErrorCodeResponseExample(operation, apiErrorCodeExample.value());
        }
        return operation;
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 리플랙션을 사용해서 해당메서드의 &lt;b&gt;ApiErrorCodeExample&lt;/b&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;apiErrorCodeExample.value()&lt;/b&gt; 로 &lt;b&gt;BaseErrorCode&lt;/b&gt; 타입의 예시를 가져올 수 있다는 점이다.&lt;/p&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;4.3. 스웨거 예시 응답값 커스텀&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private void generateErrorCodeResponseExample(
        Operation operation, Class&amp;lt;? extends BaseErrorCode&amp;gt; type) {
    ApiResponses responses = operation.getResponses();
	// 해당 이넘에 선언된 에러코드들의 목록을 가져옵니다.
    BaseErrorCode[] errorCodes = type.getEnumConstants();
	// 400, 401, 404 등 에러코드의 상태코드들로 리스트로 모읍니다.
    // 400 같은 상태코드에 여러 에러코드들이 있을 수 있습니다.
    Map&amp;lt;Integer, List&amp;lt;ExampleHolder&amp;gt;&amp;gt; statusWithExampleHolders =
            Arrays.stream(errorCodes)
                    .map(
                            baseErrorCode -&amp;gt; {
                                try {
                                    ErrorReason errorReason = baseErrorCode.getErrorReason();
                                    return ExampleHolder.builder()
                                            .holder(
                                                    getSwaggerExample(
                                                            baseErrorCode.getExplainError(),
                                                            errorReason))
                                            .code(errorReason.getStatus())
                                            .name(errorReason.getCode())
                                            .build();
                                } catch (NoSuchFieldException e) {
                                    throw new RuntimeException(e);
                                }
                            })
                    .collect(groupingBy(ExampleHolder::getCode));
	// response 객체들을 responses 에 넣습니다.
    addExamplesToResponses(responses, statusWithExampleHolders);
}
//ExampleHolder
@Getter
@Builder
public class ExampleHolder {
	// 스웨거의 Example 객체입니다. 위 스웨거 분석의 Example Object 참고.
    private Example holder;
    private String name;
    private int code;
}
//
private Example getSwaggerExample(String value, ErrorReason errorReason) {
//ErrorResponse 는 클라이언트한 실제 응답하는 공통 에러 응답 객체입니다.
    ErrorResponse errorResponse = new ErrorResponse(errorReason, &quot;요청시 패스정보입니다.&quot;);
    Example example = new Example();
    example.description(value);
    example.setValue(errorResponse);
    return example;
}&lt;/code&gt;&lt;/pre&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;400번대 상태코드에는 여러개의 Example Object 가 올수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private void addExamplesToResponses(
        ApiResponses responses, Map&amp;lt;Integer, List&amp;lt;ExampleHolder&amp;gt;&amp;gt; statusWithExampleHolders) {
    statusWithExampleHolders.forEach(
            (status, v) -&amp;gt; {
                Content content = new Content();
                MediaType mediaType = new MediaType();
                // 상태 코드마다 ApiResponse을 생성합니다. 
                ApiResponse apiResponse = new ApiResponse();
                //  List&amp;lt;ExampleHolder&amp;gt; 를 순회하며, mediaType 객체에 예시값을 추가합니다.
                v.forEach(
                        exampleHolder -&amp;gt; mediaType.addExamples(
                                exampleHolder.getName(), exampleHolder.getHolder()));
                // ApiResponse 의 content 에 mediaType을 추가합니다.
                content.addMediaType(&quot;application/json&quot;, mediaType);
                apiResponse.setContent(content);
                // 상태코드를 key 값으로 responses 에 추가합니다.
                responses.addApiResponse(status.toString(), apiResponse);
            });
}&lt;/code&gt;&lt;/pre&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;ApiResponses&lt;/b&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DkQP2/btr1QwR7D8h/xKIe5G8cWKHPCmzk75KLd1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DkQP2/btr1QwR7D8h/xKIe5G8cWKHPCmzk75KLd1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DkQP2/btr1QwR7D8h/xKIe5G8cWKHPCmzk75KLd1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDkQP2%2Fbtr1QwR7D8h%2FxKIe5G8cWKHPCmzk75KLd1%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;1454&quot; height=&quot;238&quot; data-filename=&quot;무제.jpg&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 설명을 더 적어주고 싶을땐, &lt;b&gt;ExplainError&lt;/b&gt; 라는 커스텀 어노테이션을 만들어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 예시 설명을 적어두고 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public enum UserErrorCode implements BaseErrorCode {
    @ExplainError(&quot;회원가입시에 이미 회원가입한 유저일시 발생하는 오류. 회원가입전엔 항상 register valid check 를 해주세요&quot;)
    USER_ALREADY_SIGNUP(BAD_REQUEST, &quot;USER_400_1&quot;, &quot;이미 회원가입한 유저입니다.&quot;),
    @Override
    public String getExplainError() throws NoSuchFieldException {
        Field field = this.getClass().getField(this.name());
        ExplainError annotation = field.getAnnotation(ExplainError.class);
        return Objects.nonNull(annotation) ? annotation.value() : this.getReason();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&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;스웨거는 공통의 json 형식&lt;/b&gt;을 받아서 렌더링을 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이면, 스프링. nestjs면 nestjs 결국 해당 프레임워크에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 json 형식을 만들어 주냐에 따라서&amp;nbsp;달라질 뿐이지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;view에서 받는 정보는 다 같다.&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;figure id=&quot;og_1678028287867&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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&quot; data-og-description=&quot;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/SwaggerConfig.java&quot; data-og-url=&quot;https://github.com/Gosrock/DuDoong-Backend&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dUzZW1/hyRQiNY5SV/cfXSF1YHjzhLgvejCqZFjk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/SwaggerConfig.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Gosrock/DuDoong-Backend/blob/dev/DuDoong-Api/src/main/java/band/gosrock/api/config/SwaggerConfig.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dUzZW1/hyRQiNY5SV/cfXSF1YHjzhLgvejCqZFjk/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 - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-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>스프링</category>
      <category>스웨거</category>
      <category>스프링</category>
      <category>에러코드</category>
      <author>ImNM</author>
      <guid isPermaLink="true">https://devnm.tistory.com/29</guid>
      <comments>https://devnm.tistory.com/29#entry29comment</comments>
      <pubDate>Mon, 6 Mar 2023 00:03:36 +0900</pubDate>
    </item>
  </channel>
</rss>