<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>gi_dor</title>
    <link>https://gi-dor.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 11 Aug 2026 16:58:22 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>기돌</managingEditor>
    <image>
      <title>gi_dor</title>
      <url>https://tistory1.daumcdn.net/tistory/4898523/attach/06beecd3cdfa47bbbfb8f01cfb0aaff2</url>
      <link>https://gi-dor.tistory.com</link>
    </image>
    <item>
      <title>AWS 배포 EC2 인스턴스</title>
      <link>https://gi-dor.tistory.com/258</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1년전에 했던 글을 보면서 새로 적용하면서 정리 했습니다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001019679&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://product.kyobobook.co.kr/detail/S000001019679&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1717947620266&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;스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 이동욱 - 교보문고&quot; data-og-description=&quot;스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현합니다&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001019679&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000001019679&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/FNwQR/hyWg5YKqQF/4vuArc5suqrkgXRESMp3NK/img.jpg?width=458&amp;amp;height=694&amp;amp;face=0_0_458_694,https://scrap.kakaocdn.net/dn/c1i8Q5/hyWg1ovB9t/2Cqm4zFEkT8bKbIKjn6wRK/img.jpg?width=458&amp;amp;height=694&amp;amp;face=0_0_458_694,https://scrap.kakaocdn.net/dn/dfIyUh/hyWlezI1tV/HwFrEYc1HKbtFpoz83VQp0/img.jpg?width=814&amp;amp;height=3649&amp;amp;face=0_0_814_3649&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001019679&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001019679&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/FNwQR/hyWg5YKqQF/4vuArc5suqrkgXRESMp3NK/img.jpg?width=458&amp;amp;height=694&amp;amp;face=0_0_458_694,https://scrap.kakaocdn.net/dn/c1i8Q5/hyWg1ovB9t/2Cqm4zFEkT8bKbIKjn6wRK/img.jpg?width=458&amp;amp;height=694&amp;amp;face=0_0_458_694,https://scrap.kakaocdn.net/dn/dfIyUh/hyWlezI1tV/HwFrEYc1HKbtFpoz83VQp0/img.jpg?width=814&amp;amp;height=3649&amp;amp;face=0_0_814_3649');&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;스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 이동욱 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현합니다&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책이 19년도여서 버전이 많이 안맞는다 .. 또한 사람들은 Ubuntu 또는 리눅스2 를 사용하고 책에서는 리눅스1을 &lt;br /&gt;사용하기에 쉽지 않아서 여러 블로그를 보며 하기에 시간이 오래걸렸습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;AWS 는 Amazon 에서 제공하는 클라우드 서비스 입니다&amp;nbsp;&lt;br /&gt;간단하게 생각하면 Amazon에서 가상의 컴퓨터를 대여해서 사용한다고 생각하면 됩니다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1. EC2&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;가상의 컴퓨터를 대여해준다&lt;/li&gt;
&lt;li&gt;서버 , 스토리지 , 데이터베이스 등의 서비스를 제공&lt;/li&gt;
&lt;li&gt;손쉽게 운영체제 ,CPU , RAM , 용량 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. RDS Relational Database Service&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;관계형 데이터베이스 서비스&lt;/li&gt;
&lt;li&gt;관계형 DB를 대여&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows&amp;nbsp;&lt;br /&gt;Ubuntu&lt;br /&gt;Java17&lt;br /&gt;Springboot&lt;br /&gt;EC2&lt;br /&gt;RDS&lt;br /&gt;Gradle&lt;br /&gt;MySQL8&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. 인스턴스 생성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tjoxq/btsHT9M7nfi/KqIZqvOLkxTp3NqH5Wl2x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tjoxq/btsHT9M7nfi/KqIZqvOLkxTp3NqH5Wl2x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tjoxq/btsHT9M7nfi/KqIZqvOLkxTp3NqH5Wl2x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTjoxq%2FbtsHT9M7nfi%2FKqIZqvOLkxTp3NqH5Wl2x1%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;502&quot; height=&quot;197&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;781&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mer5b/btsHSt7gUht/ksESrNxVUJDko8Mcxfx1M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mer5b/btsHSt7gUht/ksESrNxVUJDko8Mcxfx1M1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mer5b/btsHSt7gUht/ksESrNxVUJDko8Mcxfx1M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmer5b%2FbtsHSt7gUht%2FksESrNxVUJDko8Mcxfx1M1%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;781&quot; height=&quot;781&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;781&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;2. 새로운 키 생성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceq7Px/btsHRYmrv4n/xXrXER48jfv10khQme4tVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceq7Px/btsHRYmrv4n/xXrXER48jfv10khQme4tVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceq7Px/btsHRYmrv4n/xXrXER48jfv10khQme4tVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fceq7Px%2FbtsHRYmrv4n%2FxXrXER48jfv10khQme4tVK%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;692&quot; height=&quot;465&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 컴퓨터에 접근할때 사용할 비밀번호 같은&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v5qS8/dJMcafyXLo9/LcrDknVek6FgTRlKWauQ40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v5qS8/dJMcafyXLo9/LcrDknVek6FgTRlKWauQ40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v5qS8/dJMcafyXLo9/LcrDknVek6FgTRlKWauQ40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv5qS8%2FdJMcafyXLo9%2FLcrDknVek6FgTRlKWauQ40%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;653&quot; height=&quot;548&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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-origin-width=&quot;737&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPzJuI/btsHSpqhgK2/X1uDVnVjudhloUCKxrtEZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPzJuI/btsHSpqhgK2/X1uDVnVjudhloUCKxrtEZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPzJuI/btsHSpqhgK2/X1uDVnVjudhloUCKxrtEZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPzJuI%2FbtsHSpqhgK2%2FX1uDVnVjudhloUCKxrtEZK%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;737&quot; height=&quot;577&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFWED5/dJMcac9Z3gf/FKhyihk7OkTyA7IbHkJWOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFWED5/dJMcac9Z3gf/FKhyihk7OkTyA7IbHkJWOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFWED5/dJMcac9Z3gf/FKhyihk7OkTyA7IbHkJWOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFWED5%2FdJMcac9Z3gf%2FFKhyihk7OkTyA7IbHkJWOK%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;976&quot; height=&quot;734&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 스토리지 구성&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;최대 30GB 까지 사용 할수 있다고 합니다&lt;br /&gt;EC2에서 사용할수 있는 하드 디스크라고 생각하면된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjA1NF/btsHSqJyrUO/EFZsmQQC8807dz5o4UCkr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjA1NF/btsHSqJyrUO/EFZsmQQC8807dz5o4UCkr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjA1NF/btsHSqJyrUO/EFZsmQQC8807dz5o4UCkr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjA1NF%2FbtsHSqJyrUO%2FEFZsmQQC8807dz5o4UCkr1%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;710&quot; height=&quot;418&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;418&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;4. 생성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;337&quot; data-origin-height=&quot;493&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L8zbK/btsHR92mr98/wxub7jUtLkTgxkfhcc51v1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L8zbK/btsHR92mr98/wxub7jUtLkTgxkfhcc51v1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L8zbK/btsHR92mr98/wxub7jUtLkTgxkfhcc51v1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL8zbK%2FbtsHR92mr98%2Fwxub7jUtLkTgxkfhcc51v1%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;337&quot; height=&quot;493&quot; data-origin-width=&quot;337&quot; data-origin-height=&quot;493&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brL196/dJMcab4m9lg/DaQRzlv6HTJbaRtsUpKnRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brL196/dJMcab4m9lg/DaQRzlv6HTJbaRtsUpKnRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brL196/dJMcab4m9lg/DaQRzlv6HTJbaRtsUpKnRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrL196%2FdJMcab4m9lg%2FDaQRzlv6HTJbaRtsUpKnRk%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;570&quot; height=&quot;153&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 연결&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k71Yq/btsHS64EblE/SJu8OQoiPyoUqq4B68JJj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k71Yq/btsHS64EblE/SJu8OQoiPyoUqq4B68JJj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k71Yq/btsHS64EblE/SJu8OQoiPyoUqq4B68JJj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk71Yq%2FbtsHS64EblE%2FSJu8OQoiPyoUqq4B68JJj1%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;1116&quot; height=&quot;190&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnhAsz/btsHTUWRXQz/pOeG8LnliQ2EZEVZCcZyYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnhAsz/btsHTUWRXQz/pOeG8LnliQ2EZEVZCcZyYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnhAsz/btsHTUWRXQz/pOeG8LnliQ2EZEVZCcZyYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnhAsz%2FbtsHTUWRXQz%2FpOeG8LnliQ2EZEVZCcZyYK%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;1618&quot; height=&quot;117&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo7qty/btsHTD8Ulgs/cZaiR8pgtAKFpTUCMIhNPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo7qty/btsHTD8Ulgs/cZaiR8pgtAKFpTUCMIhNPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo7qty/btsHTD8Ulgs/cZaiR8pgtAKFpTUCMIhNPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo7qty%2FbtsHTD8Ulgs%2FcZaiR8pgtAKFpTUCMIhNPK%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;732&quot; height=&quot;602&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인스턴스 생성시 만든 key 파일이 있는 곳에서 Git Bash 를 통해 터미널 실행합니다&lt;/b&gt;&lt;br /&gt;ssh&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;1222&quot; data-origin-height=&quot;681&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIkxBC/btsHTneXWiU/j9cVGWQMmhJq0ay3IIjAvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIkxBC/btsHTneXWiU/j9cVGWQMmhJq0ay3IIjAvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIkxBC/btsHTneXWiU/j9cVGWQMmhJq0ay3IIjAvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIkxBC%2FbtsHTneXWiU%2Fj9cVGWQMmhJq0ay3IIjAvK%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;1222&quot; height=&quot;681&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;681&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 접속 완료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 업데이트&lt;/p&gt;
&lt;pre id=&quot;code_1718021932204&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0. 시간대 한국으로 변경&lt;/p&gt;
&lt;pre id=&quot;code_1718022957880&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
date;		// KST&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java17&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1718021940249&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install openjdk-17-jdk -y
java -version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL&lt;/p&gt;
&lt;pre id=&quot;code_1773320765512&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install mysql-server -y // 설치
sudo systemctl status mysql

sudo mysql
ALTER USER '계정'@'localhost' IDENTIFIED WITH mysql_native_password BY '비밀번호!';
FLUSH PRIVILEGES;

exit;
mysql -u 아이디 -p


CREATE DATABASE 디비명;

show databases;  // DB확인

exit;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 DB dump 만든 파일 EC2에 밀어 넣어 import&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773320866261&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;scp -i rent-server-key.pem rentinsight-dump.sql ubuntu@xx.xxx.xx.xxx:/home/ubuntu&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;key 파일이 있는 위치에서 git bash 를 통해 EC2로 보낸다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;import&lt;/p&gt;
&lt;pre id=&quot;code_1773320940487&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mysql -u 계정 -p rentinsight &amp;lt; rentinsight_dump1.sql
mysql -u 계정 -p rentinsight &amp;lt; rentinsight_dump2.sql
mysql -u 계정 -p rentinsight &amp;lt; rentinsight_dump3.sql
mysql -u 계정 -p rentinsight &amp;lt; rentinsight_dump4.sql&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;만약 하다가 꼬였다면 mysql 삭제후 재설치&lt;/p&gt;
&lt;pre id=&quot;code_1773495309341&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo systemctl stop mysql
sudo apt purge mysql-server mysql-client mysql-common mysql-server-core-* mysql-client-core-* -y

sudo rm -rf /etc/mysql
sudo rm -rf /var/lib/mysql
sudo rm -rf /var/log/mysql

sudo apt autoremove -y
sudo apt autoclean

sudo apt update
sudo apt install mysql-server -y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git clone&lt;/p&gt;
&lt;pre id=&quot;code_1718022035789&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git clone -b 브랜치명 repository주소&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;application-local.yml 만들기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;해당 파일에는 DB 접속 , API serviceKey , url 정보가 담겨있&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773321210162&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd src/main/resources
nano application-local.yml&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773321281456&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    username: DB접속 ID
    password: DB 접속 PW

api:
  serviceKey: &quot;API키&quot;

  baseUrl:
    officetel: &quot;https://apis.data.go.kr/.............&quot;
    multiHouse: &quot;https://apis.data.go.kr/...........&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;CTRL + X&lt;/li&gt;
&lt;li&gt;Y&lt;/li&gt;
&lt;li&gt;Enter&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 실행&amp;nbsp; jar 파일빌드&lt;/p&gt;
&lt;pre id=&quot;code_1718032030315&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;cd ~/Rent-Insight-Helper

chmod +x gradlew

./gradlew clean build -x test --no-daemon
// 테스트 스킵하고 빌드&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;build 과정에서 너무 오래 걸리는 상황발생&lt;/li&gt;
&lt;li&gt;종료후 EC2 메모리 증설&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gi-dor.tistory.com/262&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gi-dor.tistory.com/262&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1773321436060&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 Boot 개인,토이 프로젝트에서 CI/CD&quot; data-og-description=&quot;private Repository&amp;nbsp; - 깃허브 계정 , 토큰 복사 붙여넣기 안하기 위해&amp;nbsp;git config --global credential.helper storegit pull origin main// id// 토큰 pw이제 그대로 id 와 토큰 값이 저장되어있어 재 입력 하지 않아도된&quot; data-og-host=&quot;gi-dor.tistory.com&quot; data-og-source-url=&quot;https://gi-dor.tistory.com/262&quot; data-og-url=&quot;https://gi-dor.tistory.com/262&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hf572/dJMb9cBGygk/0S2LOpSfYKfBnuVGlkBRJ1/img.png?width=465&amp;amp;height=150&amp;amp;face=0_0_465_150,https://scrap.kakaocdn.net/dn/eqKB2v/dJMb9kT1qLH/vR4XcKBQZsnsp7lUTXUsbk/img.png?width=465&amp;amp;height=150&amp;amp;face=0_0_465_150,https://scrap.kakaocdn.net/dn/YWzo5/dJMb9kT1qLI/8APAce8EyEy28EBjU3ivwk/img.png?width=1210&amp;amp;height=809&amp;amp;face=0_0_1210_809&quot;&gt;&lt;a href=&quot;https://gi-dor.tistory.com/262&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gi-dor.tistory.com/262&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hf572/dJMb9cBGygk/0S2LOpSfYKfBnuVGlkBRJ1/img.png?width=465&amp;amp;height=150&amp;amp;face=0_0_465_150,https://scrap.kakaocdn.net/dn/eqKB2v/dJMb9kT1qLH/vR4XcKBQZsnsp7lUTXUsbk/img.png?width=465&amp;amp;height=150&amp;amp;face=0_0_465_150,https://scrap.kakaocdn.net/dn/YWzo5/dJMb9kT1qLI/8APAce8EyEy28EBjU3ivwk/img.png?width=1210&amp;amp;height=809&amp;amp;face=0_0_1210_809');&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 Boot 개인,토이 프로젝트에서 CI/CD&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;private Repository&amp;nbsp; - 깃허브 계정 , 토큰 복사 붙여넣기 안하기 위해&amp;nbsp;git config --global credential.helper storegit pull origin main// id// 토큰 pw이제 그대로 id 와 토큰 값이 저장되어있어 재 입력 하지 않아도된&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gi-dor.tistory.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;pre id=&quot;code_1773321426137&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 메모리 상태 확인
free -h

// swap 파일을 생성해준다. (메모리 상태 확인 시 swap이 있었지만 디렉토리 파일은 만들어줘야한다.)
sudo mkdir /var/spool/swap
sudo touch /var/spool/swap/swapfile
sudo dd if=/dev/zero of=/var/spool/swap/swapfile count=2048000 bs=1024

// swap 파일을 설정한다.
sudo chmod 600 /var/spool/swap/swapfile
sudo mkswap /var/spool/swap/swapfile
sudo swapon /var/spool/swap/swapfile

// swap 파일을 등록한다.
sudo vim /etc/fstab
파일이 열리면 해당 파일 아래쪽에 하단 내용 입력 후 저장
/var/spool/swap/swapfile    none    swap    defaults    0 0

// 메모리 상태 확인
free -h&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773321463407&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -jar build/libs/*.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;http://탄력적IP:8080&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;933&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZv3mT/dJMcaadsyd3/cB5KRdPlCH5quFfgkU6U91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZv3mT/dJMcaadsyd3/cB5KRdPlCH5quFfgkU6U91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZv3mT/dJMcaadsyd3/cB5KRdPlCH5quFfgkU6U91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZv3mT%2FdJMcaadsyd3%2FcB5KRdPlCH5quFfgkU6U91%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;1562&quot; height=&quot;933&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;933&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 블로그&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@nefertiri/AWS-RDS%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0#3-%EB%A1%9C%EC%BB%AC-pc%EC%97%90%EC%84%9C-rds-%EC%A0%91%EC%86%8D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;1번 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@nefertiri/AWS-EC2-%EC%84%9C%EB%B2%84%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2번 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kang-james.tistory.com/entry/%EB%B0%B0%ED%8F%AC-AWS%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%B0%B0%ED%8F%AC-%EB%B0%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%EA%B3%BC-%EC%97%B0%EA%B2%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3번 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@nefertiri/AWS-EC2-%EC%84%9C%EB%B2%84%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0#2-%EB%B0%B0%ED%8F%AC-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9E%91%EC%84%B1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;4번 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gaemi606.tistory.com/entry/AWS-EC2%EC%97%90-JAVA%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;5번 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://thalals.tistory.com/408&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;6번 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kang-james.tistory.com/entry/%EB%B0%B0%ED%8F%AC-AWS%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%B0%B0%ED%8F%AC-%EB%B0%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0EC2-%EC%84%9C%EB%B2%84-%EC%8B%A4%ED%96%89&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;7번 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=s1Sysxw9JIA&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;8번 유튜브&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AWS/CI CD</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/258</guid>
      <comments>https://gi-dor.tistory.com/258#entry258comment</comments>
      <pubDate>Sat, 14 Mar 2026 22:39:59 +0900</pubDate>
    </item>
    <item>
      <title>팝업 모달 &amp;amp; 24시간 보지 않기  with JS ,  Cookie</title>
      <link>https://gi-dor.tistory.com/289</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발 중인 미니 프로젝트는 만들어가는 단계 이므로 배포 이후 사용자가 미구현된 버튼을 클릭하고 안되는 기능이나&lt;br /&gt;데이터가 없어서 작동하지 않는 검색조건이 있을수 있어 사용자가 사이트를 이탈하는 것을 방지하기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 페이지 진입 시 &lt;b&gt;'서비스 업데이트 로드맵'을 안내하는 팝업 모달 &amp;amp; 검색시 없는 데이터 안내문 '&lt;/b&gt; 을 띄우기로 결정&lt;br /&gt;다만, 매번 접속할 때마다 팝업이 뜨면 피로감을 줄 수 있으므로 &lt;b&gt;'24시간 동안 보지 않기'&lt;/b&gt; 기능을 쿠키(Cookie)를 활용해 구현했음&lt;/p&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;/li&gt;
&lt;li&gt;팝업 안에는 24시간 다시보지 않기 체크박스 있다&lt;/li&gt;
&lt;li&gt;체크박스를 선택하지 않고 팝업을 닫으면 메인 페이지 재방문시 팝업 발생&lt;/li&gt;
&lt;li&gt;체크박스 선택후 팝업을 닫으면 메인 페이지 재방문시 팝업 미 발생&lt;/li&gt;
&lt;/ul&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/pNEee/dJMcafMtK0O/ZUiuPzJHKZlvaKBMRK9Od0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pNEee/dJMcafMtK0O/ZUiuPzJHKZlvaKBMRK9Od0/img.png&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;612&quot; data-is-animation=&quot;false&quot; style=&quot;width: 46.3314%; margin-right: 10px;&quot; data-widthpercent=&quot;46.88&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pNEee/dJMcafMtK0O/ZUiuPzJHKZlvaKBMRK9Od0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpNEee%2FdJMcafMtK0O%2FZUiuPzJHKZlvaKBMRK9Od0%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;558&quot; height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byALbP/dJMcadVmnDB/nWWJSHjTDiRSwhOXPolxBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byALbP/dJMcadVmnDB/nWWJSHjTDiRSwhOXPolxBk/img.png&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;541&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.5058%;&quot; data-widthpercent=&quot;53.12&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byALbP/dJMcadVmnDB/nWWJSHjTDiRSwhOXPolxBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyALbP%2FdJMcadVmnDB%2FnWWJSHjTDiRSwhOXPolxBk%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;559&quot; height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XCf69/dJMcachTVyr/YcZv8zYNuPgnm6YQMSXgVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XCf69/dJMcachTVyr/YcZv8zYNuPgnm6YQMSXgVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XCf69/dJMcachTVyr/YcZv8zYNuPgnm6YQMSXgVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXCf69%2FdJMcachTVyr%2FYcZv8zYNuPgnm6YQMSXgVK%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;777&quot; height=&quot;162&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;162&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;모달html&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;data-bs-dismiss=&quot;modal&quot;&lt;/code&gt; Bootstrap이 제공하는 기본 닫기 기능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onclick=&quot;closeNoticeModal()&lt;/code&gt; 닫기 버튼 누를때 쿠키 생성 처리할 JS 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- src/main/resources/templates/common/noticeModal.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;[http://www.thymeleaf.org](http://www.thymeleaf.org)&quot;&amp;gt;
&amp;lt;div th:fragment=&quot;noticeModal&quot;&amp;gt;
    &amp;lt;div class=&quot;modal fade&quot; id=&quot;noticeModal&quot; tabindex=&quot;-1&quot; aria-hidden=&quot;true&quot;&amp;gt;
        &amp;lt;div class=&quot;modal-dialog modal-dialog-centered&quot;&amp;gt;
            &amp;lt;div class=&quot;modal-content border-0 shadow-lg&quot;&amp;gt;
                &amp;lt;!-- 모달 헤더 --&amp;gt;
                &amp;lt;div class=&quot;modal-header bg-dark text-white&quot;&amp;gt;
                    &amp;lt;h5 class=&quot;modal-title fw-bold&quot;&amp;gt;
                        &amp;lt;i class=&quot;bi bi-megaphone-fill me-2&quot;&amp;gt;&amp;lt;/i&amp;gt;서비스 업데이트 로드맵 안내
                    &amp;lt;/h5&amp;gt;
                    &amp;lt;button type=&quot;button&quot; class=&quot;btn-close btn-close-white&quot; data-bs-dismiss=&quot;modal&quot;&amp;gt;&amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;!-- 모달 바디 (로드맵 내용 생략) --&amp;gt;
                &amp;lt;div class=&quot;modal-body p-3&quot;&amp;gt;
                   &amp;lt;!-- ... 업데이트 예정 기능 목록 ... --&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;!-- 모달 푸터 (24시간 보지 않기) --&amp;gt;
                &amp;lt;div class=&quot;modal-footer d-flex justify-content-between bg-light&quot;&amp;gt;
                    &amp;lt;div class=&quot;form-check mb-0&quot;&amp;gt;
                        &amp;lt;input class=&quot;form-check-input&quot; type=&quot;checkbox&quot; id=&quot;hideFor24Hours&quot;&amp;gt;
                        &amp;lt;label class=&quot;form-check-label text-muted small mt-1&quot; for=&quot;hideFor24Hours&quot;&amp;gt;
                            24시간 동안 보지 않기
                        &amp;lt;/label&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;!-- 닫기 버튼 클릭 시 이벤트 발생 --&amp;gt;
                    &amp;lt;button type=&quot;button&quot; class=&quot;btn btn-secondary btn-sm px-4&quot; data-bs-dismiss=&quot;modal&quot; onclick=&quot;closeNoticeModal()&quot;&amp;gt;닫기&amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 페이지 html&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- src/main/resources/templates/index.html --&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!-- 1. 네비게이션 바 --&amp;gt;
    &amp;lt;nav th:replace=&quot;~{common/navbar :: navbar}&quot;&amp;gt;&amp;lt;/nav&amp;gt;

    &amp;lt;!-- 2. 메인  영역 --&amp;gt;
    &amp;lt;main&amp;gt;

    &amp;lt;/main&amp;gt;

    &amp;lt;!-- 3. 공지사항 모달 Fragment 삽입 --&amp;gt;
    &amp;lt;div th:replace=&quot;~{common/noticeModal :: noticeModal}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;!-- 4. 푸터 --&amp;gt;
    &amp;lt;footer th:replace=&quot;~{common/footer :: footer}&quot;&amp;gt;&amp;lt;/footer&amp;gt;

    &amp;lt;!-- Bootstrap JS (반드시 모달 제어 스크립트보다 위에 위치해야 함!!!!!!) --&amp;gt;
    &amp;lt;script src=&quot;[https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js](https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js)&quot;&amp;gt;&amp;lt;/script&amp;gt;

    &amp;lt;!-- 모달 팝업 제어용 커스텀 JS 영역 --&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달 팝업 JS&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 페이지 로드 다되면 쿠키 확인후에 팝업 출력 결정
window.onload = function () {
    // 쿠키 문자열에 'hideNotice=true'가 포함되어 있는지 검사
    if(document.cookie.includes(&quot;hideNotice=true&quot;)) {
        // 쿠키가 존재하면 아무 동작도 하지 않음 (팝업 숨김)
    } else {
        // 쿠키가 없으면 Bootstrap 모달 객체를 생성
        let noticeModal = new bootstrap.Modal(document.getElementById('noticeModal'));
        noticeModal.show();    // 화면에 표시
    }
}

// 닫기 버튼 클릭 시 동작
function closeNoticeModal() {
    let checkbox = document.getElementById('hideFor24Hours');
    if(checkbox.checked) {
        // 체크박스가 선택된 상태로 닫으면 유효기간 1일짜리 쿠키 생성
        setCookie(&quot;hideNotice&quot;, &quot;true&quot;, 1);
    }
}

// 재사용 가능한 쿠키 생성됨
/**
 * @param {string} name - 생성할 쿠키의 이름
 * @param {string} value - 쿠키에 저장할 값
 * @param {number} days - 쿠키의 유효기간 (일 단위)
 */
function setCookie(name, value, days) {
    let todayDate = new Date();
    // 일(days) 단위의 시간을 밀리초(ms)로 변환
    let duration = days * 24 * 60 * 60 * 1000;

    // 현재 시간에 계산된 밀리초를 더하여 만료 시점을 설정
    todayDate.setTime(todayDate.getTime() + duration);
    let expires = todayDate.toUTCString();

    // 브라우저에 쿠키 저장 (path=/ 옵션으로 사이트 전체에서 쿠키 공유)
    document.cookie = name + &quot;=&quot; + value + &quot;; expires=&quot; + expires + &quot;; path=/&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지가 열릴때 JS에서 브라우저의 쿠키 저장소를 확인해 팝업그만보기 쿠키가 있는지 확인 &lt;code&gt;( Ex. hideNotice )&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;팝업 결정 여부&lt;/li&gt;
&lt;li&gt;팝업 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;24시간 보지 않기 체크 후 닫기 누르면 JS는 브라우저에 &lt;code&gt;hideNotice=true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 여기까지는 해피엔딩이어씀&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;includes() 방식과 getCookie()의 필요성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상치 못한 쿠키 이름 충돌에 매우 취약하다&lt;br /&gt;예를 들어 테스트를 위해 추가된 쿠키 :&lt;code&gt;do_not_hideNotice=true&lt;/code&gt;&lt;br /&gt;이제 브라우저에 저장된 &lt;code&gt;document.cookie&lt;/code&gt;문자열은 아래와 같은 형태가 된다&lt;br /&gt;&lt;code&gt;&quot;abcdCookie=true; do_not_hideNotice=true; sdaCookie=txxxxxxx; hideNotice=true&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서, 페이지가 로드될 때 includes() 코드가 실행되면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드: &lt;code&gt;if (document.cookie.includes(&quot;hideNotice=true&quot;)) { ... }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;동작 : JavaScript는 전체 문자열 안에서 &lt;code&gt;&quot;hideNotice=true&quot;&lt;/code&gt; 라는 텍스트를 찾는다&lt;/li&gt;
&lt;li&gt;결과 : &lt;code&gt;&quot;do_not_hideNotice=true&quot;&lt;/code&gt; 라는 문자열 안에 &quot;hideNotice=true&quot;가 포함되어 있으므로 해당 조건은 true를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getCookie() 함수는 해당 문제를 해결할수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;document.cookie&lt;/code&gt; 문자열을 세미콜론(;)으로 쪼개어, [&quot;...&quot;, &quot;do_not_hideNotice=true&quot;] 와 같이 각 쿠키를 분리&lt;/li&gt;
&lt;li&gt;정확한 시작점 확인: &lt;code&gt;(&quot;do_not_hideNotice=true&quot;)&lt;/code&gt;가 찾으려는 정확한 키와 등호 &lt;code&gt;(&quot;hideNotice=&quot;)&lt;/code&gt; 로 시작하는지&lt;code&gt;(indexOf(...) === 0)&lt;/code&gt;를 검사&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;do_not_hideNotice=true&quot;는 &quot;hideNotice=&quot;&lt;/code&gt;로 시작하지 않으므로 무시된다&lt;/li&gt;
&lt;li&gt;오직 정확히 &lt;code&gt;&quot;hideNotice=true&quot;&lt;/code&gt; 라는 조각을 찾았을 때만 값을 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 getCookie()를 사용해야 하는가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;includes()&lt;/code&gt; : 빠르고 간편하다 (최고!) 다른 쿠키의 이름에 내가 찾는 문자열이 '포함'되는 경우 오작동할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getCookie()&lt;/code&gt;: 코드는 조금 더 길고 쿠키를 구분하고 정확한 매개변수를 기준으로 값을 찾아오므로 이름 충돌에서 안전&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;  window.onload = function () {
    if(!getCookie('hideSearchModal')) {
        let noticeModal = new bootstrap.Modal(document.getElementById('noticeModal'));
        noticeModal.show();
  }


// 쿠키 확인 함수
function getCookie(name) {
    let nameCookie = name + &quot;=&quot;;
    console.log('nameCookie',nameCookie)    // &quot;hideSearchModal=&quot;

    // (2) ['hideNotice=true', ' hideSearchModal=true']
    let splitCookie = document.cookie.split(';');
    console.log('splitCookie',splitCookie)

    for(let i = 0; i &amp;lt; splitCookie.length; i++) {
        let cookie = splitCookie[i];
        console.log('splitCookie[i]',cookie)    // hideNotice=true ,  hideSearchModal=true

        // 두번째 쿠키 부터는 앞에 ' hideSearchModal=true' 이런식으로 공백이있다
        while (cookie.charAt(0) === ' ') {
            cookie = cookie.substring(1, cookie.length);
            console.log('while cookie',cookie)
        }

        if (cookie.indexOf(nameCookie) === 0) {
            console.log('nameCookie.length',nameCookie.length)  // 16
            console.log('cookie.length',cookie.length)          // 20
            console.log('cookieString',cookie)                  // hideSearchModal=true
            return  cookie.substring(nameCookie.length, cookie.length);
        }
    }
    return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZSxtg/dJMcahcqhSa/OJ3l6VyhGkkj2F37sqCDq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZSxtg/dJMcahcqhSa/OJ3l6VyhGkkj2F37sqCDq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZSxtg/dJMcahcqhSa/OJ3l6VyhGkkj2F37sqCDq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZSxtg%2FdJMcahcqhSa%2FOJ3l6VyhGkkj2F37sqCDq0%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;463&quot; height=&quot;256&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 내 무신사 사이트내 쿠키만 37개인데 막말로 문자열 같은 쿠키 있을수도 있잖아!!!!!!&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Language/JavaScript</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/289</guid>
      <comments>https://gi-dor.tistory.com/289#entry289comment</comments>
      <pubDate>Thu, 26 Feb 2026 21:42:32 +0900</pubDate>
    </item>
    <item>
      <title>RentInsight _ Index 로 조회 성능 올리기 , MySql Profiling으로 병목 구간 측정</title>
      <link>https://gi-dor.tistory.com/288</link>
      <description>&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/bXP61C/dJMcagYQ9kc/5cRzynIkP7IAHC7YyeeMqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXP61C/dJMcagYQ9kc/5cRzynIkP7IAHC7YyeeMqk/img.png&quot; data-origin-width=&quot;260&quot; data-origin-height=&quot;131&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;52.44&quot; style=&quot;width: 51.8307%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXP61C/dJMcagYQ9kc/5cRzynIkP7IAHC7YyeeMqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXP61C%2FdJMcagYQ9kc%2F5cRzynIkP7IAHC7YyeeMqk%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;260&quot; height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O7Zyz/dJMcadnuZqH/i0nbNPMtMco5E9eXZ5qlzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O7Zyz/dJMcadnuZqH/i0nbNPMtMco5E9eXZ5qlzK/img.png&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;170&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.0065%;&quot; data-widthpercent=&quot;47.56&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O7Zyz/dJMcadnuZqH/i0nbNPMtMco5E9eXZ5qlzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO7Zyz%2FdJMcadnuZqH%2Fi0nbNPMtMco5E9eXZ5qlzK%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;306&quot; height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUMZLH/dJMcabcaAkO/6lpHILPW5WFSnwQEVvtm11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUMZLH/dJMcabcaAkO/6lpHILPW5WFSnwQEVvtm11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUMZLH/dJMcabcaAkO/6lpHILPW5WFSnwQEVvtm11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUMZLH%2FdJMcabcaAkO%2F6lpHILPW5WFSnwQEVvtm11%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;343&quot; height=&quot;480&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1771912649846&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;EXPLAIN SELECT *
        FROM rent_complete
        WHERE sgg_cd = '11305' AND umd_nm = '미아동'
        AND area BETWEEN 25 AND 36;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/enUxY4/dJMcab4jHtP/3IAKoiUfyDDsbHtMW97uwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/enUxY4/dJMcab4jHtP/3IAKoiUfyDDsbHtMW97uwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/enUxY4/dJMcab4jHtP/3IAKoiUfyDDsbHtMW97uwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FenUxY4%2FdJMcab4jHtP%2F3IAKoiUfyDDsbHtMW97uwk%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;1720&quot; height=&quot;162&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;5,0,0&quot; data-index-in-node=&quot;0&quot;&gt;type: ALL&amp;nbsp;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-path-to-node=&quot;5,0,1&quot;&gt;
&lt;li&gt;데이터베이스가 '풀 테이블 스캔(Full Table Scan)'을 수행&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;5,0,1,1,0&quot; data-index-in-node=&quot;0&quot;&gt;의미:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;rent_complete 테이블의 처음부터 끝까지 모든 데이터를 하나하나 다 뒤져서 조건을 검사하고 있다는 뜻&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;5,1,0&quot; data-index-in-node=&quot;0&quot;&gt;possible_keys: &amp;lt;null&amp;gt;, key: &amp;lt;null&amp;gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-path-to-node=&quot;5,1,1&quot;&gt;
&lt;li&gt;이 검색 쿼리를 빠르게 만들어줄&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-path-to-node=&quot;5,1,1,0,0&quot; data-index-in-node=&quot;22&quot;&gt;'인덱스(색인)'가 아예 존재하지 않거나 사용되지 않고 있다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;5,2,0&quot; data-index-in-node=&quot;0&quot;&gt;rows: 165,169&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-path-to-node=&quot;5,2,1&quot;&gt;
&lt;li&gt;이 쿼리의 결과를 찾기 위해 DB 엔진이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-path-to-node=&quot;5,2,1,0,0&quot; data-index-in-node=&quot;27&quot;&gt;약 16만 5천 건의 데이터를 읽어야 한다고 `예측`&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;하고 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;5,3,0&quot; data-index-in-node=&quot;0&quot;&gt;filtered: 0.11&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-path-to-node=&quot;5,3,1&quot;&gt;
&lt;li&gt;스캔한 16만 5천 건의 데이터 중, 실제 조건(미아동, 25~36㎡)에 맞는 데이터는 **단 0.11%**에 불과할 것이라고 예측하고 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;5,4,0&quot; data-index-in-node=&quot;0&quot;&gt;Extra: Using where&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-path-to-node=&quot;5,4,1&quot;&gt;
&lt;li&gt;스토리지 엔진에서 일단 데이터를 다 끌어올린 다음, MySQL 엔진 단에서 WHERE 조건을 걸어 불필요한 데이터를 버리고 있다는 의미입니다. (비 효율적이란다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-path-to-node=&quot;8&quot; data-index-in-node=&quot;0&quot;&gt;[현재 상태 : 심각한 병목(Bottleneck) 후보]&lt;/b&gt;&lt;br /&gt;현재 16만 건 정도의 데이터에서는 0.1초 만에 결과가 나올 수도 있다. 하지만 현재 매일 데이터를 업데이트 하는중인데&lt;br /&gt;전월세 데이터가 쌓여서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-path-to-node=&quot;8&quot; data-index-in-node=&quot;95&quot;&gt;100만 건, 1,000만 건이 되면 어떻게 될까?&lt;/b&gt;&lt;br /&gt;사용자가 검색 버튼을&amp;nbsp; 딸깍 누를 때마다 DB는 1,000만 건을 다 뒤져야 하고 동시 접속자가 10명만 되어도 데이터베이스 CPU와 메모리가 폭주하여&amp;nbsp; 서비스가 다운(장애)될 수 있는 매우 위험한 쿼리이다.&lt;br /&gt;16만 건을 뒤져서 고작 0.11%의 진짜 데이터를 찾기 위해 99.89%의 헛고생을 하고 있는 상태인것이다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-path-to-node=&quot;9&quot; data-index-in-node=&quot;0&quot;&gt;[해결책: 복합 인덱스(Composite Index) 생성]&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 문제를 해결하기 위해 반드시 인덱스(Index)를 만들어주어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;검색 조건이 sgg_cd = ? (동등 비교), umd_nm = ? (동등 비교), area BETWEEN ? AND ? (범위 검색) 이므로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-path-to-node=&quot;10&quot; data-index-in-node=&quot;81&quot;&gt;동등 비교를 먼저, 범위 검색을 나중에&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;두는 복합 인덱스를 생성하는 것이 맞다고 판단된다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 설정 기준&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리에서 자주 사용되는 컬럼
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE , JOIN , ORDER 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카디널리티 (값의 분산도 또는 고유성) &lt;b&gt;수치가 높은&lt;/b&gt; 컬럼에 인덱스 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sgg_cd&lt;/code&gt;시군구 코드 같은경우 서울에 25개 구의 코드가 저장되므로 카더닐리티는 25
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 데이터 수에 비하면 변별력이 충분하다고 생각되므로 인덱스 후보로 좋다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;umd_nm&lt;/code&gt; 법정동 이름은 시군구 코드보다 더많으므로 인덱스 후보가된다&lt;/li&gt;
&lt;li&gt;반대로 &lt;code&gt;contract_type&lt;/code&gt; 계약 종류 같은경우 '신규' , '갱신' 두종류의 값만 있기에 인덱스를 걸면 전체의 절반을 스캔 해야해서 효과가 거의 없을것이다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sgg_cd + umn_nm&lt;/code&gt; 을 섞은 복합 인덱스를 후보로 보고있다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sgg_cd&lt;/code&gt; 단독 인덱스 , &lt;code&gt;sgg_cd + umn_nm&lt;/code&gt;복합 인덱스 2개를 만들어서 사용하게 된다면&lt;br /&gt;두개의 인덱스 모두 &lt;code&gt;sgg_cd&lt;/code&gt;라는 정보를 중복해서 저장하고 있다&lt;br /&gt;&lt;br /&gt;&lt;b&gt;종로구 코드&lt;/b&gt; 와 &lt;b&gt;종로구 코드 + 숭인동&lt;/b&gt; 이라는 정보를 모두 가지고 있으므로 &lt;br /&gt;두개의 인덱스 모두 종로구 라는 정보를&lt;br /&gt;두번 저장되므로 디스크 공간을 더 많이 차지하게된다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(sgg_cd, umd_nm) 복합 인덱스 하나만 있어도 MySQL 옵티마이저는 WHERE sgg_cd = '...' 와 같은 쿼리가 들어왔을 때에 복합 인덱스의 첫 번째 부분(sgg_cd)만 사용해서 검색을 효율적으로 수행한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가로 area 컬럼또한 &lt;code&gt;WHERE .... AND area BETWEEN 53 AND 66&lt;/code&gt; 으로 사용하기에 선택했다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    -- 1. 고유 식별자
    COUNT(DISTINCT id) AS id_cardinality, -- 거의 전체 행 개수와 동일하게 나옴 (매우 높음)

    -- 2. 검색 조건으로 사용될 후보들
    COUNT(DISTINCT sgg_cd) AS sgg_cd_cardinality,          -- 시군구 코드 (중간 정도)
    COUNT(DISTINCT umd_nm) AS umd_nm_cardinality,          -- 법정동 이름 (비교적 높음)
    COUNT(DISTINCT name) AS name_cardinality,            -- 건물 이름 
    COUNT(DISTINCT area) AS area_cardinality,            -- 전용 면적 (매우 높음)
    COUNT(DISTINCT build_year) AS build_year_cardinality,  -- 건축 년도 

    -- 3. 범위 검색에 사용될 값들
    COUNT(DISTINCT deposit) AS deposit_cardinality,      -- 보증금 (매우 높음)
    COUNT(DISTINCT monthly_rent) AS monthly_rent_cardinality, -- 월세 (높음)

    -- 4. 카테고리성 정보 (카디널리티가 낮을 것으로 예상)
    COUNT(DISTINCT contract_type) AS contract_type_cardinality -- 계약 종류 (매우 낮음, 예: '신규', '갱신')
FROM
    rent_complete;&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;style6&quot; /&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;CREATE INDEX idx_rent_complete_sgg_umd_area ON rent_complete (sgg_cd, umd_nm, area);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C0f6r/dJMcaflne9u/jkSgd9hsdI64N7VkK325hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C0f6r/dJMcaflne9u/jkSgd9hsdI64N7VkK325hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C0f6r/dJMcaflne9u/jkSgd9hsdI64N7VkK325hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC0f6r%2FdJMcaflne9u%2FjkSgd9hsdI64N7VkK325hk%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;1800&quot; height=&quot;124&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;type: range &amp;nbsp;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스가 ' 인덱스 범위 스캔&amp;nbsp; ' 을 수행&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,1,1,0&quot;&gt;의미:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;rent_complete 테이블의 처음부터 끝까지 모든 데이터를 하나하나 다 뒤져서 조건을 검사하고 있다는 뜻&lt;/li&gt;
&lt;li&gt;WHERE 조건으로&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;534&quot;&gt;sgg_cd = ? (동등 비교)&lt;/li&gt;
&lt;li data-end=&quot;579&quot; data-start=&quot;557&quot;&gt;umd_nm = ? (동등 비교)&lt;/li&gt;
&lt;li data-end=&quot;612&quot; data-start=&quot;580&quot;&gt;area BETWEEN ? AND ? (범위 검색)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;possible_keys,&amp;nbsp; key:&amp;nbsp; idx_rent_complete_sgg_umd_area&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제로 만든 복합 인덱스가 사용되고 있다&amp;nbsp;&lt;/li&gt;
&lt;li&gt;풀테이블 스캔 X&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,2,0&quot;&gt;rows: 43 &lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전에 풀 테이블 스캔 당시 16만 건을 읽어야 했지만 현재 43건만 탐색하면 된&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,3,0&quot;&gt;filtered: 100 &lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5,3,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽은 데이터 43건이 모두 조건에 만족한다&amp;nbsp;&lt;/li&gt;
&lt;li&gt;불필요한 데이터가 없다 , 인덱스에서 필터링이 되었&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,4,0&quot;&gt;Extra: Using index condition &lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5,4,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InnoDB 스토리지 엔진 레벨에서 WHERE 조건 일부를 미리 필터링하고 있다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 탐색&lt;/li&gt;
&lt;li&gt;인덱스에 포함된 컬럼으로 WHERE 조건 검사&lt;/li&gt;
&lt;li&gt;조건을 통과한 것만 실제 테이블에 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;INDEX&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;1355&quot; data-origin-height=&quot;693&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhBHy3/dJMcadgMRl6/U9uih9AvSWPKumEkmG4ug0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhBHy3/dJMcadgMRl6/U9uih9AvSWPKumEkmG4ug0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhBHy3/dJMcadgMRl6/U9uih9AvSWPKumEkmG4ug0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhBHy3%2FdJMcadgMRl6%2FU9uih9AvSWPKumEkmG4ug0%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;1355&quot; height=&quot;693&quot; data-origin-width=&quot;1355&quot; data-origin-height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[인덱스&amp;nbsp;적용&amp;nbsp;전&amp;nbsp;성능&amp;nbsp;지표]&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 TPS (초당 처리량): 6,468.8 req/sec&lt;/li&gt;
&lt;li&gt;평균 응답 시간: 15.21 ms&lt;/li&gt;
&lt;li&gt;한계점: 동시 접속자 수가 99명을 초과하면 에러가 발생하며, 시스템이 더 이상 부하를 감당하지 못했음&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;INDEX 적용&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1347&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OV7CS/dJMb99ZMWzM/PC4BtTTFKundjrzgEGuBrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OV7CS/dJMb99ZMWzM/PC4BtTTFKundjrzgEGuBrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OV7CS/dJMb99ZMWzM/PC4BtTTFKundjrzgEGuBrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOV7CS%2FdJMb99ZMWzM%2FPC4BtTTFKundjrzgEGuBrk%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;1347&quot; height=&quot;655&quot; data-origin-width=&quot;1347&quot; data-origin-height=&quot;655&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;table style=&quot;border-collapse: collapse; width: 100%; height: 147px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 21px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 22.7907%; height: 21px; text-align: center;&quot;&gt;인덱스 적용 전&lt;/td&gt;
&lt;td style=&quot;width: 23.9535%; height: 21px; text-align: center;&quot;&gt;인덱스 적용 후&lt;/td&gt;
&lt;td style=&quot;width: 28.2558%; height: 21px; text-align: center;&quot;&gt;개선효과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 42px; text-align: center;&quot;&gt;평균&amp;nbsp;TPS&lt;/td&gt;
&lt;td style=&quot;width: 22.7907%; height: 42px; text-align: center;&quot;&gt;6,468.8&lt;/td&gt;
&lt;td style=&quot;width: 23.9535%; height: 42px; text-align: center;&quot;&gt;7,884.2&lt;/td&gt;
&lt;td style=&quot;width: 28.2558%; height: 42px; text-align: left;&quot;&gt;+21.9%&amp;nbsp;(초당&amp;nbsp;1,415건&amp;nbsp;추가&amp;nbsp;처리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 42px; text-align: center;&quot;&gt;최고&amp;nbsp;TPS&lt;/td&gt;
&lt;td style=&quot;width: 22.7907%; height: 42px; text-align: center;&quot;&gt;7,117.0&lt;/td&gt;
&lt;td style=&quot;width: 23.9535%; height: 42px; text-align: center;&quot;&gt;8,828.5&lt;/td&gt;
&lt;td style=&quot;width: 28.2558%; height: 42px; text-align: left;&quot;&gt;+24.0%&amp;nbsp;(순간&amp;nbsp;최대&amp;nbsp;처리량&amp;nbsp;증가)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 21px; text-align: center;&quot;&gt;평균 응답시간&lt;/td&gt;
&lt;td style=&quot;width: 22.7907%; height: 21px; text-align: center;&quot;&gt;15.21&amp;nbsp;m s&lt;/td&gt;
&lt;td style=&quot;width: 23.9535%; height: 21px; text-align: center;&quot;&gt;12.46 ms&lt;/td&gt;
&lt;td style=&quot;width: 28.2558%; height: 21px; text-align: left;&quot;&gt;-18.1%&amp;nbsp;(응답&amp;nbsp;속도&amp;nbsp;향상)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 처리 용량 (Throughput) 22% 증가&lt;br /&gt;가장 큰 성과는 평균 TPS가 약 22% 증가한 것이다&lt;br /&gt;동일한 하드웨어 자원으로 같은 시간 동안 22% 더 많은 사용자 요청을 처리할 수 있게 되었음을 의미한다. &lt;br /&gt;풀 테이블 스캔으로 인한 불필요한 I/O 작업이 사라지면서, DB와 서버가 원래의 작업에 더 집중할 수 있게 되었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 사용자 경험&amp;nbsp;&lt;br /&gt;평균 응답 시간이 18% 감소하면서, 사용자는 검색 시 더 빠르고 쾌적한 반응 속도를 체감할수 있을것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 안정성&lt;br /&gt;&amp;nbsp;99명이 한계였던 시스템이 이제는 더 높은 수인 150명까지&amp;nbsp; 동시 접속을 에러 없이 처리할 수 있게 되었다&lt;br /&gt;데이터가가 더&amp;nbsp; 증가하더라도, 급격한 성능 저하 없이 안정적으로 서비스를 운영할 수 있을것 같다&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;style8&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;가상 사용자 500명&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;에러 발생&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1335&quot; data-origin-height=&quot;663&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqlvjn/dJMcajgXnOh/AQFNWgwBF32Q325hra04kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqlvjn/dJMcajgXnOh/AQFNWgwBF32Q325hra04kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqlvjn/dJMcajgXnOh/AQFNWgwBF32Q325hra04kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqlvjn%2FdJMcajgXnOh%2FAQFNWgwBF32Q325hra04kK%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;1335&quot; height=&quot;663&quot; data-origin-width=&quot;1335&quot; data-origin-height=&quot;663&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;Mysql&amp;nbsp;Profiling&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;MySQL 엔진이 쿼리 하나를 실행하는&amp;nbsp; 실제로 각 단계에서 얼마나 시간이 걸리는지 마이크로초 단위로 측정&lt;/blockquote&gt;
&lt;pre id=&quot;code_1772023615041&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show variables like '%profiling%';
set profiling=1;
set profiling_history_size=100;

SELECT *
FROM rent_complete
WHERE sgg_cd = '11215'
  AND umd_nm = '자양동'
  AND area BETWEEN 25 AND 36
order by contract_date DESC;

show profiles;  -- id : 18

-- 해당 쿼리문의 수행 시간을 더 상세한 단위로 확인
show profile for query 255;
show profile for query 344;&lt;/code&gt;&lt;/pre&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/bAb2m8/dJMcajgXoXD/clNmqzzFXKRGQF4VtWnWkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAb2m8/dJMcajgXoXD/clNmqzzFXKRGQF4VtWnWkK/img.png&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;460&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;49.79&quot; style=&quot;width: 49.2095%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAb2m8/dJMcajgXoXD/clNmqzzFXKRGQF4VtWnWkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAb2m8%2FdJMcajgXoXD%2FclNmqzzFXKRGQF4VtWnWkK%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;508&quot; height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/87Hhb/dJMcaaj4DCH/EhtmPE8gQXH0SCXCZPRYV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/87Hhb/dJMcaaj4DCH/EhtmPE8gQXH0SCXCZPRYV1/img.png&quot; data-origin-width=&quot;519&quot; data-origin-height=&quot;466&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6277%;&quot; data-widthpercent=&quot;50.21&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/87Hhb/dJMcaaj4DCH/EhtmPE8gQXH0SCXCZPRYV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F87Hhb%2FdJMcaaj4DCH%2FEhtmPE8gQXH0SCXCZPRYV1%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;519&quot; height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 적용 전: 0.076037 초 (약 76.0 ms)&lt;/li&gt;
&lt;li&gt;인덱스 적용 후: 0.001675 초 (약 1.7 ms)&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;절대 시간 감소: 약 74.3 ms의 시간이 단축
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;성능 향상률: (0.076037 - 0.001675) / 0.076037 * 100 &amp;asymp; 97.8%&lt;/li&gt;
&lt;li&gt;결론: 순수한 DB 쿼리 실행 속도가 약 98% 개선, 즉 약 45배 빨라졌다. (76.0 / 1.7 = 44.7058....&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 저수준(Low-Level)&amp;nbsp;성능&amp;nbsp;분석의&amp;nbsp;필요성&lt;br /&gt;nGrinder를 통한 부하 테스트는 API의 전체 응답 시간과 처리량을 측정하는 데 유용했다&lt;br /&gt;하지만 실제 병목이 발생하는 데이터베이스 내부의 동작을 직접적으로 보여주지는 못한다. &lt;br /&gt;쿼리 성능 개선 효과를 명확하게 증명하기 위해, MySQL의 내장 기능인 Profiling 을 사용하여 순수 SQL 실행 시간을 &lt;br /&gt;마이크로초 단위로 측정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&amp;nbsp;Profiling을&amp;nbsp;통한&amp;nbsp;병목&amp;nbsp;구간&amp;nbsp;측정 &lt;br /&gt;인덱스 적용 전, WHERE 절에 여러 조건이 포함된 검색 쿼리의 프로파일을 분석한 결과, 총 실행 시간은 약 76ms가 소요&lt;br /&gt;프로파일 상세 내역을 보면 &lt;b&gt;executing&lt;/b&gt; 같은 실제 데이터에 접근하여 WHERE 을 필터링하고 , 데이터를 가져오고 , 정렬하는 등&lt;br /&gt;쿼리의 가장 중요한 부분을 수행하는데 시간이 차이 가 많이난다 &lt;br /&gt;풀 테이블 스캔(Full Table Scan) 으로 인한 전형적인 성능 저하 패턴이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&amp;nbsp;인덱스&amp;nbsp;적용&amp;nbsp;후&amp;nbsp;성능&amp;nbsp;측정 &lt;br /&gt;검색&amp;nbsp;조건에&amp;nbsp;최적화된&amp;nbsp;(sgg_cd,&amp;nbsp;umd_nm,&amp;nbsp;area)&amp;nbsp;복합&amp;nbsp;인덱스를&amp;nbsp;생성한&amp;nbsp;후,&amp;nbsp;동일한&amp;nbsp;쿼리에&amp;nbsp;대해&amp;nbsp;다시&amp;nbsp;프로파일링을&amp;nbsp;실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;45배의 성능 향상 그 의미는? &lt;br /&gt;프로파일링 결과 순수 DB 쿼리 실행 시간이 76ms에서 1.7ms로 단축되어, 약 45배의 폭발적인 성능 향상을 이루었다&lt;br /&gt;nGrinder 테스트에서 TPS가 22% 향상된 것의 근본적인 원인이 바로 이 쿼리 자체의 최적화에 있음을 명확하게 증명한다. &lt;br /&gt;인덱스는 불필요한 디스크 I/O를 제거하고 MySQL이 필요한 데이터에 직접 접근하도록 유도하고 DB 엔진의 부하를&amp;nbsp; 줄였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 최적화는 단순히 단일 쿼리를 빠르게 만드는 것을 넘어&amp;nbsp; 제한된 DB 서버 자원으로 더 많은 동시 요청을 처리할 수 있게 만들어주고 전체 서비스의 처리 용량과 안정성 향상으로 직결된다&lt;/p&gt;</description>
      <category>Back_End/SpringBoot</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/288</guid>
      <comments>https://gi-dor.tistory.com/288#entry288comment</comments>
      <pubDate>Wed, 25 Feb 2026 22:01:04 +0900</pubDate>
    </item>
    <item>
      <title>다시 돌아온 페이징처리 SpringBoot + MyBatis // Pageable XX</title>
      <link>https://gi-dor.tistory.com/287</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;RentCompleteSearchDTO 이름을 RentSearchConditionDTO로 변경해야하는데 수정할곳이 많아서 그냥두기로함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;1. 검색 조건을 담을 DTO&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 에서 Controller 로 검색 조건들을 전달하기 위해 필요하다&lt;/p&gt;
&lt;pre id=&quot;code_1771855371934&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
public class RentCompleteSearchDTO {
    private String sggCd;       // 시군구 코드
    private String umdNm;       // 법정동 이름
    private String name;        // 건물명
    private Integer minDeposit; // 최소 보증금
    private Integer maxDeposit; // 최대 보증금
    private Integer minRent;    // 최소 월세
    private Integer maxRent;    // 최대 월세
}&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;2. 페이징 계산 처리해줄 DTO&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 데이터 갯수와 , 현재 페이지 번호를 알려주면 SQL 쿼리와 UI 에 필요한 모든 값 ( offset , beginPage, endPage 등등)&lt;br /&gt;자동 계산해준다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771855431604&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.realestate.rent_insight.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RentPaginationDTO {

    //  페이징 계산 담당일찐


    // 입력 받는 값
    private int totalRows;    // DB내의 전체 데이터        -- 쿼리타면 여기에 갯수 들어감
    private int currentPage;  // 사용자가 요청한 페이지 번호 --  URL의 ?page=1


    // 기본 설정 값
    private int rows = 30;    // 페이지당  계약건수 갯수
    private int pages = 5;    // 페이지네이션 바에 보여줄 페이지 갯수 (1 2 3 4 5)


    // 계산되어야하는 값
    private int totalPages;
    private int totalBlocks;
    private int currentBlock;   // 현재 페이지가 속한 블록 번호
    private int beginPage;      // 현재 블록 시작 페이지
    private int endPage;        // 현재 블록 끝 페이지

    private int offset;         // SQL 쿼리에 사용할 offset 값

    // 현재  첫번째 페이지 ,마지막 페이지 구분
    private boolean isFirst;
    private boolean isLast;

    public RentPaginationDTO() {
    }

    public RentPaginationDTO(int currentPage, int totalRows) {
        this.currentPage = currentPage;
        this.totalRows = totalRows;
        // 객체가 생성되면서 모든 페이징 값 계산 해버리기
        init();
    }

    private void init() {

        // 글이 존재하는지
        if (totalRows &amp;gt; 0) {
            // 전체 페이지 갯수 계산하기
            totalPages = (int) Math.ceil((double) totalRows / rows);    // 3000개 데이터 /  30행 = totalPages= 100개

            // (현재페이지 번호 -1) * 한 페이지당 보여줄 데이터
            offset = (currentPage - 1) * rows;

            // 페이징 바 계산기
            totalBlocks = (int)Math.ceil((double) totalPages / pages);
            currentBlock = (int) Math.ceil((double) currentPage / pages);
            beginPage = (currentBlock - 1) * pages + 1;
            endPage = currentBlock * pages;

            // 마지막 블럭의 끝 페이지 번호 - 총 12페이지일 때 3번째 블록(11, 12)의 끝 페이지는 15가 아니라 12가 되어야 함
            if (currentBlock == totalBlocks) {
                endPage = totalPages;
            }
            // 혹시 모를 상황 대비: endPage가 totalPages를 넘지 않도록
            if (endPage &amp;gt; totalPages) {
                endPage = totalPages;
            }
            isFirst = (currentPage == 1);
            isLast = (currentPage == totalPages);
        }
    }


}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 최종결과를 담아줄 DTO&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면으로 보낼 결과물은 데이터목록 + 페이징 정보 두가지다&amp;nbsp;&lt;br /&gt;이것을 처리할 DTO&lt;/p&gt;
&lt;pre id=&quot;code_1771855544093&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.realestate.rent_insight.dto;

import lombok.Getter;

import java.util.List;

@Getter
public class RentPaginationResultDTO&amp;lt;T&amp;gt; {

    private List&amp;lt;T&amp;gt; list;
    private RentPaginationDTO rentPaginationDTO;        // init() 계산끝난 페이징 정보객체

    public RentPaginationResultDTO(List&amp;lt;T&amp;gt; list, RentPaginationDTO rentPaginationDTO) {
        this.list = list;
        this.rentPaginationDTO = rentPaginationDTO;
    }
}&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;4. 데이터베이스에 실제 SQL 명력을 내릴 Mapper&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Param : 여러 개의 파라미터를 XML에 전달할 때 각각의 파라미터에 이름표 를 붙여준다&lt;/p&gt;
&lt;pre id=&quot;code_1771855614226&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.realestate.rent_insight.domain.mapper;

import com.realestate.rent_insight.domain.entity.RentComplete;
import com.realestate.rent_insight.dto.RentCompleteSearchDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * resources/mappers/RentCompleteMapper.xml 파일에 정의된 SQL 쿼리 연결
 */
@Mapper
public interface RentCompleteMapper {

    List&amp;lt;RentComplete&amp;gt; findByComplexConditions(
            @Param(&quot;searchDto&quot;) RentCompleteSearchDTO searchDto,
            @Param(&quot;limit&quot;) int limit,
            @Param(&quot;offset&quot;) int offset);


    int countByComplexConditions(@Param(&quot;searchDto&quot;) RentCompleteSearchDTO searchDto);
}&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;5. 실제 실행될 SQL 쿼리 작성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;lt;sql&amp;gt; 태그 를 사용하여 중복되는 WHERE 절 분리 (사실 같은거 길어서 꼴보기 싫었음)&lt;/li&gt;
&lt;li&gt;&amp;lt;if&amp;gt; 태그 검색 조건 (searchDto) 에 값이 있을때만 AND 조건 동적으로 추가한다&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;

&amp;lt;mapper namespace=&quot;com.realestate.rent_insight.domain.mapper.RentCompleteMapper&quot;&amp;gt;

    &amp;lt;select id=&quot;findByComplexConditions&quot;
            resultType=&quot;com.realestate.rent_insight.domain.entity.RentComplete&quot;&amp;gt;
        SELECT
            id,                             -- 계약 고유번호
            contract_date AS contractDate,  -- 계약일
            sgg_cd AS sggCd,                -- 시군구 코드
            sgg_nm AS sggNm,                -- 시군구 이름
            umd_nm AS umdNm,                -- 법정동 이름
            jibun,                          -- 지번 주소
            name,                           -- 오피스텔 단지명
            deposit,                        -- 보증금
            monthly_rent AS monthlyRent,    -- 월세
            area,                           -- 전용 면적
            build_year AS buildYear,        -- 건축 년도
            floor,                          -- 층수
            contract_term AS contractTerm,  -- 계약 기간
            contract_type AS contractType   -- 계약 종류 (신규/갱신)
        FROM
            rent_complete
        &amp;lt;include refid=&quot;whereCondition&quot;&amp;gt;&amp;lt;/include&amp;gt;
        ORDER BY contract_date DESC
        LIMIT #{limit} OFFSET #{offset}
    &amp;lt;/select&amp;gt;

    &amp;lt;sql id=&quot;whereCondition&quot;&amp;gt;
        &amp;lt;where&amp;gt;
            &amp;lt;if test=&quot;searchDto.sggCd != null and searchDto.sggCd != ''&quot;&amp;gt;
                AND sgg_cd = SUBSTRING(#{searchDto.sggCd}, 1, 5)
            &amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;searchDto.umdNm != null and searchDto.umdNm != ''&quot;&amp;gt;
                AND umd_nm = #{searchDto.umdNm}
            &amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;searchDto.name != null and searchDto.name != ''&quot;&amp;gt;
                AND name LIKE CONCAT('%', #{searchDto.name}, '%')
            &amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;searchDto.minDeposit != null and searchDto.maxDeposit != null&quot;&amp;gt;
                AND deposit BETWEEN #{searchDto.minDeposit} AND #{searchDto.maxDeposit}
            &amp;lt;/if&amp;gt;
            &amp;lt;if test=&quot;searchDto.minRent != null and searchDto.maxRent != null&quot;&amp;gt;
                AND monthly_rent BETWEEN #{searchDto.minRent} AND #{searchDto.maxRent}
            &amp;lt;/if&amp;gt;
        &amp;lt;/where&amp;gt;
    &amp;lt;/sql&amp;gt;

    &amp;lt;select id=&quot;countByComplexConditions&quot; resultType=&quot;int&quot;&amp;gt;
        SELECT COUNT(*)
        FROM rent_complete
        &amp;lt;include refid=&quot;whereCondition&quot; /&amp;gt;
    &amp;lt;/select&amp;gt;

&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 비즈니스 로직 처리 - 페이징 처리 모든 과정을 순서대로 사용한다고 보면된다&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package com.realestate.rent_insight.service;

import com.realestate.rent_insight.domain.entity.RentComplete;
import com.realestate.rent_insight.domain.mapper.RentCompleteMapper;
import com.realestate.rent_insight.dto.RentCompleteSearchDTO;
import com.realestate.rent_insight.dto.RentPaginationDTO;
import com.realestate.rent_insight.dto.RentPaginationResultDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 전월세 계약 정보를 검색기능
 * MyBatis 매퍼를 사용하여 복합적인 조건의 검색
 */
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // 기본적으로 읽기 전용 트랜잭션으로 실행
public class RentCompleteSearchService {

    private final RentCompleteMapper rentCompleteMapper;

    public RentPaginationResultDTO&amp;lt;RentComplete&amp;gt; searchRent(RentCompleteSearchDTO rentCompleteSearchDTO, int page) {
        int totalRows = rentCompleteMapper.countByComplexConditions(rentCompleteSearchDTO);
        RentPaginationDTO rentPaginationDTO = new RentPaginationDTO(page,totalRows);

        List&amp;lt;RentComplete&amp;gt; list = rentCompleteMapper.findByComplexConditions(
                rentCompleteSearchDTO,
                rentPaginationDTO.getRows(),
                rentPaginationDTO.getOffset()
        );
        return new RentPaginationResultDTO&amp;lt;&amp;gt;(list, rentPaginationDTO);
    }


}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 웹에서 사용자가 요청을 주면 , 서비스에게 일시키고 그 결과를 화면에 전달만 하는 컨트롤러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종결과를 Model 에 담아 HTML 파일에 보냄&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;package com.realestate.rent_insight.controller;

import com.realestate.rent_insight.domain.entity.DataUpdateLog;
import com.realestate.rent_insight.domain.entity.Region;
import com.realestate.rent_insight.domain.entity.RentComplete;
import com.realestate.rent_insight.domain.repository.DataUpdateLogRepository;
import com.realestate.rent_insight.dto.RentCompleteSearchDTO;
import com.realestate.rent_insight.dto.RentPaginationResultDTO;
import com.realestate.rent_insight.service.RegionService;//import com.realestate.rent_insight.service.RentSearchService;

import com.realestate.rent_insight.service.RentCompleteSearchService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;
import java.util.Optional;

@Controller
@RequiredArgsConstructor
@RequestMapping(&quot;/rent&quot;)
public class RentSearchController {

    private final RentCompleteSearchService rentCompleteSearchService;
    private final RegionService regionService;
    private final DataUpdateLogRepository dataUpdateLogRepository;

    @GetMapping(&quot;/search&quot;)
    public String searchForm(@ModelAttribute(&quot;searchDto&quot;) RentCompleteSearchDTO rentCompleteSearchDTO,
                             @RequestParam(value =&quot;page&quot; , defaultValue = &quot;1&quot;) int page,
                             Model model) {
        // 1. RegionService를 사용하여 시군구 목록(코드, 이름 포함)을 조회
        List&amp;lt;Region&amp;gt; sigunguList = regionService.getSigunguList();
        model.addAttribute(&quot;sigunguList&quot;, sigunguList);

        // 2. 서비스 계층을 호출하여 검색 조건에 맞는 데이터를 조회
        RentPaginationResultDTO rentSearchResult = rentCompleteSearchService.searchRent(rentCompleteSearchDTO,page);
        model.addAttribute(&quot;rentSearchResult&quot;, rentSearchResult);

        // 3. SUCCESS 로그 조회
        Optional&amp;lt;DataUpdateLog&amp;gt; lastLog = dataUpdateLogRepository.findFirstByStatusOrderByIdDesc(&quot;SUCCESS&quot;);
        if(lastLog.isPresent()) {
            DataUpdateLog log = lastLog.get();
            model.addAttribute(&quot;lastLog&quot;, log.getCompletionTime());
        }

        // 4. 뷰
        return &quot;rent/search&quot;;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;&amp;lt;!-- 페이지네이션 UI --&amp;gt;
&amp;lt;div class=&quot;card-footer bg-white&quot; th:if=&quot;${rentSearchResult.rentPaginationDTO.totalRows &amp;gt; 0}&quot;&amp;gt;
    &amp;lt;nav aria-label=&quot;Page navigation&quot;&amp;gt;
        &amp;lt;ul class=&quot;pagination justify-content-center mb-0&quot;&amp;gt;
            &amp;lt;!-- '처음' 버튼 --&amp;gt;
            &amp;lt;li class=&quot;page-item&quot; th:classappend=&quot;${rentSearchResult.rentPaginationDTO.first} ? 'disabled'&quot;&amp;gt;
                &amp;lt;a th:if=&quot;${!rentSearchResult.rentPaginationDTO.first}&quot; class=&quot;page-link&quot; th:href=&quot;@{/rent/search(page=1, sggCd=${searchDto.sggCd}, umdNm=${searchDto.umdNm}, name=${searchDto.name}, minDeposit=${searchDto.minDeposit}, maxDeposit=${searchDto.maxDeposit}, minRent=${searchDto.minRent}, maxRent=${searchDto.maxRent})}&quot;&amp;gt;&amp;amp;laquo;&amp;lt;/a&amp;gt;
                &amp;lt;span th:if=&quot;${rentSearchResult.rentPaginationDTO.first}&quot; class=&quot;page-link&quot;&amp;gt;&amp;amp;laquo;&amp;lt;/span&amp;gt;
            &amp;lt;/li&amp;gt;
            &amp;lt;!-- '이전' 버튼 --&amp;gt;
            &amp;lt;li class=&quot;page-item&quot; th:classappend=&quot;${rentSearchResult.rentPaginationDTO.currentPage == 1} ? 'disabled'&quot;&amp;gt;
                &amp;lt;a th:if=&quot;${rentSearchResult.rentPaginationDTO.currentPage &amp;gt; 1}&quot; class=&quot;page-link&quot; th:href=&quot;@{/rent/search(page=${rentSearchResult.rentPaginationDTO.currentPage - 1}, sggCd=${searchDto.sggCd}, umdNm=${searchDto.umdNm}, name=${searchDto.name}, minDeposit=${searchDto.minDeposit}, maxDeposit=${searchDto.maxDeposit}, minRent=${searchDto.minRent}, maxRent=${searchDto.maxRent})}&quot;&amp;gt;&amp;amp;lt;&amp;lt;/a&amp;gt;
                &amp;lt;span th:if=&quot;${rentSearchResult.rentPaginationDTO.currentPage == 1}&quot; class=&quot;page-link&quot;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;
            &amp;lt;/li&amp;gt;
            &amp;lt;!-- 페이지 번호 버튼 --&amp;gt;
            &amp;lt;li th:each=&quot;pageNumber : ${#numbers.sequence(rentSearchResult.rentPaginationDTO.beginPage, rentSearchResult.rentPaginationDTO.endPage)}&quot; class=&quot;page-item&quot; th:classappend=&quot;${pageNumber == rentSearchResult.rentPaginationDTO.currentPage} ? 'active'&quot;&amp;gt;
                &amp;lt;a class=&quot;page-link&quot; th:href=&quot;@{/rent/search(page=${pageNumber}, sggCd=${searchDto.sggCd}, umdNm=${searchDto.umdNm}, name=${searchDto.name}, minDeposit=${searchDto.minDeposit}, maxDeposit=${searchDto.maxDeposit}, minRent=${searchDto.minRent}, maxRent=${searchDto.maxRent})}&quot; th:text=&quot;${pageNumber}&quot;&amp;gt;1&amp;lt;/a&amp;gt;
            &amp;lt;/li&amp;gt;
            &amp;lt;!-- '다음' 버튼 --&amp;gt;
            &amp;lt;li class=&quot;page-item&quot; th:classappend=&quot;${rentSearchResult.rentPaginationDTO.currentPage == rentSearchResult.rentPaginationDTO.totalPages} ? 'disabled'&quot;&amp;gt;
                &amp;lt;a th:if=&quot;${rentSearchResult.rentPaginationDTO.currentPage &amp;lt; rentSearchResult.rentPaginationDTO.totalPages}&quot; class=&quot;page-link&quot; th:href=&quot;@{/rent/search(page=${rentSearchResult.rentPaginationDTO.currentPage + 1}, sggCd=${searchDto.sggCd}, umdNm=${searchDto.umdNm}, name=${searchDto.name}, minDeposit=${searchDto.minDeposit}, maxDeposit=${searchDto.maxDeposit}, minRent=${searchDto.minRent}, maxRent=${searchDto.maxRent})}&quot;&amp;gt;&amp;amp;gt;&amp;lt;/a&amp;gt;
                &amp;lt;span th:if=&quot;${rentSearchResult.rentPaginationDTO.currentPage == rentSearchResult.rentPaginationDTO.totalPages}&quot; class=&quot;page-link&quot;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/li&amp;gt;
            &amp;lt;!-- '마지막' 버튼 --&amp;gt;
            &amp;lt;li class=&quot;page-item&quot; th:classappend=&quot;${rentSearchResult.rentPaginationDTO.last} ? 'disabled'&quot;&amp;gt;
                &amp;lt;a th:if=&quot;${!rentSearchResult.rentPaginationDTO.last}&quot; class=&quot;page-link&quot; th:href=&quot;@{/rent/search(page=${rentSearchResult.rentPaginationDTO.totalPages}, sggCd=${searchDto.sggCd}, umdNm=${searchDto.umdNm}, name=${searchDto.name}, minDeposit=${searchDto.minDeposit}, maxDeposit=${searchDto.maxDeposit}, minRent=${searchDto.minRent}, maxRent=${searchDto.maxRent})}&quot;&amp;gt;&amp;amp;raquo;&amp;lt;/a&amp;gt;
                &amp;lt;span th:if=&quot;${rentSearchResult.rentPaginationDTO.last}&quot; class=&quot;page-link&quot;&amp;gt;&amp;amp;raquo;&amp;lt;/span&amp;gt;
            &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/nav&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Back_End/SpringBoot</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/287</guid>
      <comments>https://gi-dor.tistory.com/287#entry287comment</comments>
      <pubDate>Mon, 23 Feb 2026 23:13:28 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 로그인 실패 시 에러메세지 SimpleUrlAuthenticationFailureHandler</title>
      <link>https://gi-dor.tistory.com/286</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;423&quot; data-origin-height=&quot;45&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FPV2D/dJMcafllcZg/8NkMGa8AkRyvcqQkwI2KJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FPV2D/dJMcafllcZg/8NkMGa8AkRyvcqQkwI2KJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FPV2D/dJMcafllcZg/8NkMGa8AkRyvcqQkwI2KJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFPV2D%2FdJMcafllcZg%2F8NkMGa8AkRyvcqQkwI2KJ0%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;423&quot; height=&quot;45&quot; data-origin-width=&quot;423&quot; data-origin-height=&quot;45&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;563&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QtBJx/dJMcahjbHBI/quDOHEIx8Za17FSqiefv90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QtBJx/dJMcahjbHBI/quDOHEIx8Za17FSqiefv90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QtBJx/dJMcahjbHBI/quDOHEIx8Za17FSqiefv90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQtBJx%2FdJMcahjbHBI%2FquDOHEIx8Za17FSqiefv90%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;563&quot; height=&quot;640&quot; data-origin-width=&quot;563&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;로그인과 회원가입 기능을 Security로 구현했지만 로그인에 실패하면 사용자 로그인이 왜 안되는지 알수 없다&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;로그인에 실패할 경우 사용자에게 안내 문구를 제공해 왜 로그인이 되지 않는지 명확히 알려주는 것이 필요하다고 생각했다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아무런 실패 처리가 되어있지 않고 있는 상태&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771838144833&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -&amp;gt; csrf.disable())
                .authorizeHttpRequests(authorize -&amp;gt; authorize
                        .requestMatchers(&quot;/&quot;, &quot;/css/**&quot;, &quot;/js/**&quot;, &quot;/images/**&quot;, &quot;/members/login&quot;, &quot;/members/join&quot;).permitAll()
                        .requestMatchers(&quot;/members/mypage&quot;).authenticated()
                        .anyRequest().authenticated()
                )
                // 폼 기반 로그인 설정
                .formLogin(form -&amp;gt; form
                        .loginPage(&quot;/members/login&quot;)
                        .loginProcessingUrl(&quot;/members/login&quot;)
                        .usernameParameter(&quot;email&quot;)
                        .defaultSuccessUrl(&quot;/&quot;, true) // 로그인 성공 시 항상 메인 페이지로 이동
                        .permitAll() // 로그인 페이지 자체는 누구나 접근 가능
                )
                // 로그아웃 설정
                .logout(logout -&amp;gt; logout
                        .logoutUrl(&quot;/members/logout&quot;) // 로그아웃을 처리할 URL
                        .logoutSuccessUrl(&quot;/&quot;) // 로그아웃 성공 후 리다이렉트될 URL
                        .invalidateHttpSession(true) // 세션 무효화
                        .deleteCookies(&quot;JSESSIONID&quot;) // 쿠키 삭제
                );

        return http.build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;failureHandler()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;failureHandler() 메서드는&amp;nbsp; 추가해야 예외를 캐치할수 있다&lt;/li&gt;
&lt;li&gt;파라미터로 AuthenticationFailureHandler 인터페이스 구현체를 받는다고 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 처리를 위한 핸들러 - LoginFailHandler&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;로그인 실패에 대해서 관리하는 핸들러 AuthenticationFailureHandler 인터페이스를 구현함&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;AuthenticationFailureHandler&lt;span&gt; 인터페이스를 구현해야한다&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span&gt; &lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;AuthenticationFailureHandler&lt;span&gt; 인터페이스를 구현한 구현체인 SimpleUrlAuthenticationFailureHandler&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771838487640&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface AuthenticationFailureHandler {

	/**
	 * Called when an authentication attempt fails.
	 * @param request the request during which the authentication attempt occurred.
	 * @param response the response.
	 * @param exception the exception which was thrown to reject the authentication
	 * request.
	 */
	void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException;

}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException exception) throws IOException, ServletException {
    if (this.defaultFailureUrl == null) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(&quot;Sending 401 Unauthorized error since no failure URL is set&quot;);
        }
        else {
            this.logger.debug(&quot;Sending 401 Unauthorized error&quot;);
        }
        response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        return;
    }
    saveException(request, exception);
    if (this.forwardToDestination) {
        this.logger.debug(&quot;Forwarding to &quot; + this.defaultFailureUrl);
        request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
    }
    else {
        this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
    }
}

public void setDefaultFailureUrl(String defaultFailureUrl) {
    Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl),
            () -&amp;gt; &quot;'&quot; + defaultFailureUrl + &quot;' is not a valid redirect URL&quot;);
    this.defaultFailureUrl = defaultFailureUrl;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SimpleUrlAuthenticationFailureHandler 를 사용한 EU
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;setDefaultFailureUrl () 를 사용하기 위함&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;nbsp;로그인 실패시 URL 을 지정 해준다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;onAuthenticationFailure 메서드를 오버라이딩해서 예외처리 , 에러메시지를 띄운다&lt;/li&gt;
&lt;li&gt;한글 인코딩 깨지는 것을 방지 하기위해 UTF-8 처리 .&amp;nbsp; &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;URLEncoder.encode(errorMessage, &quot;UTF-8&quot;);&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771838689674&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String errorMessage = null;

        if (exception instanceof BadCredentialsException) {
            errorMessage = &quot;아이디와 비밀번호를 확인해주세요.&quot;;
        } else if (exception instanceof InternalAuthenticationServiceException) {
            errorMessage = &quot;내부 시스템 문제로 로그인할 수 없습니다. 관리자에게 문의하세요.&quot;;
        } else if (exception instanceof UsernameNotFoundException) {
            errorMessage = &quot;존재하지 않는 계정입니다.&quot;;
        } else {
            errorMessage = &quot;알 수없는 오류입니다.&quot;;
        }

        errorMessage = URLEncoder.encode(errorMessage, &quot;UTF-8&quot;);
        setDefaultFailureUrl(&quot;/members/login?error=&quot; + errorMessage);
        super.onAuthenticationFailure(request, response, exception);

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;super.onAuthenticationFailure(request, response, exception);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 동작을 수행하는 부분 부모 클래스인 SimpleUrlAuthenticationFailureHandler의 기본 동작을 그대로 호출&lt;/p&gt;
&lt;pre id=&quot;code_1771839282985&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소(defaultFailureUrl)로, 사용자에게 리다이렉트&lt;br /&gt;RedirectStrategy 는 브라우저에게 defaultFailureUrl로 재접속하라는 명령을 내린다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;LoginFailHandler는 defaultFailureUrl을 /members/login?error=에러메시지로 설정&lt;/li&gt;
&lt;li&gt;super.onAuthenticationFailure(...)를 호출했음&lt;/li&gt;
&lt;li&gt;1, 2, 3단계를 모두 건너뛰고, 마지막 4단계에 도달하여, 우리가 설정한 바로 그 URL로 리다이렉트를 실행시킨 것이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한대로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setDefaultFailureUrl() 사용&amp;nbsp; &amp;rarr; 로그인 실패 시, 다음에 이동할 URL은 /members/login?error=에러메시지&lt;/li&gt;
&lt;li&gt;onAuthenticationFailure() 오버라이딩&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;URLEncoder.encode(errorMessage, &quot;UTF-8&quot;); 사용 완료&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771838775840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public LoginFailHandler loginFailHandler() {
    return new LoginFailHandler();
}




// 폼 기반 로그인 설정
.formLogin(form -&amp;gt; form
        .loginPage(&quot;/members/login&quot;)
        .loginProcessingUrl(&quot;/members/login&quot;)
        .failureHandler(loginFailHandler())		// 로그인 실패
        .usernameParameter(&quot;email&quot;)
        .defaultSuccessUrl(&quot;/&quot;, true) // 로그인 성공 시 항상 메인 페이지로 이동
        .permitAll() // 로그인 페이지 자체는 누구나 접근 가능
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771839564985&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form th:action=&quot;@{/members/login}&quot; method=&quot;post&quot; th:object=&quot;${memberDTO}&quot;&amp;gt;
    &amp;lt;!-- 로그인 실패 시 에러 메시지 표시 --&amp;gt;
    &amp;lt;div th:if=&quot;${param.error}&quot; class=&quot;alert alert-danger&quot; th:text=&quot;${param.error}&quot; role=&quot;alert&quot;&amp;gt;
        에러 메시지 표시
    &amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;param&amp;nbsp; : 타임리프가 제공하는 객체&lt;/li&gt;
&lt;li&gt;error :&amp;nbsp; setDefaultFailureUrl(&quot;/members/login?error=&quot; + errorMessage); 에서 가져온 error&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HBWWM/dJMcafr6djC/sARG1a7BV42zypIv8Ne2UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HBWWM/dJMcafr6djC/sARG1a7BV42zypIv8Ne2UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HBWWM/dJMcafr6djC/sARG1a7BV42zypIv8Ne2UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHBWWM%2FdJMcafr6djC%2FsARG1a7BV42zypIv8Ne2UK%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;518&quot; height=&quot;658&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Back_End/SpringSecurity</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/286</guid>
      <comments>https://gi-dor.tistory.com/286#entry286comment</comments>
      <pubDate>Mon, 23 Feb 2026 18:41:37 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 로그인 / 로그아웃</title>
      <link>https://gi-dor.tistory.com/285</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;과거 : 직접 세션을 다루는 로그인&lt;/p&gt;
&lt;pre id=&quot;code_1771757002288&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   @PostMapping(&quot;/login&quot;)
    public String loginComplete(@Valid @ModelAttribute MemberDTO memberDTO,BindingResult bindingResult,HttpServletRequest request) {
        // DTO에서 정의한 입력값 검증하기
        if (bindingResult.hasErrors()) {
            return &quot;login&quot;; // login.html
        }

        // memberService에서 login 메서드 호출
        Member loginMember = memberService.login(memberDTO.getEmail(), memberDTO.getPassword());


        // 로그인 실패 처리 - 아이디 공백 , 비밀번호 틀릴경우
        if (loginMember == null) {
            bindingResult.reject(&quot;loginFail&quot;, &quot;아이디 또는 비밀번호가 맞지 않습니다.&quot;);
            log.warn(&quot;로그인 실패 : {} ,{}&quot;,memberDTO.getEmail(),memberDTO.getPassword());
            return &quot;login&quot;;
        }

        // 로그인 성공 (세션 생성)
        HttpSession session = request.getSession();
        session.setAttribute(&quot;loginMember&quot;, loginMember);

        return &quot;redirect:/&quot;;
    }
    
        // 로그아웃
    @PostMapping(&quot;/logout&quot;)
    public String logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        return &quot;redirect:/&quot;;

    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1771757029079&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 로그인하지 않은 경우 --&amp;gt;
&amp;lt;th:block th:if=&quot;${session.loginMember == null}&quot;&amp;gt;
    &amp;lt;a th:href=&quot;@{/members/login}&quot;&amp;gt;로그인&amp;lt;/a&amp;gt;
&amp;lt;/th:block&amp;gt;

&amp;lt;!-- 로그인한 경우 --&amp;gt;
&amp;lt;th:block th:if=&quot;${session.loginMember != null}&quot;&amp;gt;
    &amp;lt;form th:action=&quot;@{/members/logout}&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;로그아웃&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/th:block&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;로그인 : POST /members/login 요청은 Spring Security 가 가로채기에 MemberService 에서 loadUserByUserName을 통해 인증을 처리한다&lt;/li&gt;
&lt;li&gt;로그아웃 : POST /members/logout 요청은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Spring Security가 세션을 무효화 하고 쿠키 삭제를 해서 처리해준다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;상태확인 : HTML 에서 sec:authorize을 통해 컨트롤러 도움없이 로그인 상태를 확인할 수있다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에게 모든 것을 위임해버리기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771757296486&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1771757309774&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;
      xmlns:sec=&quot;http://www.thymeleaf.org/extras/spring-security&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetails와&amp;nbsp;UserDetailsService&lt;/p&gt;
&lt;pre id=&quot;code_1771757085866&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class) // 생성일/수정일 자동 관리
@Entity // JPA가 관리하는 엔티티가됨
@Getter // getter 필드 값 가져오기
@Table(name = &quot;member&quot;) // 테이블 이름을 명시적으로 지정
public class Member implements UserDetails { // Java에는 Member 객체있음 , DB에는 member 라는 테이블 있음 이걸 1:1 매핑해주는 Entity
    // UserDetails 상속받아서 인증객체로 사용


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 내가 건들이지 않고 DB가 알아서 증가시킴
    private Long id;        // 회원 고유 ID

    @Column(nullable = false, unique = true)        // 중복 불가
    private String email;   // 이메일 (로그인 ID)

    @Column(nullable = false)
    private String password;

    private String name;

    @Column(nullable = false, unique = true)
    private String nickname;    // 사용자 커뮤니티나 마이정보에서 사용할 닉네임

    // 나중에 카카오 로그인을 위해 미리 만들어둠 &quot;google&quot;, &quot;kakao&quot;, &quot;naver&quot; 등
    private String provider;

    // 사용자인지 , 관리자인지 구분 - 기본 회원가입시 사용자 default값 설정
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 10)
    private MemberRole memberRole;

    // 등록일
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    // 수정일
    @LastModifiedDate
    private LocalDateTime updatedAt;


    @Builder
    public Member(String email, String password, String name, String nickname, String provider, MemberRole memberRole) {
        this.email = email;
        this.password = password;
        this.name = name;
        this.nickname = nickname;
        this.provider = provider;
        this.memberRole = memberRole;
    }

    @Builder
    public Member(String email, String password) {
        this.email = email;
        this.password = password;
    }

    // 권한반환
    @Override
    @NonNull
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot;+ memberRole.name()));
    }

    // 사용자의 id를 반환 - 고유한값
    @Override
    @NonNull
    public String getUsername() {
        return this.email;
    }

    // 계정이 만료되었는지 확인
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정이 잠금되어있는 지 확인
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 패스워드 만료 확인
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정 사용 여부 확인
    @Override
    public boolean isEnabled() {
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771757166491&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor 
@Transactional(readOnly = true) /
public class MemberService implements UserDetailsService {

    private final MemberRepository memberRepository;
    // 비밀번호를 암호화하기 위한 도구 (SecurityConfig에 만들어둠)
    private final PasswordEncoder passwordEncoder;


    @NonNull
    @Override
    public UserDetails loadUserByUsername(@NonNull String email) {
        return memberRepository.findByEmail(email)
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;해당하는 이메일을 찾을 수 없습니다 : &quot; + email));
    }&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;SecurityConfig를&amp;nbsp;설정하여,&amp;nbsp;로그인/로그아웃&amp;nbsp;절차를&amp;nbsp;구체적으로&amp;nbsp;지시&lt;/p&gt;
&lt;pre id=&quot;code_1771757270703&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // http 객체를 사용하여 메서드 체이닝 방식으로 보안 설정을 구성합니다.
        http
                .csrf(csrf -&amp;gt; csrf.disable())
                .authorizeHttpRequests(authorize -&amp;gt; authorize
                        .requestMatchers(&quot;/&quot;, &quot;/css/**&quot;, &quot;/js/**&quot;, &quot;/images/**&quot;, &quot;/members/login&quot;, &quot;/members/join&quot;).permitAll()
                        .requestMatchers(&quot;/members/mypage&quot;).authenticated()
                        // 그외 페이지는 인증 요구할꺼임
                        .anyRequest().authenticated()
                )
                // 폼 기반 로그인 설정
                .formLogin(form -&amp;gt; form
                        .loginPage(&quot;/members/login&quot;) // 로그인 페이지 URL GET
                        .loginProcessingUrl(&quot;/members/login&quot;) // 로그인 처리 URL POST
                        .usernameParameter(&quot;email&quot;)
                        .defaultSuccessUrl(&quot;/&quot;, true) // 로그인 성공 시 항상 메인 페이지로 이동
                        .permitAll() // 로그인 페이지 자체는 누구나 접근 가능
                )
                // 로그아웃 설정
                .logout(logout -&amp;gt; logout
                        .logoutUrl(&quot;/members/logout&quot;) // 로그아웃을 처리할 URL
                        .logoutSuccessUrl(&quot;/&quot;) // 로그아웃 성공 후 리다이렉트될 URL
                        .invalidateHttpSession(true) // 세션 무효화
                        .deleteCookies(&quot;JSESSIONID&quot;) // 쿠키 삭제
                );

        return http.build();
    }



}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;.loginPage(&quot;/members/login&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;컨트롤러의 GET 요청과 매핑이라고 생각하면된다&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;.loginProcessingUrl(&quot;/members/login&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Security 가 POST 요청을 보고 , 중간에 가로채서 MemberSerivce 안에 있는 loadUserByUserName 을 호출&lt;/li&gt;
&lt;li&gt;컨트롤러 XXX&lt;/li&gt;
&lt;li&gt;&amp;nbsp;.loginProcessingUrl()에 설정된 URL은 Spring Security의 인증 필터를 깨우는 '가상의' 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 42px; text-align: center;&quot;&gt;설정&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 42px; text-align: center;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%; height: 42px; text-align: center;&quot;&gt;HTTP&amp;nbsp;메서드&lt;/td&gt;
&lt;td style=&quot;width: 20.9302%; height: 42px; text-align: center;&quot;&gt;매핑&amp;nbsp;대상&lt;/td&gt;
&lt;td style=&quot;width: 25.5814%; height: 42px; text-align: center;&quot;&gt;컨트롤러에&amp;nbsp;메서드&amp;nbsp;필요한가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 17px; text-align: center;&quot;&gt;.loginPage()&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 17px; text-align: center;&quot;&gt;로그인&amp;nbsp;화면&amp;nbsp;보기&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%; height: 17px; text-align: center;&quot;&gt;GET&lt;/td&gt;
&lt;td style=&quot;width: 20.9302%; height: 17px; text-align: center;&quot;&gt;MemberController&lt;/td&gt;
&lt;td style=&quot;width: 25.5814%; height: 17px; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 17px; text-align: center;&quot;&gt;.loginProcessingUrl()&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 17px; text-align: center;&quot;&gt;로그인&amp;nbsp;정보&amp;nbsp;제출/처리&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%; height: 17px; text-align: center;&quot;&gt;POST&lt;/td&gt;
&lt;td style=&quot;width: 20.9302%; height: 17px; text-align: center;&quot;&gt;Spring&amp;nbsp;Security&lt;/td&gt;
&lt;td style=&quot;width: 25.5814%; height: 17px; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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 id=&quot;code_1771757892644&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 로그인하지 않은 경우 --&amp;gt;
&amp;lt;!-- sec:isAnonymous() : 현재 사용자가 익명(비로그인) 상태인지 확인 --&amp;gt;
&amp;lt;th:block sec:authorize=&quot;isAnonymous()&quot;&amp;gt;
    &amp;lt;li class=&quot;nav-item ms-2&quot;&amp;gt;
        &amp;lt;a class=&quot;btn btn-outline-light btn-sm&quot; th:href=&quot;@{/members/login}&quot;&amp;gt;로그인&amp;lt;/a&amp;gt;
    &amp;lt;/li&amp;gt;
    &amp;lt;li class=&quot;nav-item ms-2&quot;&amp;gt;
        &amp;lt;a class=&quot;btn btn-primary btn-sm&quot; th:href=&quot;@{/members/join}&quot;&amp;gt;회원가입&amp;lt;/a&amp;gt;
    &amp;lt;/li&amp;gt;
&amp;lt;/th:block&amp;gt;

&amp;lt;!-- 로그인한 경우 --&amp;gt;
&amp;lt;!-- sec:isAuthenticated() : 현재 사용자가 인증(로그인)된 상태인지 확인 --&amp;gt;
&amp;lt;th:block sec:authorize=&quot;isAuthenticated()&quot;&amp;gt;
    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
        &amp;lt;!-- sec:authentication=&quot;name&quot; : 현재 인증된 사용자의 이름을 표시 --&amp;gt;
        &amp;lt;span class=&quot;nav-link text-white&quot;&amp;gt;환영합니다 &amp;lt;span sec:authentication=&quot;principal.nickname&quot;&amp;gt;&amp;lt;/span&amp;gt;님&amp;lt;/span&amp;gt;
    &amp;lt;/li&amp;gt;
    &amp;lt;li class=&quot;nav-item ms-2&quot;&amp;gt;
        &amp;lt;form th:action=&quot;@{/members/logout}&quot; method=&quot;post&quot; class=&quot;d-inline&quot;&amp;gt;
            &amp;lt;button type=&quot;submit&quot; class=&quot;btn btn-outline-light btn-sm&quot;&amp;gt;로그아웃&amp;lt;/button&amp;gt;
        &amp;lt;/form&amp;gt;
    &amp;lt;/li&amp;gt;
&amp;lt;/th:block&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1496&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n4TAR/dJMcafySET3/fOf4dO4DuR848oOG5lUdKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n4TAR/dJMcafySET3/fOf4dO4DuR848oOG5lUdKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n4TAR/dJMcafySET3/fOf4dO4DuR848oOG5lUdKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4TAR%2FdJMcafySET3%2FfOf4dO4DuR848oOG5lUdKk%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;1496&quot; height=&quot;153&quot; data-origin-width=&quot;1496&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1465&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FqZQH/dJMcafySEVJ/OFfljYwbE7y0cW9vS2mi4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FqZQH/dJMcafySEVJ/OFfljYwbE7y0cW9vS2mi4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FqZQH/dJMcafySEVJ/OFfljYwbE7y0cW9vS2mi4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFqZQH%2FdJMcafySEVJ%2FOFfljYwbE7y0cW9vS2mi4k%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;1465&quot; height=&quot;245&quot; data-origin-width=&quot;1465&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Back_End/SpringSecurity</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/285</guid>
      <comments>https://gi-dor.tistory.com/285#entry285comment</comments>
      <pubDate>Sun, 22 Feb 2026 19:58:50 +0900</pubDate>
    </item>
    <item>
      <title>[Jackson] API 필드명 @JsonProperty .araboja</title>
      <link>https://gi-dor.tistory.com/284</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 연동하거나 JSON/XML 데이터를 다룰 때 '필드 이름 불일치' 문제를 해결해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;필드 다 만들었는데, 왜 데이터가 안 들어오는거지 ?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot로 이번 사이드 프로젝트를 개발하면서 RestTemplate 으로 외부 API를 호출하고 그 결과를 DTO 로 변환했다. &lt;br /&gt;그런데 API 응답 데이터의 필드 이름과 내 DTO의 필드 이름이 미묘하게 달라서 데이터가 매핑되지 않고 null 값만 들어왔다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;API는 user_id라고 주는데&amp;nbsp; 내 DTO에는 Java 스타일인 userId라고 선언하거나&amp;nbsp; &lt;br /&gt;API가 offiNm처럼 알기 어려운 줄임말 ? 축약어 ? 를 사용하고 있을 때 문제가 발생한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;@JsonProperty , 데이터와 객체를 이어준다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;데이터(JSON/XML)의 필드 이름(Key)과 Java 객체의 필드 이름이 서로 다를 때, 이 둘을 정확하게 연결해주는 '매핑(Mapping) 정보' 역할을 합니다.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JsonProperty 이럴 때 사용하자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명명 규칙(Naming Convention)이 다를 때 &lt;br /&gt;데이터 형식과 프로그래밍 언어는 각자 선호하는 명명 규칙이 다르다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON/XML: snake_case (user_id), kebab-case (user-id) 등 &lt;br /&gt;Java: camelCase (userId) &lt;br /&gt;이럴 때 @JsonProperty를 사용하면 나만의 Java 코딩 스타일을 해치지 않으면서 데이터를 매핑할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771680086789&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class UserDto {
    // JSON의 &quot;user_id&quot;를 Java의 &quot;userId&quot; 필드에 매핑
    @JsonProperty(&quot;user_id&quot;)
    private String userId;

    // JSON의 &quot;user_name&quot;을 Java의 &quot;userName&quot; 필드에 매핑
    @JsonProperty(&quot;user_name&quot;)
    private String userName;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2. API의 필드 이름이 모호하거나 마음에 안 들 때
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개인 프로젝트 &amp;nbsp;rent-insight&amp;nbsp; 에서 겪었던 문제다&lt;/li&gt;
&lt;li&gt;공공데이터 API는 그들만의 기준으로 필드 이름을 제공한다&lt;/li&gt;
&lt;li&gt;excluUseAr: &quot;전용 면적&quot;이라는 뜻이지만, 처음 봤을때 이게 뭔가 ? 싶어서 API 문서를 까봤다&lt;/li&gt;
&lt;li&gt;offiNm: &quot;오피스텔 이름&quot;이라는 뜻이지만, 너무 축약되어 가독성이 떨어졌다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771680109763&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ApiRentDTO.java의 일부

// API의 모호한 'excluUseAr'를 'area'로 사용
@JsonProperty(&quot;excluUseAr&quot;)
private double area; // 전용면적

// API의 축약된 'offiNm'을 'name'으로 사용
@JsonProperty(&quot;offiNm&quot;)
private String name; // 오피스텔 이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3. 필드 이름으로 사용할 수 없는 문자가 포함될 때&lt;/li&gt;
&lt;li&gt;JSON의 key는 문자열이므로 하이픈(-)이나 공백을 포함할 수 있지만, Java의 변수 이름은 사용불가&lt;/li&gt;
&lt;li&gt;@JsonProperty는 이럴 때 사용하자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771680123996&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProductDto {
    @JsonProperty(&quot;product-id&quot;)
    private String productId;

    @JsonProperty(&quot;stock count&quot;)
    private int stockCount;
}&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;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;rent-insight 프로젝트에서 공공데이터 API 응답을 받기 위해 만든 ApiRentDTO는 @JsonProperty 를 잘 써먹었다&lt;/p&gt;
&lt;pre id=&quot;code_1771680147507&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ApiRentDTO {

    @JsonProperty(&quot;buildYear&quot;)
    private Integer buildYear;

    // API의 'excluUseAr' -&amp;gt;'area'
    @JsonProperty(&quot;excluUseAr&quot;)
    private double area;

    // API의 'offiNm' -&amp;gt; 'name'
    @JsonProperty(&quot;offiNm&quot;)
    private String name;

    // API 응답에서 &quot;18,000&quot; 처럼 쉼표가 포함된 문자열로 오기 때문에
    // 일단 String으로 받은 뒤, 나중에 서비스 계층에서 숫자로 가공함
    @JsonProperty(&quot;deposit&quot;)
    private String deposit;

    @JsonProperty(&quot;monthlyRent&quot;)
    private String monthlyRent;

    // --- 나머지 필드들 ---
    @JsonProperty(&quot;contractTerm&quot;)
    private String contractTerm;

    @JsonProperty(&quot;contractType&quot;)
    private String contractType;

    @JsonProperty(&quot;dealDay&quot;)
    private Integer dealDay;

    @JsonProperty(&quot;dealMonth&quot;)
    private Integer dealMonth;

    @JsonProperty(&quot;dealYear&quot;)
    private Integer dealYear;

    @JsonProperty(&quot;floor&quot;)
    private Integer floor;

    @JsonProperty(&quot;jibun&quot;)
    private String jibun;

    @JsonProperty(&quot;sggCd&quot;)
    private String sggCd;

    @JsonProperty(&quot;sggNm&quot;)
    private String sggNm;

    @JsonProperty(&quot;umdNm&quot;)
    private String umdNm;

    @JsonProperty(&quot;preDeposit&quot;)
    private String preDeposit;

	// 여기도 하려했는데 귀찮음
    @JsonProperty(&quot;useRRRight&quot;)
    private String useRRRight;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가독성 향상: 내 코드에서는 내가 스스로 이해하기 쉽게 개발 할수 있다&lt;/li&gt;
&lt;li&gt;유연성 확보: 외부 API의 필드명이 변경되더라도, DTO의 @JsonProperty 값만 수정하면 되므로 유지보수가 굿&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Back_End/SpringBoot</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/284</guid>
      <comments>https://gi-dor.tistory.com/284#entry284comment</comments>
      <pubDate>Sat, 21 Feb 2026 22:31:45 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security와 BCrypt 해싱</title>
      <link>https://gi-dor.tistory.com/282</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;사이드 프로젝트를 진행하면서 입력한 비밀번호 값이 데이터베이스에 화끈하게 다 보이고있는 상태다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암호화 되지 않은 야생의 비밀번호 그 자체이기 때문에 데이터베이스를&amp;nbsp; 누군가 탈취한다면 비밀번호가 그대로 저장되어있어&lt;br /&gt;DB가 유출된다면 모든 사용자의 비밀번호가 유출되는 상황이 올수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(과거 프로젝트 만들면서 DB 접속 호스트랑 비밀번호를 깃허브에 올려서 화끈하게 해킹 당한적이있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 스프링을 놓고 솔루션만 개발한지 시간이 지나 대부분 다 까먹어서 하나씩 알아가면서 하려고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 HttpSession 을 통해 로그인 로그아웃을 만들어놓은 상태이다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Spring&amp;nbsp;Security&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring으로 만든 웹 애플리케이션의 인증(Authentication)과 권한 부여(Authorization)를 전문적으로 담당하는 프레임워크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;역할
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증 (Authentication) : 사용자가 누구인지 확인하는 과정입니다. (예: 아이디/비밀번호로 로그인)&lt;/li&gt;
&lt;li&gt;권한 부여 , 인가 (Authorization) : 인증된 사용자가 어떤 작업을 수행할 수 있는지 허가해주는 과정. &lt;br /&gt;(예: '사용자 USER'는 사용자페이지만 접근 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;왜 쓰는지 ? 우리가 직접 로그인/로그아웃, 세션 관리, 페이지 접근 제어 같은 복잡하고 실수하기 쉬운 보안 로직을 만들 필요 없이, Spring Security가 제공하는 표준적이고 안전한 방법을 그대로 사용하기 위해&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;BCrypt&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호를 안전하게 암호화하기 위해 사용하는 해시(Hash) 함수 중 하나이다. Spring Security에서 기본으로 권장하는 방식&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단방향 해시 : 비밀번호를 암호화된 문자열(해시 값)로 바꿀 수는 있지만, 암호화된 문자열을 다시 원래 비밀번호로 되돌릴 수는 절대 없다. (관리자도 모름 , ** 가장 중요 **)&lt;br /&gt;해커가 DB를 탈취해도 원래 비밀번호를 알아낼 수 없&lt;/li&gt;
&lt;li&gt;솔트 (Salt) 사용: 같은 비밀번호를 입력해도, 암호화할 때마다 매번 다른 결과가 나온다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜?&amp;nbsp; 암호화 시 '솔트' 라는 임의의 데이터를 비밀번호에 섞어서 암호화하기 때문&amp;nbsp;&lt;br /&gt;해커들이 미리 계산된 해시 값 목록(레인보우 테이블)을 사용한 공격을 할 수 없게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;어떻게 로그인 처리해?:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 로그인 시 입력한 '날것'의 비밀번호와 DB에 저장된 '암호화된' 비밀번호를 BCrypt에게 함께 전달. &lt;br /&gt;그러면 BCrypt가 알아서 &quot;이 두 개가 같은 비밀번호가 맞는지&quot; 검증하여 true 또는 false를 알려준다. &lt;br /&gt;우리는 그 결과만 믿고 사용하면 된다&lt;/li&gt;
&lt;li&gt;검증(Verification) &lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,2,0&quot;&gt;:&lt;/b&gt; 두 문자열이 같은지 ==로 비교하는 것이 아니라, BCrypt가 제공하는 matches() 기능을 통해 원본 비밀번호와 저장된 해시값이 수학적으로 일치하는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SecurityConfig 파일&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 개발 초기이니 전부다 허가 해놓는다&lt;/p&gt;
&lt;pre id=&quot;code_1770205560346&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class SecurityConfig {

// @Bean: 이 메서드가 반환하는 객체(PasswordEncoder)를 Spring 컨테이너가 관리하는 Bean으로 등록
// BCrypt 암호화 도구를 생성해서 Spring에 등록.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    
       @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // http 객체를 사용하여 메서드 체이닝 방식으로 보안 설정을 구성합니다.
        http.csrf(csrf -&amp;gt; csrf.disable())
        .authorizeHttpRequests(authorize -&amp;gt; authorize.requestMatchers(&quot;/**&quot;).permitAll());
        // 개발 편의상 일단 로그인을 안 한 사람이든 한 사람이든 상관없이 검사하지 말고 들여보낸다
        return http.build();
    }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MemberService 변경전&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770206716664&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberService {

	@Transactional
    public Long join(String email, String password, String name ,String nickname) {
        validateDuplicateEmail(email);
        validateDuplicateNickName(nickname);

        //  member 객체 생성할때 createAt은 null로 들어감
        // repository.save 호출
        // DB 넣기 전에 @EntityListeners 가 작동해서 저장할때 현재 시간을 주입해줌
        Member member = Member.builder()
                .email(email)
                .password(password)
                .name(name)
                .nickname(nickname)
                .memberRole(MemberRole.USER)
                .build();

        Member savedMember = memberRepository.save(member);
        return savedMember.getId();
    }
    
    public Member login(String email, String password) {
        Member member =  memberRepository.findByEmail(email);

        if(member == null) {
            return null;
        }

        if(!member.getPassword().equals(password)) {
            return null;
        }

        return member;
	}


	    /**
     * 이메일 중복 체크
     */
    private void validateDuplicateEmail(String email) {
        if (memberRepository.existsByEmail(email)) {
            throw new DuplicateFieldException(&quot;email&quot;,&quot;이미 존재하는 회원(이메일)입니다.&quot;);
        }
    }

    /**
     * 닉네임 중복 체크
     */
    private void validateDuplicateNickName(String nickname) {
        if (memberRepository.existsByNickname(nickname)) {
            throw new DuplicateFieldException(&quot;nickname&quot;,&quot;이미 존재하는 닉네임 입니다.&quot;);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MemberService 변경후&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770206763198&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberService {


	@Transactional // 이 메서드는 데이터베이스에 정보를 저장(쓰기)하므로, @Transactional을 붙여서 쓰기 모드로 설정합니다.
	public Long join(String email, String password, String name ,String nickname) {
        // 호옥시 이미 가입된 이메일이나 닉네임인지 확인하기 (중복 체크)
        validateDuplicateEmail(email);
        validateDuplicateNickName(nickname);

        //  Member(회원) 객체를 만듬
        Member member = Member.builder()
                .email(email)
                // 사용자가 입력한 비밀번호를 그대로 저장하면 해킹 시 위험
                // 비밀번호를 아무도 알아볼 수 없는 암호문으로 바꿔준다 개꿀. (예: $2a$10$...)
                .password(passwordEncoder.encode(password))
                .name(name)
                .nickname(nickname)
                .memberRole(MemberRole.USER) // 처음 가입하는 사람은 모두 일반 사용자(USER)
                .build();

        // 완성된 회원 정보를 데이터베이스에 저장(save)
        Member savedMember = memberRepository.save(member);

        // 데이터베이스에 저장된 회원의 ID 번호를 알려준다
        return savedMember.getId();
    }
    
     public Member login(String email, String password) {
        // 사용자가 입력한 이메일로 데이터베이스에서 회원 찾기
        Member member =  memberRepository.findByEmail(email);

        // 만약 그런 이메일을 가진 회원이 없다면, 당연히 로그인 실패이므로 null을 반환하고 끝낸다
        if(member == null) {
            return null;
        }

        // 데이터베이스에 저장된 암호문(member.getPassword())과
        // 사용자가 방금 입력한 날것의 비밀번호(password)가 서로 짝이 맞는지 확인
        // equals() , ==  이딴거 쓰기 XXXX   passwordEncoder.matches() 사용
        if(!passwordEncoder.matches(password, member.getPassword())) {
            // 만약 짝이 맞지 않는다면, 비밀번호가 틀린 것이므로 null을 반환하고 끝낸다
            return null;
        }

        // 이메일도 존재하고 비밀번호도 짝이 맞는다면 로그인 성공
        // 찾았던 회원 정보를 통째로 반환
        return member;
    }


}&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;size18&quot;&gt;&lt;b&gt; 왜 PasswordEncoder가 필요한가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호는 &lt;b&gt;절대 DB에 그대로 저장하면 안 된다.&lt;/b&gt;&lt;br /&gt;DB가 유출되는 순간, 모든 사용자의 계정이 즉시 위험해진다&lt;/p&gt;
&lt;p data-end=&quot;383&quot; data-start=&quot;266&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Spring Security에서는 비밀번호를 저장할 때&lt;br /&gt;&lt;b&gt;단방향 해시(Hash) 함수&lt;/b&gt;를 사용하도록 강제하고 있고 그 역할을 담당하는 인터페이스가 바로 PasswordEncoder&lt;/p&gt;
&lt;p data-end=&quot;420&quot; data-start=&quot;385&quot; data-ke-size=&quot;size16&quot;&gt;PasswordEncoder는 다음 두 가지 역할을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;494&quot; data-start=&quot;422&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;454&quot; data-start=&quot;422&quot;&gt;사용자가 입력한 &lt;b&gt;평문 비밀번호를 해시값으로 변환&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;494&quot; data-start=&quot;455&quot;&gt;로그인 시 입력한 비밀번호가 &lt;b&gt;저장된 해시값과 일치하는지 검증&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;609&quot; data-start=&quot;496&quot; data-ke-size=&quot;size16&quot;&gt;즉, 애플리케이션 코드에서&lt;br /&gt;&amp;ldquo;어떤 해시 알고리즘을 쓸지&amp;rdquo;, &amp;ldquo;salt를 어떻게 처리할지&amp;rdquo;를 직접 고민하지 않도록 &lt;br /&gt;Spring Security가 제공하는 &lt;b&gt;보안 추상화 계층&lt;/b&gt;이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;609&quot; data-start=&quot;496&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;609&quot; data-start=&quot;496&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; BCryptPasswordEncoder를 사용하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;753&quot; data-start=&quot;654&quot; data-ke-size=&quot;size16&quot;&gt;BCryptPasswordEncoder는 PasswordEncoder의 구현체 중 하나로,&lt;br /&gt;비밀번호 저장에 특화된 &lt;b&gt;단방향 해시 알고리즘 BCrypt&lt;/b&gt;를 사용&lt;/p&gt;
&lt;p data-end=&quot;774&quot; data-start=&quot;755&quot; data-ke-size=&quot;size16&quot;&gt;BCrypt의 특징은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;876&quot; data-start=&quot;776&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;802&quot; data-start=&quot;776&quot;&gt;내부적으로 &lt;b&gt;랜덤 salt&lt;/b&gt;를 자동 생성&lt;/li&gt;
&lt;li data-end=&quot;834&quot; data-start=&quot;803&quot;&gt;동일한 비밀번호라도 &lt;b&gt;매번 다른 해시 결과&lt;/b&gt; 생성&lt;/li&gt;
&lt;li data-end=&quot;876&quot; data-start=&quot;835&quot;&gt;연산 비용(cost)을 조절할 수 있어 &lt;b&gt;무차별 대입 공격에 강함&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;960&quot; data-start=&quot;878&quot; data-ke-size=&quot;size16&quot;&gt;따라서 Spring Security 환경에서는 별도의 이유가 없다면 BCryptPasswordEncoder를 사용하는 것이 표준&lt;/p&gt;
&lt;p data-end=&quot;960&quot; data-start=&quot;878&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;비밀번호 저장과정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770208298820&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String encodedPassword = passwordEncoder.encode(rawPassword);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;encode() 메서드는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;평문 비밀번호&lt;/li&gt;
&lt;li&gt;랜덤 salt&lt;/li&gt;
&lt;li&gt;Bcrypt 알고리즘&amp;nbsp; 들을 사용해 복호화 불가능한 해시 문자열을 생성한다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; matches()는 어떻게 동작하는가 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 시에는 비밀번호를 다시 복호화해서 비교하지 않는다.&lt;br /&gt;애초에 BCrypt는 &lt;b&gt;복호화 자체가 불가능한 알고리즘&lt;/b&gt;이기 때문&lt;/p&gt;
&lt;pre id=&quot;code_1770208395919&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;passwordEncoder.matches(rawPassword, encodedPassword);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DB에 저장된 encodedPassword에서 salt 값과 cost 값 추출&lt;/li&gt;
&lt;li&gt;사용자가 입력한 rawPassword에 동일한 salt + 동일한 cost로 다시 해시 수행&lt;/li&gt;
&lt;li&gt;새로 계산된 해시결과와 DB에 저장된 해시값 비교&lt;/li&gt;
&lt;li&gt;같으면 true , 틀리면 false 반환&lt;/li&gt;
&lt;li&gt;단순 문자열 비교 XXXXXXXX&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; 왜 equals()로 비교하면 안 되는가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770208481533&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;passwordEncoder.encode(rawPassword).equals(encodedPassword);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCrypt 는 랜덤 salt 를 사용하므로 같은 비밀번호 여도 equals() 결과는 매번 다르다&lt;br /&gt;그러므로 항상 실패 , 반드시 matches() 메서드 사용하기&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-end=&quot;2035&quot; data-start=&quot;2027&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2197&quot; data-start=&quot;2037&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2085&quot; data-start=&quot;2037&quot;&gt;PasswordEncoder는 비밀번호 해시와 검증을 책임지는 보안 인터페이스다&lt;/li&gt;
&lt;li data-end=&quot;2123&quot; data-start=&quot;2086&quot;&gt;BCrypt는 복호화가 불가능한 &lt;b&gt;단방향 해시 알고리즘&lt;/b&gt;이다&lt;/li&gt;
&lt;li data-end=&quot;2161&quot; data-start=&quot;2124&quot;&gt;로그인 시에는 복호화가 아니라 &lt;b&gt;재해시 후 비교&lt;/b&gt;를 수행한다&lt;/li&gt;
&lt;li data-end=&quot;2197&quot; data-start=&quot;2162&quot;&gt;비밀번호 비교는 반드시 matches()를 사용해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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-origin-width=&quot;1170&quot; data-origin-height=&quot;167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhiNhk/dJMcacveH1c/HQTM2Qj61DZArEXMydiI2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhiNhk/dJMcacveH1c/HQTM2Qj61DZArEXMydiI2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhiNhk/dJMcacveH1c/HQTM2Qj61DZArEXMydiI2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhiNhk%2FdJMcacveH1c%2FHQTM2Qj61DZArEXMydiI2k%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;1170&quot; height=&quot;167&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;167&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bazMrc/dJMb99L0bA9/PAlOo9ioZWyzjk8dKSV9wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bazMrc/dJMb99L0bA9/PAlOo9ioZWyzjk8dKSV9wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bazMrc/dJMb99L0bA9/PAlOo9ioZWyzjk8dKSV9wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbazMrc%2FdJMb99L0bA9%2FPAlOo9ioZWyzjk8dKSV9wK%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;612&quot; height=&quot;143&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;143&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;br /&gt;$2a$&amp;nbsp; :&amp;nbsp; 사용할 BCrypt 알고리즘의 버전&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$10$ : &amp;nbsp;비용(Cost) 또는 작업량(Work Factor)을 의미한다고 함 , 10은 일반적 권장되는 기본값\&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sDgalEi17wU7GzWCuy565utADxB8.Qvkd025H2iAH0dD37UjVYUEO&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔트(Salt) : 앞부분 22글자(sDgalEi17wU7GzWCuy565u)는 암호화 시 사용된 '임의의 소금' 값&lt;br /&gt;솔트&amp;nbsp;덕분에&amp;nbsp;같은&amp;nbsp;비밀번호라도&amp;nbsp;매번&amp;nbsp;다른&amp;nbsp;암호문이&amp;nbsp;생성&lt;br /&gt;&lt;br /&gt;해시 결과(Hashed Result) : 뒷부분 31글자(tADxB8.Qvkd025H2iAH0dD37UjVYUEO)가&amp;nbsp;&lt;br /&gt;사용자의 비밀번호와 솔트를 조합한 해시 계산을 한 최종 결과물&lt;/p&gt;</description>
      <category>Back_End/SpringSecurity</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/282</guid>
      <comments>https://gi-dor.tistory.com/282#entry282comment</comments>
      <pubDate>Wed, 4 Feb 2026 21:25:55 +0900</pubDate>
    </item>
    <item>
      <title>gidor_Helper 메모</title>
      <link>https://gi-dor.tistory.com/270</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/gi-dor/gidor_Helper&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/gi-dor/gidor_Helper&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1747208771779&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 - gi-dor/gidor_Helper: C# 자동화 프로그램&quot; data-og-description=&quot;C# 자동화 프로그램 . Contribute to gi-dor/gidor_Helper development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/gi-dor/gidor_Helper&quot; data-og-url=&quot;https://github.com/gi-dor/gidor_Helper&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LAG0r/hyYTaIUwYP/fdYstxGxj9l5KgDL5RrEW1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/boqM5U/hyYRnCDWI4/WmPSr0Yn5v6Wnck2f4fcQ1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/gi-dor/gidor_Helper&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/gi-dor/gidor_Helper&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LAG0r/hyYTaIUwYP/fdYstxGxj9l5KgDL5RrEW1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/boqM5U/hyYRnCDWI4/WmPSr0Yn5v6Wnck2f4fcQ1/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 - gi-dor/gidor_Helper: C# 자동화 프로그램&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;C# 자동화 프로그램 . Contribute to gi-dor/gidor_Helper development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 폼이 로드될 때 데이터베이스 연결을 시도하고 연결에 성공, 실패 여부 메세지를 보여준다&lt;br /&gt;또한 버튼 클릭시 데이터를 조회하는 코드이다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725496061471&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace Helper
{
    public partial class CommonCod : Form
    {

        // 데이터베이스 연결
        public static String uid = &quot;&quot;;
        public static String password = &quot;&quot;;  // 비밀번호가 없으므로 빈 문자열
        public static String database = &quot;&quot;;
        public static String server = &quot;&quot;;

        public static String conStr = $&quot;Server={server}; Database={database}; User Id={uid}; Password={password}; Integrated Security=True&quot;;
        SqlConnection conn = new SqlConnection(conStr);
        
    private void CommonCod_Load(object sender, EventArgs e)
      {
         try
            {
                conn.Open();
                MessageBox.Show(&quot;Success , DB 연결 되었습니다  &quot;, &quot;DB 연결메세지&quot;, MessageBoxButtons.OK, MessageBoxIcon.Information);

                // 해당 컬럼의 ReadOnly 속성 true 
                cod_dataGridView1.ReadOnly = true;
                cod_dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;

                } catch(Exception ex){
                	MessageBox.Show($&quot;DB 연결실패 \r\n Error : {ex.Message}&quot;, &quot;DB Error&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
      }
        
	private void CodeManageButton_Click(object sender, EventArgs e)
        {
            try
            {
             
                String sqlQuery = &quot; SELECT * FROM dbo.COD order by COD_NO ASC&quot;;
                // SQL 쿼리를 실행하기 위한 객체
                SqlCommand cmd = new SqlCommand(sqlQuery, conn);

                // SqlDataAdapter 는 DB에서 데이터를 가져와 Data Table을 채운다
                SqlDataAdapter dataAdapter = new SqlDataAdapter(cmd);

                // 가져온 데이터를 담기
                DataTable dataTable = new DataTable();

                // 데이터 어댑터를 통해 데이터를 데이터 테이블에 채움
                dataAdapter.Fill(dataTable);

                // 데이터 테이블을 DataGridView에 연결
                cod_dataGridView1.DataSource = dataTable;
            }
            catch (Exception ex)
            {
                MessageBox.Show($&quot;데이터 조회 실패 \r\n Error : {ex.Message}&quot;, &quot;DB Error&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
        
    }
        
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에 연결이 열리면 , 연결을 끊는 로직이 필요 할수가 있다&amp;nbsp;&lt;br /&gt;연결을 끊지 않으면 리소스가 소모되므로 , 폼이 닫힐 때 연결을 해제하는것이 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을위해 2가지 방법이 있다고 한다 using 과 Dispose()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SqlConnection과 SqlCommand는 둘 다 Dispose()를 구현한 객체지만 사용자가 Dispose()를 호출하지 않았기 때문에 finalizer가 호출될 때까지 메모리에 남게 된다.&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;using { } 구문은 코드의 블럭이 끝나면 자동으로 Dispose()를 호출한다 , 이것을 통해 Dispose() 를 호출하지 않아도&amp;nbsp;&lt;br /&gt;안전하게 리소스를 해제할수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;using{ } 구문을 사용해 코드 수가 감소함&lt;/p&gt;
&lt;pre id=&quot;code_1725508961862&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace Helper
{
    public partial class CommonCod : Form
    {
        // 데이터베이스 연결
        public static String uid = &quot;&quot;;
        public static String password = &quot;&quot;;  // 비밀번호가 없으므로 빈 문자열
        public static String database = &quot;&quot;;
        public static String server = &quot;&quot;;

        public static String conStr = $&quot;Server={server}; Database={database}; User Id={uid}; Password={password}; Integrated Security=True&quot;;

        public CommonCod()
        {
            InitializeComponent();
        }

        private void CommonCod_Load(object sender, EventArgs e)
        {
            try
            {
                using (SqlConnection conn = new SqlConnection(conStr))
                {
                    conn.Open();
                    MessageBox.Show(&quot;Success, DB 연결 되었습니다&quot;, &quot;DB 연결 메시지&quot;, MessageBoxButtons.OK, MessageBoxIcon.Information);

                    // 해당 컬럼의 ReadOnly 속성 true
                    cod_dataGridView1.ReadOnly = true;
                    cod_dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show($&quot;DB 연결 실패 \r\n Error: {ex.Message}&quot;, &quot;DB Error&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void CodeManageButton_Click(object sender, EventArgs e)
        {
            try
            {
                String sqlQuery = &quot;SELECT * FROM dbo.COD ORDER BY COD_NO ASC&quot;;
                
                // using 구문을 사용하여 SqlConnection을 자동으로 닫음
                using (SqlConnection conn = new SqlConnection(conStr))
                {
                    SqlCommand cmd = new SqlCommand(sqlQuery, conn);
                    SqlDataAdapter dataAdapter = new SqlDataAdapter(cmd);
                    DataTable dataTable = new DataTable();
                    dataAdapter.Fill(dataTable);

                    // 데이터 테이블을 DataGridView에 연결
                    cod_dataGridView1.DataSource = dataTable;
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show($&quot;데이터 조회 실패 \r\n Error: {ex.Message}&quot;, &quot;DB Error&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명시적인 Disposs()&lt;/p&gt;
&lt;pre id=&quot;code_1725509104190&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace Helper
{
    public partial class CommonCod : Form
    {
        // 데이터베이스 연결
        public static String uid = &quot;;
        public static String password = &quot;&quot;;  // 비밀번호가 없으므로 빈 문자열
        public static String database = &quot;&quot;;
        public static String server = &quot;&quot;;

        public static String conStr = $&quot;Server={server}; Database={database}; User Id={uid}; Password={password}; Integrated Security=True&quot;;

        public CommonCod()
        {
            InitializeComponent();
        }

        private void CommonCod_Load(object sender, EventArgs e)
        {
       	try{
        	  MessageBox.Show(&quot;불러오기 성공&quot;, &quot;정보&quot;, MessageBoxButtons.OK, MessageBoxIcon.Information);
            
                // 해당 컬럼의 ReadOnly 속성 true
                cod_dataGridView1.ReadOnly = true;
                cod_dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
            }
            catch (Exception ex)
            {
                MessageBox.Show($&quot;DB 연결 실패 \r\n Error: {ex.Message}&quot;, &quot;DB Error&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void CodeManageButton_Click(object sender, EventArgs e)
        {
            SqlConnection conn = null;

            try
            {
                String sqlQuery = &quot;SELECT * FROM dbo.COD ORDER BY COD_NO ASC&quot;;
                conn = new SqlConnection(conStr);
                SqlCommand cmd = new SqlCommand(sqlQuery, conn);
                SqlDataAdapter dataAdapter = new SqlDataAdapter(cmd);
                DataTable dataTable = new DataTable();
                dataAdapter.Fill(dataTable);

                // 데이터 테이블을 DataGridView에 연결
                cod_dataGridView1.DataSource = dataTable;
            }
            catch (Exception ex)
            {
                MessageBox.Show($&quot;데이터 조회 실패 \r\n Error: {ex.Message}&quot;, &quot;DB Error&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                // 연결을 닫고 자원을 해제
                if (conn != null)
                {
                    conn.Dispose();  // 명시적으로 Dispose() 호출
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1725498578835&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using (SqlConnection conn = new SqlConnection(conStr))
{
    conn.Open();
    // 데이터베이스 작업 수행
}  // using 블록을 벗어나면 자동으로 Dispose()가 호출됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dispose()&amp;nbsp; 수동으로 리소스를 해제 하기 위해 코드를 명시적으로 작성해 호출한다&lt;/p&gt;
&lt;pre id=&quot;code_1725498571443&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SqlConnection conn = new SqlConnection(conStr);

try
{
    conn.Open();
    // 데이터베이스 작업 수행
}
finally
{
    // 리소스를 수동으로 해제
    if (conn != null)
    {
        conn.Dispose();  // 명시적으로 Dispose() 호출
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** C#&amp;nbsp; GC에 대해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#은 가비지 컬렉터가 메모리를 자동으로 관리한다&amp;nbsp;&lt;br /&gt;대신 필요없는 클래스의 인스턴스를 메모리에서 바로 지우는게 아니라 조건이 될 때까지 기다렸다가 지우기 때문에 &lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스를 지웠다고 해도 그게 실제로 바로 삭제되는 것은 아니다.&lt;/span&gt; &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Dispose를 호출하는 걸 깜박하면 어떻게 될까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럴 경우에는 GC에서 해당 인스턴스를 제거할 때, (소멸자에서) Dispose를 호출해준다.&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;다만 다음과 같은 부작용이 생길 수 있다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1.GC가 해당 인스턴스를 언제 지울 지 알 수 없다.&lt;br /&gt;&amp;nbsp;Dispose가 언제 호출될지 알 수 없다. 애플리케이션이 종료될 때까지 호출이 안될 수도 있다. 중요한 리소스를 사용하거나, 메모리를 많이 먹는 클래스라면 위험할 수도 있겠다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2.GC가 해당 인스터스를 지울 때 Dispose를 호출하게 되면 그 순간 부하가 걸린다.&lt;br /&gt;즉, 랜덤하게 랙을 유발할 수도 있다. 성능에 예민한 앱이라면 주의가 필요하다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3.GC가 해당 인스턴스를 지울 때 Dispose에서 예외가 발생하면 어플 자체가 크래시 할 수도 있다.&lt;br /&gt;&amp;nbsp;수동으로 Dispose를 할 때에는 try/catch로 보호할 수 있지만, 자동제거일 경우 예외를 잡을 수 없기 때문에 매우 위험할 수도 있다&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자바의 가비지 컬렉션은 프로그래머가 GC를 호출할수 없으며 , JVM이 메모리 상황을 모니터링하다가 필요시 GC를 자동으로 실행한다&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;C# 에서 가비지 컬렌셕도 메모리가 부족하거나 특정조건이 될 때 GC가 실행되며 직접적인 컨트롤없이 처리를한다&lt;br /&gt;다만 GC.Collect()를&amp;nbsp; 메서드를 호출하면 프로그래머가 GC를 유발할수 있지만 시스템에 부하를 준다고 한다&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;자바는 기본적으로 시스템이 메모리를 관리하고 하며 , 개발자는 신경쓰지 않게 설계 되어있다&lt;br /&gt;C#은 개발자에게 약간의 메모리 관리 권한을 부여하며 Dispose() 와 using { } 구문을 통해 리소스를 해체하는 패턴을 사용&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725508828263&quot; class=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;자원을 다룰 때 using 키워들 이용하자
- using 키워드를 이용하면 괄호( {} )를 빠져나올 때 자동으로 Dispose를 해준다.
여기서 자원이란 파일, 폰트 등과 같은 Resource를 말한다.&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;프로젝트에서 데이터베이스와의 연결을 효과적으로 관리하기 위해, using 구문을 사용하여 SqlConnection 객체를 처리하였습니다. using 구문을 사용하면 데이터베이스 연결과 같은 리소스가 자동으로 해제되므로, 연결을 수동으로 닫는 것을 잊을 염려가 없으며, 리소스 누수 문제를 예방할 수 있습니다. 이를 통해 코드의 안정성과 유지보수성을 향상시킬 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 예시:&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1725509593022&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void CodeManageButton_Click(object sender, EventArgs e)
{
    try
    {
        String sqlQuery = &quot;SELECT * FROM dbo.COD ORDER BY COD_NO ASC&quot;;

        using (SqlConnection conn = new SqlConnection(conStr))
        {
            conn.Open();

            SqlCommand cmd = new SqlCommand(sqlQuery, conn);
            SqlDataAdapter dataAdapter = new SqlDataAdapter(cmd);
            DataTable dataTable = new DataTable();
            dataAdapter.Fill(dataTable);

            cod_dataGridView1.DataSource = dataTable;
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show($&quot;데이터 조회 실패 \r\n Error : {ex.Message}&quot;, &quot;DB Error&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 사항:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자동 리소스 관리:&lt;/b&gt; using 구문을 활용하여 SqlConnection 객체를 생성하고 사용 후 자동으로 리소스를 해제합니다. 이는 메모리 누수 및 리소스 낭비를 방지합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 처리:&lt;/b&gt; 데이터베이스와의 상호작용 중 발생할 수 있는 예외를 적절히 처리하여 사용자에게 친절한 에러 메시지를 제공하고, 프로그램의 안정성을 높였습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폼 종료 시 연결 해제:&lt;/b&gt; 폼이 닫힐 때 데이터베이스 연결을 안전하게 종료하도록 using 구문을 사용하면 SqlConnection 객체는 블록이 끝나면 자동으로 닫히기 때문에, FormClosing 이벤트에서 별도로 연결을 해제하는 코드를 작성할 필요가 없습니다 블록을 벗어나면 자동으로 Dispose() 메서드가 호출되어 리소스가 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 CodeManageButton_Click 메서드와 같은 경우에는 SqlConnection을 using 구문으로 처리하여 리소스 관리를 자동으로 할 수 있으며, 폼이 닫힐 때 별도로 연결을 해제할 필요는 없습니다.&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;p data-ke-size=&quot;size16&quot;&gt;dateGridView에서 해당컬럼 선택시 해당하는 컬럼의 값만 복사가 되었는데&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;엑셀 작업으로 데이터를 전송하는 경우가 자주 발생해 해당 컬럼 선택시 해당하는 행의 모든 컬럼의 값을 가져오기 위해&lt;br /&gt;SelectionMode의 값을 FullColumnSelect를 선택된 행의 전부선택되게 설정했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&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;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnkj1N/btsJsRkubqn/WWAy3y2dikOpH4hxxKlCYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnkj1N/btsJsRkubqn/WWAy3y2dikOpH4hxxKlCYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnkj1N/btsJsRkubqn/WWAy3y2dikOpH4hxxKlCYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnkj1N%2FbtsJsRkubqn%2FWWAy3y2dikOpH4hxxKlCYK%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;1240&quot; height=&quot;599&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;599&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dataGridView에서 DB에 맞게 해당 테이블의 컬럼을 설정하여 만들었다&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-origin-width=&quot;1283&quot; data-origin-height=&quot;851&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNSpXA/btsJsxfu4ZO/ZOY7kgAeSwdUlxsisgBxo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNSpXA/btsJsxfu4ZO/ZOY7kgAeSwdUlxsisgBxo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNSpXA/btsJsxfu4ZO/ZOY7kgAeSwdUlxsisgBxo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNSpXA%2FbtsJsxfu4ZO%2FZOY7kgAeSwdUlxsisgBxo1%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;1283&quot; height=&quot;851&quot; data-origin-width=&quot;1283&quot; data-origin-height=&quot;851&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P0ptV/btsJrBwu00k/CSyQByM646VQYn1vJ0QC5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P0ptV/btsJrBwu00k/CSyQByM646VQYn1vJ0QC5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P0ptV/btsJrBwu00k/CSyQByM646VQYn1vJ0QC5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP0ptV%2FbtsJrBwu00k%2FCSyQByM646VQYn1vJ0QC5K%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;1448&quot; height=&quot;672&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. winform 에서 dataGridView에 설정한 컬럼을 삭제한뒤에 사용&lt;br /&gt;&amp;rarr; 사용자 입장으로서 최악의 선택이다 버튼을&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-origin-width=&quot;1231&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rAQ1c/btsJr0WRhuc/6njqKtk7wHC4P2mTNKS7fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rAQ1c/btsJr0WRhuc/6njqKtk7wHC4P2mTNKS7fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rAQ1c/btsJr0WRhuc/6njqKtk7wHC4P2mTNKS7fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrAQ1c%2FbtsJr0WRhuc%2F6njqKtk7wHC4P2mTNKS7fk%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;1231&quot; height=&quot;691&quot; data-origin-width=&quot;1231&quot; data-origin-height=&quot;691&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. dataGridView에 컬럼을 작성 후에 조회 클릭이벤트를 발생하는 메서드에&lt;br /&gt;&amp;nbsp;cod_dataGridView1.Columns.Clear(); 를 사용해 기존에있던 컬럼들을 모두 삭제하고&amp;nbsp;&lt;br /&gt;데이터를 삽입한다&amp;nbsp;&lt;br /&gt;이렇게하면 초기화면에서 사용자는 컬럼명들을 볼수 있고&amp;nbsp;&lt;br /&gt;조회 이벤트 발생시 중복된 컬럼없이 알맞은 데이터들을 볼수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 코드로 컬럼을 생성하지 않고 winform에서 속성창을 이용해서 컬럼을 만들기에 더 쉽고 간편하고 코드의 양이 줄어든다&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BCDuj/btsJCDNiW69/SdqetUNabLZcKoNDCNkYdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BCDuj/btsJCDNiW69/SdqetUNabLZcKoNDCNkYdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BCDuj/btsJCDNiW69/SdqetUNabLZcKoNDCNkYdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBCDuj%2FbtsJCDNiW69%2FSdqetUNabLZcKoNDCNkYdk%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;1288&quot; height=&quot;798&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;798&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;dataGrideView1 에서 송장을 클릭하게되면 해당 송장번호에 맞는 상세 정보들을 볼수 있다&lt;br /&gt;하단에 비어있는 행을 클릭후 삭제 요청시&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uQciX/btsJBSYUysH/LemZuPBE09EisYHyDiaB91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uQciX/btsJBSYUysH/LemZuPBE09EisYHyDiaB91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uQciX/btsJBSYUysH/LemZuPBE09EisYHyDiaB91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuQciX%2FbtsJBSYUysH%2FLemZuPBE09EisYHyDiaB91%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;1194&quot; height=&quot;292&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;String INV_NO = ScanDataGridView2.SelectedRows[0].Cells[&quot;INV_NO&quot;].Value.ToString();&lt;br /&gt;&lt;br /&gt;System.Windows.Forms.DataGridViewCell.Value.get이(가) null을(를) 반환했습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;System.NullReferenceException은 Value 속성이 null인 경우 발생한다&lt;br /&gt;ToString() 메서드를 호출하기 전에 Value가 null인지 확인하는 것이 필요하다&lt;br /&gt;지금까지 사용했던 String.IsNullOrWhiteSpace는 null 값뿐만 아니라 빈 문자열이나 공백 문자열도 처리할 수 있지만, Value 속성이 null인 경우에는 ToString()을 호출하기 전에 확인이 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 예외를 처리하기 위해 2가지 방법이있다&lt;br /&gt;1. Value?.ToString()&lt;br /&gt;2. ScanDataGridView2에 AllowUserToAddRTows&amp;nbsp; 속성을 FALSE 로 설정한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Value?.ToString() 를 사용하게되면&amp;nbsp; Value의 값이 null이라면 ToString() 메서드가 호출되지 않아서&lt;br /&gt;INV_NO는 null 이되며 NULL 참조 오류를 방지할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AllowUserToAddRTows&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp; 속성은 행의 추가 옵션이 사용자에게 표시되는지 여부를 보여주는 속성이다&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;ScanDataGridView2에 AllowUserToAddRTows&amp;nbsp; 속성의 기본값은 TRUE 이지만&amp;nbsp; FALSE 로 설정하게되면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKOv49/btsJC02nZid/Jk1xj9XEpfaUoPRWf7ulCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKOv49/btsJC02nZid/Jk1xj9XEpfaUoPRWf7ulCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKOv49/btsJC02nZid/Jk1xj9XEpfaUoPRWf7ulCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKOv49%2FbtsJC02nZid%2FJk1xj9XEpfaUoPRWf7ulCK%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;1156&quot; height=&quot;414&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;414&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 INSERT 를 하는 기능을 구현한다면 Value?.ToString() 을 사용해야 하지만&lt;br /&gt;해당 프로그램은 ScanData에 대해 데이터 추가기능은 제한 하기에 늘어나는 코드양도 없으며 손쉽게 구현할수 있기에 &lt;br /&gt;&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;AllowUserToAddRTows&amp;nbsp;&lt;span&gt; 속성을 선택했다&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre id=&quot;code_1727168287621&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String sqlQuery = &quot;SELECT top 100000 &quot; +
                        &quot; A.INV_NO       as '송장번호'   ,&quot; +
                        &quot; A.BRA_ID       as '영업소'   , &quot; +
                        &quot; A.SCANN_SLT    as '스캔 상태'   , &quot; +
                        &quot; B.COD_CONT     as '코드 내용'   , &quot; +
                        &quot; A.SCANN_DATE   as '스캔 일자'  , &quot; +
                        &quot; A.SCANN_TIME   as '스캔 시간'   , &quot; +
                        &quot; A.CAR_ID       as '배송 차량'   , &quot; +
                        &quot; A.SCANN_USR_ID as '스캔 ID'   , &quot; +
                        &quot; A.TRS_ID       as '처리자 ID'   , &quot; +
                        &quot; A.TRS_NAME     as '처리자 명'   , &quot; +
                        &quot; A.TRS_DATE     as '처리 일자'&quot; +
                     &quot; FROM SLIS_MASTER.dbo.LS101T0 A &quot; +
                     &quot; INNER JOIN SLIS_MASTER.dbo.LS901T0 B &quot; +
                     &quot; ON A.SCANN_SLT = B.COD &quot;+
                     &quot; ORDER BY A.SCANN_DATE DESC , A.SCANN_TIME DESC&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Language/C#</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/270</guid>
      <comments>https://gi-dor.tistory.com/270#entry270comment</comments>
      <pubDate>Thu, 5 Sep 2024 13:15:18 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] 스토리지 엔진</title>
      <link>https://gi-dor.tistory.com/266</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 배포 당시 MySQL8.xxx 버전을 사용했으며 취준당시 &lt;br /&gt;면접 예상 질문으로 5버전과 8 버전대의 차이점에 대한 알아보면서&amp;nbsp; 취업 이후 어느새 까먹어버려서&amp;nbsp; 간단하게 &lt;br /&gt;기록 해보려고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스토리지 엔진 ?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 엔진 또는 스토리지 엔진으로 부르며 , 데이터를 조회 , 추가 , 수정 , 삭제 하는 담당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 에서는 여러 스토리지 엔진을 지원한다고 한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InnoDB 엔진&lt;/li&gt;
&lt;li&gt;MyISAM 엔진&lt;/li&gt;
&lt;li&gt;Memory 엔진&lt;/li&gt;
&lt;li&gt;Archive 엔진&lt;/li&gt;
&lt;li&gt;CSV 엔진&lt;/li&gt;
&lt;li&gt;Federated 엔진&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 프로젝트를 통해 사용했던 8.xx 버전의 사용된 InnoDB 엔진과 그 전 버전인 MyISAM엔진에 대해알아보려고한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MyISAM&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2009.12월 까지 기본엔진으로 MyISAM이 사용 되었다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;COMMIT , ROLLBACK 같은 충돌 복구 기능을 가진다&lt;/li&gt;
&lt;li&gt;트랜잭션을 제공하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;전체적인 속도가 InnoDB보다 빠르다&lt;/li&gt;
&lt;li&gt;SELECT 작업 시 속도가 빨라서 읽기 작업에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터 무결성에 대한 보장이 되지 않는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;T&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;&lt;b&gt;able-level Lock을 사용&lt;/b&gt;하기 때문에 갱신 작업 속도가 느리다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 데이터 조회가 자주 발생하는 시스템에서 유용하다. 대체로 티스토리 블로그 , 뉴스 , 통계 시스템 같은 읽기 작업이 주된 곳에서 쓰이면 유용하다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;InnoDB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5.5 버전 이후&lt;/b&gt;부터는 InnoDB 를 기본엔진으로 사용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;MyISAM과 마찬가지로 COMMIT , ROLLBACK 충돌 복구 기능을 가진다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 키를 지원한다 Foreign Key&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Row- Level Lock 을 제공&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;트랜잭션을 제공하며 ACID 속성을 보장한다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;원자성(Atomicity),&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span data-ved=&quot;2ahUKEwiZmpj-0_iHAxWPfvUHHZiXBYsQmpgGegQIJBAD&quot; data-width=&quot;250&quot; data-theme=&quot;0&quot; data-send-open-event=&quot;true&quot; data-hover-open-delay=&quot;500&quot; data-hover-hide-delay=&quot;1000&quot; data-extra-container-classes=&quot;ZLo7Eb&quot; data-enable-toggle-animation=&quot;true&quot;&gt;&lt;span data-segment-text=&quot;일관성&quot; data-bubble-link=&quot;&quot;&gt;일관성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;(Consistency), 격리(Isolation)&amp;nbsp; 지속성(Durability)&lt;/b&gt; &lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;MyISAM 보다 COMMIT , ROLLBACK 충돌 복구 기능이 뛰어나다&lt;/li&gt;
&lt;li&gt;Row Level Lock을 사용하기에 갱신 작업 속도가 빠르다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;시스템의 자원을 많이 사용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자상거래 &lt;span&gt;eCommerce 같이 구매,결제,재고파악등 다양한 트랜잭션이 발생하는 환경&lt;br /&gt;금융서비스 , 커뮤니티사이트 ( 작성 , 댓글 등 읽기 쓰기 수정 작업이 혼합된 환경 )에 사용 하기 적합하다&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;T&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;able Level Lock&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;테이블에 대한 잠금방식을 의미&lt;/li&gt;
&lt;li&gt;테이블 전체를 잠그며 , 어떤 작업이 테이블에 있는 데이터에 접근할 때 , 모든 테이블 전체가 잠긴다&lt;/li&gt;
&lt;li&gt;하나의 쓰기 작업이 진행중이라면 다른 모든 읽기와 쓰기에 대한 작업이 대기해야한다.&lt;/li&gt;
&lt;li&gt;커뮤니티같은 익명의 여러 사람들이 글을 작성하거나 읽을시 문제가 생긴다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Row Level Lock&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;특정 행만 잠근다 , 특정 행만 잠그기에 다른행은 여전히 접근이 가능하다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;A라는 사용자가 하나의 행을 수정 할 때 , A 사용자는 다른 행을 잠글 필요가 없다&lt;/li&gt;
&lt;li&gt;여러 사용자가 동시에 서로 다른 행을 수정하거나 읽을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User 테이블이 있고 user_id 와 user_name 컬럼이 있다&lt;br /&gt;여러 사용자가 동시에 회원 정보인 user_name을 수정한다면&amp;nbsp;&lt;br /&gt;MyISAM 같은 경우 user_id = 1 인 사용자가 user_name을 수정하는동안 다른 사용자는 테이블에서 읽기와 쓰기 작업을 할수 없다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDM 경우 user_id = 1 인 사용자가 user_name을 수정하는 동안 다른 사용자인 user_id = 2 인 사용자는&lt;br /&gt;user_name을 수정 할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;간단하게 두 엔진의 차이점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FK 지원유무&lt;/li&gt;
&lt;li&gt;트랜잭션 지원유무&lt;/li&gt;
&lt;li&gt;Table Lock 과 Row Lock&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724921421350&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 
	d.department_name, 
	e.name AS manager_name
FROM departments d , employees 
WHERE d.manager_id = e.id;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;JOIN 은 외래 키 제약이 없더라도 두 테이블의 관련 데이터를 조회할 수 있다.&lt;/li&gt;
&lt;li&gt;외래 키가 설정된 경우, 데이터의 무결성을 유지하면서 JOIN 연산을 수행&lt;/li&gt;
&lt;li&gt;쿼리 작성 방법은 외래 키가 있는 경우와 없는 경우 동일하지만, 외래 키는 데이터의 정확성과 일관성을 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 무결성은 DB에서 데이터를 정확하고 일관되게 유지하는 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@mooh2jj/Mysql-InnoDB-vs-MyISAM&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@mooh2jj/Mysql-InnoDB-vs-MyISAM&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1723780336453&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;[MySQL] - InnoDB vs MyISAM &amp;amp; 5.7 vs 8.0&quot; data-og-description=&quot;InnoDB는 사용자의 데이터를 보호하고 내결함성을 제공하는 commit, rollback 및 충돌 복구 기능과 같은 기능을 갖춘 MySQL용 transaction-safe (ACID 호환)스토리지 엔진이다.row-level locking을 사용하며 non-lockin&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@mooh2jj/Mysql-InnoDB-vs-MyISAM&quot; data-og-url=&quot;https://velog.io/@mooh2jj/Mysql-InnoDB-vs-MyISAM&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qkwm0/hyWOfTSIuO/SkhF9KUIMQdvaSoku9G8C1/img.jpg?width=1020&amp;amp;height=426&amp;amp;face=0_0_1020_426,https://scrap.kakaocdn.net/dn/yjUU3/hyWOpIVa2D/yoNbG8aOS5h7zpO2G8QtR1/img.jpg?width=1020&amp;amp;height=426&amp;amp;face=0_0_1020_426,https://scrap.kakaocdn.net/dn/FfWro/hyWScnBjsG/VgKAKEh6znasHq8Szm3POK/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=0_0_1440_1440&quot;&gt;&lt;a href=&quot;https://velog.io/@mooh2jj/Mysql-InnoDB-vs-MyISAM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@mooh2jj/Mysql-InnoDB-vs-MyISAM&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qkwm0/hyWOfTSIuO/SkhF9KUIMQdvaSoku9G8C1/img.jpg?width=1020&amp;amp;height=426&amp;amp;face=0_0_1020_426,https://scrap.kakaocdn.net/dn/yjUU3/hyWOpIVa2D/yoNbG8aOS5h7zpO2G8QtR1/img.jpg?width=1020&amp;amp;height=426&amp;amp;face=0_0_1020_426,https://scrap.kakaocdn.net/dn/FfWro/hyWScnBjsG/VgKAKEh6znasHq8Szm3POK/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=0_0_1440_1440');&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;[MySQL] - InnoDB vs MyISAM &amp;amp; 5.7 vs 8.0&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;InnoDB는 사용자의 데이터를 보호하고 내결함성을 제공하는 commit, rollback 및 충돌 복구 기능과 같은 기능을 갖춘 MySQL용 transaction-safe (ACID 호환)스토리지 엔진이다.row-level locking을 사용하며 non-lockin&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&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>DataBase</category>
      <author>기돌</author>
      <guid isPermaLink="true">https://gi-dor.tistory.com/266</guid>
      <comments>https://gi-dor.tistory.com/266#entry266comment</comments>
      <pubDate>Thu, 29 Aug 2024 17:57:41 +0900</pubDate>
    </item>
  </channel>
</rss>