<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>deftkang의 IT 블로그</title>
    <link>https://deftkang.tistory.com/</link>
    <description>deftkang의 IT 블로그</description>
    <language>ko</language>
    <pubDate>Wed, 15 Apr 2026 11:58:13 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>deftkang</managingEditor>
    <item>
      <title>[Oauth2] Slack api 사용 콘솔 설정및 이벤트 구독 설정</title>
      <link>https://deftkang.tistory.com/303</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack app 생성 URL :&amp;nbsp;&lt;a href=&quot;https://api.slack.com/apps&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://api.slack.com/apps&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 탭 Basic Information에서 APP ID, ClientID, Cleint Secrit 데이터를 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;787&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duW1Ey/btsObmnZtZo/BMMWhp3GotHeVfRVKbMYK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duW1Ey/btsObmnZtZo/BMMWhp3GotHeVfRVKbMYK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duW1Ey/btsObmnZtZo/BMMWhp3GotHeVfRVKbMYK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduW1Ey%2FbtsObmnZtZo%2FBMMWhp3GotHeVfRVKbMYK1%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;1006&quot; height=&quot;787&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;787&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;Redirect URL은 왼쪽 탭 OAtuh &amp;amp; Permissions에서 Redriect URLs 에서 추가 하면된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhKNoo/btsObpShqjH/F6VBI6J6yyGW6lDfaKCLI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhKNoo/btsObpShqjH/F6VBI6J6yyGW6lDfaKCLI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhKNoo/btsObpShqjH/F6VBI6J6yyGW6lDfaKCLI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhKNoo%2FbtsObpShqjH%2FF6VBI6J6yyGW6lDfaKCLI1%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;1014&quot; height=&quot;1012&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;1012&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;그리고 App에서 사용할 Scopes를 세팅한다.&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;709&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfwaxJ/btsOcBEcqUD/MZLsgW3WFHcnrQdeqblqyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfwaxJ/btsOcBEcqUD/MZLsgW3WFHcnrQdeqblqyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfwaxJ/btsOcBEcqUD/MZLsgW3WFHcnrQdeqblqyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfwaxJ%2FbtsOcBEcqUD%2FMZLsgW3WFHcnrQdeqblqyK%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;709&quot; height=&quot;666&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;666&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;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_channels:history&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/channels:history&quot;&gt;&lt;span&gt;channels:history&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View messages and other content in a user&amp;rsquo;s public channels&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_channels:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/channels:read&quot;&gt;&lt;span&gt;channels:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View basic information about public channels in a workspace&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_channels:write&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/channels:write&quot;&gt;&lt;span&gt;channels:write&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Manage a user&amp;rsquo;s public channels and create new ones on a user&amp;rsquo;s behalf&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_channels:write.topic&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/channels:write.topic&quot;&gt;&lt;span&gt;channels:write.topic&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Set the description of public channels&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_chat:write&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/chat:write&quot;&gt;&lt;span&gt;chat:write&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Send messages on a user&amp;rsquo;s behal&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_emoji:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/emoji:read&quot;&gt;&lt;span&gt;emoji:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View custom emoji in a workspace&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_files:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/files:read&quot;&gt;&lt;span&gt;files:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View files shared in channels and conversations that a user has access to&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_groups:history&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/groups:history&quot;&gt;&lt;span&gt;groups:history&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View messages and other content in a user&amp;rsquo;s private channels&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_groups:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/groups:read&quot;&gt;&lt;span&gt;groups:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View basic information about a user&amp;rsquo;s private channels&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_groups:write&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/groups:write&quot;&gt;&lt;span&gt;groups:write&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Manage a user&amp;rsquo;s private channels and create new ones on a user&amp;rsquo;s behalf&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_groups:write.topic&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/groups:write.topic&quot;&gt;&lt;span&gt;groups:write.topic&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Set the description of private channels&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_im:history&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/im:history&quot;&gt;&lt;span&gt;im:history&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View messages and other content in a user&amp;rsquo;s direct messages&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_im:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/im:read&quot;&gt;&lt;span&gt;im:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View basic information about a user&amp;rsquo;s direct messages&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_im:write&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/im:write&quot;&gt;&lt;span&gt;im:write&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Start direct messages with people on a user&amp;rsquo;s behalf&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_mpim:history&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/mpim:history&quot;&gt;&lt;span&gt;mpim:history&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View messages and other content in a user&amp;rsquo;s group direct messages&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_mpim:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/mpim:read&quot;&gt;&lt;span&gt;mpim:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View basic information about a user&amp;rsquo;s group direct messages&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_mpim:write&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/mpim:write&quot;&gt;&lt;span&gt;mpim:write&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Start group direct messages with people on a user&amp;rsquo;s behalf&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_mpim:write.topic&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/mpim:write.topic&quot;&gt;&lt;span&gt;mpim:write.topic&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Set the description in group direct messages&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_reactions:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/reactions:read&quot;&gt;&lt;span&gt;reactions:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View emoji reactions in a user&amp;rsquo;s channels and conversations and their associated content&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_reactions:write&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/reactions:write&quot;&gt;&lt;span&gt;reactions:write&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Add and edit emoji reactions on a user's behalf&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_remote_files:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/remote_files:read&quot;&gt;&lt;span&gt;remote_files:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View remote files added by the app in a workspace&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_team:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/team:read&quot;&gt;&lt;span&gt;team:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View the name, email domain, and icon for workspaces a user is connected to&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_users:read&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/users:read&quot;&gt;&lt;span&gt;users:read&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View people in a workspace&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_users:read.email&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/users:read.email&quot;&gt;&lt;span&gt;users:read.email&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;View email addresses of people in a workspace&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: start;&quot; data-qa=&quot;app_scopes_list_row_users:write&quot;&gt;
&lt;div data-qa=&quot;app_scopes_list_row_scope&quot;&gt;&lt;a style=&quot;color: #1264a3;&quot; href=&quot;https://api.slack.com/scopes/users:write&quot;&gt;&lt;span&gt;users:write&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;Set a user&amp;rsquo;s presence&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;사용할 Scopes를 추가했는데 필요에 따라 추가하고 삭제하면된다.&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;구독에 대한 알람은 왼쪽탭 Event Subscriptions에서 하면된다.&lt;/div&gt;
&lt;div data-qa=&quot;app_scopes_list_row_description&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1UzDu/btsOckW5W52/bbEohBf0f7Lmk4wL2fC9A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1UzDu/btsOckW5W52/bbEohBf0f7Lmk4wL2fC9A0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1UzDu/btsOckW5W52/bbEohBf0f7Lmk4wL2fC9A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1UzDu%2FbtsOckW5W52%2FbbEohBf0f7Lmk4wL2fC9A0%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;1026&quot; height=&quot;706&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&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;Subscribe to events on behalf of users에서 구독 알림을 받을 Event name을 설정하면 그 이벤트에 대한 알림을 받을수 있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;761&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bixYJX/btsOdzsbXVT/wQGZWwI0KHj87DZVfpfQ50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bixYJX/btsOdzsbXVT/wQGZWwI0KHj87DZVfpfQ50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bixYJX/btsOdzsbXVT/wQGZWwI0KHj87DZVfpfQ50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbixYJX%2FbtsOdzsbXVT%2FwQGZWwI0KHj87DZVfpfQ50%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;722&quot; height=&quot;761&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;761&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>기타/Oauth2</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/303</guid>
      <comments>https://deftkang.tistory.com/303#entry303comment</comments>
      <pubDate>Mon, 26 May 2025 13:21:34 +0900</pubDate>
    </item>
    <item>
      <title>[Oauth2] Apple 로그인 console 세팅과 email, name 받는법</title>
      <link>https://deftkang.tistory.com/302</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플 oauth2 로그인 콘솔 접속 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플 개발자 콘솔 접속 URL : &lt;a href=&quot;https://developer.apple.com/account&quot;&gt;https://developer.apple.com/account&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 URL 접속해서 인증서로 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wiEW6/btsOcD2mtf9/ut0RlOo6Rd7m0IKrCLbezk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wiEW6/btsOcD2mtf9/ut0RlOo6Rd7m0IKrCLbezk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wiEW6/btsOcD2mtf9/ut0RlOo6Rd7m0IKrCLbezk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwiEW6%2FbtsOcD2mtf9%2Fut0RlOo6Rd7m0IKrCLbezk%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;400&quot; height=&quot;816&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;816&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;왼쪽탭 Identifiers 를 들어가서 오른쪽 ServicesIDs에 들어가서 Identifiers를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2513&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/epOuU4/btsObUDxvPs/lCA4iuEk9OR3cebmI5zSu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/epOuU4/btsObUDxvPs/lCA4iuEk9OR3cebmI5zSu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/epOuU4/btsObUDxvPs/lCA4iuEk9OR3cebmI5zSu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FepOuU4%2FbtsObUDxvPs%2FlCA4iuEk9OR3cebmI5zSu1%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;2513&quot; height=&quot;916&quot; data-origin-width=&quot;2513&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Sign In with Apple 의 Enabled체크와&amp;nbsp; Configure 에서&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;1617&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vEW2r/btsObT5Jjip/ekGYvuirvYCXhkwH2VeDkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vEW2r/btsObT5Jjip/ekGYvuirvYCXhkwH2VeDkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vEW2r/btsObT5Jjip/ekGYvuirvYCXhkwH2VeDkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvEW2r%2FbtsObT5Jjip%2FekGYvuirvYCXhkwH2VeDkk%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;1617&quot; height=&quot;950&quot; data-origin-width=&quot;1617&quot; data-origin-height=&quot;950&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;그리고 Website URLs 의 + 버튼을 클릭해서&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;1331&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sqJaM/btsObqwco2A/v1au26PeOp2wdpZHbSXkSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sqJaM/btsObqwco2A/v1au26PeOp2wdpZHbSXkSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sqJaM/btsObqwco2A/v1au26PeOp2wdpZHbSXkSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsqJaM%2FbtsObqwco2A%2Fv1au26PeOp2wdpZHbSXkSk%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;1331&quot; height=&quot;414&quot; data-origin-width=&quot;1331&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;도메인과, Return URLs를 기입하면된다.&amp;nbsp; Return URLs가 허용할 redirect URL 이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUaRS5/btsOb91IK5x/7baq6dKudYGxM8HkorJ3K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUaRS5/btsOb91IK5x/7baq6dKudYGxM8HkorJ3K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUaRS5/btsOb91IK5x/7baq6dKudYGxM8HkorJ3K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUaRS5%2FbtsOb91IK5x%2F7baq6dKudYGxM8HkorJ3K0%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;642&quot; height=&quot;567&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;934&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;기입했으면 저장해야되는데 나와서 Continue 버튼을 클릭하고 save버튼을 누르면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y3klz/btsObygFUHn/rw2YTVd5D4GKR0dwmRCF60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y3klz/btsObygFUHn/rw2YTVd5D4GKR0dwmRCF60/img.png&quot; data-origin-width=&quot;2513&quot; data-origin-height=&quot;909&quot; data-is-animation=&quot;false&quot; width=&quot;702&quot; height=&quot;254&quot; data-widthpercent=&quot;41.04&quot; style=&quot;width: 40.5626%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y3klz/btsObygFUHn/rw2YTVd5D4GKR0dwmRCF60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy3klz%2FbtsObygFUHn%2Frw2YTVd5D4GKR0dwmRCF60%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;2513&quot; height=&quot;909&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6mEzw/btsOblICS7i/TAOSgJH6MQZ6cMKpUtxnh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6mEzw/btsOblICS7i/TAOSgJH6MQZ6cMKpUtxnh0/img.png&quot; data-origin-width=&quot;2391&quot; data-origin-height=&quot;602&quot; data-is-animation=&quot;false&quot; style=&quot;width: 58.2747%;&quot; data-widthpercent=&quot;58.96&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6mEzw/btsOblICS7i/TAOSgJH6MQZ6cMKpUtxnh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6mEzw%2FbtsOblICS7i%2FTAOSgJH6MQZ6cMKpUtxnh0%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;2391&quot; height=&quot;602&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;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;로그인할때 scope로 name과 email, response_type에는 code, id_token을 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;code로 accessToken을 발급받아서 email을 조회할수있지만 그럴필요없이 id_token으로 jwt 데이터를 받아서 파싱하면 email을 받을수 있다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apple:
  oauth2:
    host: https://appleid.apple.com/auth/authorize
    clientId: 
    response_mode: form_post
    response_type: code id_token
    scope: name email
    redirectUri:&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;다른 ouath2는 code를 받아서 accessToken을 조회하지만 apple만 id_token의 값으로 email을 추출하고 처음 로그인 사용자시에 나오는 name 데이터로 이름을 받을수있다.&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;아래가 그 redirect URL인데&amp;nbsp; code는 받지 않고 id_token과 user만 받는다.&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;1205&quot; data-origin-height=&quot;1103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/233N5/btsOaGfvfOB/fsmkjzb0ABYVMpyJjyQ281/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/233N5/btsOaGfvfOB/fsmkjzb0ABYVMpyJjyQ281/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/233N5/btsOaGfvfOB/fsmkjzb0ABYVMpyJjyQ281/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F233N5%2FbtsOaGfvfOB%2Ffsmkjzb0ABYVMpyJjyQ281%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;1205&quot; height=&quot;1103&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;1103&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;name은 firstName과 lastName으로 나뉘어서 온다.&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;위 파라미터의 name은 처음 로그인 사용자에게만 뜨는 화면이 있고 그 화면에 설정한대로 나온다. 화면은 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 로그인 사용자는 이름을 수정하거나 그대로 사용할수있고, 이메일도 나의 이메일을 그대로 사용할것인지 아니면 감출것인지 설정할수있다. 만약 감추기를 하면 email은 진짜로 사용하는 이메일이 아닌 다른 이메일로 오게된다. 그래서 만약 같은 구글이메일을 애플이메일로 사용했을때&amp;nbsp; 구글 로그인과 애플로그인은 같은 사용자로 취급해야 하지만 애플이메일을 감추게 되면 새로운 사용자로 될수밖에 없다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;1222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H2p3U/btsOburNLJt/TEerPhuyWn0nmLK4kkMqv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H2p3U/btsOburNLJt/TEerPhuyWn0nmLK4kkMqv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H2p3U/btsOburNLJt/TEerPhuyWn0nmLK4kkMqv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH2p3U%2FbtsOburNLJt%2FTEerPhuyWn0nmLK4kkMqv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;583&quot; height=&quot;1222&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;1222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타/Oauth2</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/302</guid>
      <comments>https://deftkang.tistory.com/302#entry302comment</comments>
      <pubDate>Sat, 24 May 2025 18:43:33 +0900</pubDate>
    </item>
    <item>
      <title>[Oauth2] Google 메일 알림 구독 개발 콘솔 설정 방법</title>
      <link>https://deftkang.tistory.com/301</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;구글은 email이나 calenar에 대한 알림을 구독 하려면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub/Sub을 이용해야 한다.&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://console.cloud.google.com/cloudpubsub&quot;&gt;https://console.cloud.google.com/cloudpubsub&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 URL에 접속해서 Topics에 들어간다음 Topic을 만들고&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;1028&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz6yKz/btsOa7o6aG5/NL0wyLNkdXZhhGm8N5gLOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz6yKz/btsOa7o6aG5/NL0wyLNkdXZhhGm8N5gLOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz6yKz/btsOa7o6aG5/NL0wyLNkdXZhhGm8N5gLOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz6yKz%2FbtsOa7o6aG5%2FNL0wyLNkdXZhhGm8N5gLOk%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;1028&quot; height=&quot;844&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;844&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;구독을 추가 하면된다. 전송유형은 push로 해서 알림을 받을 endpoint를 기입해주면 된다.&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;770&quot; data-origin-height=&quot;1071&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmiw3z/btsN93IiW4l/UcaZIZaY4Whepa6WcyOcz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmiw3z/btsN93IiW4l/UcaZIZaY4Whepa6WcyOcz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmiw3z/btsN93IiW4l/UcaZIZaY4Whepa6WcyOcz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdmiw3z%2FbtsN93IiW4l%2FUcaZIZaY4Whepa6WcyOcz1%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;770&quot; height=&quot;1071&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;1071&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 소스에서는&amp;nbsp; 구글 sdk를 이용해서 WatchRequest()에 그 위에서만든 토픽 이름을 이용해서 watchRequest 객체를 만들고&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;val resourceList = listOf(&quot;INBOX&quot;)

/**
 * Gmail watch 요청은 Cloud Pub/Sub 이용하여 푸시 알림을 전달
 */
val watchRequest = WatchRequest().apply {
    topicName = properties.subscriptionTopic
    labelIds = resourceList // Only monitor INBOX emails
    labelFilterAction = &quot;include&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;p data-ke-size=&quot;size16&quot;&gt;구독을 API를 신청하면된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;val watchResponse = gmailService.users().watch(&quot;me&quot;, watchRequest).execute()
&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[에러해결]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;Topic의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Publisher&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;권한이 없으면 Gmail Watch 요청 시 다음 에러가 발생한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&quot;code&quot;:&amp;nbsp;403,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;message&quot;:&amp;nbsp;&quot;User&amp;nbsp;not&amp;nbsp;authorized&amp;nbsp;to&amp;nbsp;perform&amp;nbsp;this&amp;nbsp;action.&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;errors&quot;:&amp;nbsp;[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;reason&quot;:&amp;nbsp;&quot;forbidden&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;message&quot;:&amp;nbsp;&quot;Error&amp;nbsp;sending&amp;nbsp;test&amp;nbsp;message&amp;nbsp;to&amp;nbsp;Cloud&amp;nbsp;PubSub...&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&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;p data-ke-size=&quot;size16&quot;&gt;User not authorized to perform this action.&quot;,&quot;status&quot;:&quot;PERMISSION_DENIED&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결하려면 아래 Permission을 추가해줘야 한다.&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;414&quot; data-origin-height=&quot;633&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkI8sf/btsObmGx0V5/mFakvIaGsbxolBO19YrRRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkI8sf/btsObmGx0V5/mFakvIaGsbxolBO19YrRRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkI8sf/btsObmGx0V5/mFakvIaGsbxolBO19YrRRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkI8sf%2FbtsObmGx0V5%2FmFakvIaGsbxolBO19YrRRk%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;414&quot; height=&quot;633&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;633&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;IAM 에서 계정을 추가후에&amp;nbsp; 위에 ADD Principal 에서 넣어주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpmoUr/btsPC4MUK0C/mZemXjqmzAUTer06sVBMi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpmoUr/btsPC4MUK0C/mZemXjqmzAUTer06sVBMi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpmoUr/btsPC4MUK0C/mZemXjqmzAUTer06sVBMi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpmoUr%2FbtsPC4MUK0C%2FmZemXjqmzAUTer06sVBMi0%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;925&quot; height=&quot;1114&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;1114&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;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;gmail-api-push@system.gserviceaccount.com&amp;nbsp;&lt;br /&gt;&lt;br /&gt;publisher 설정 필수 !!!&lt;/span&gt;&lt;/p&gt;</description>
      <category>기타/Oauth2</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/301</guid>
      <comments>https://deftkang.tistory.com/301#entry301comment</comments>
      <pubDate>Fri, 23 May 2025 15:17:42 +0900</pubDate>
    </item>
    <item>
      <title>[Oauth2] Microsoft 로그인과 API 사용하기 위한 console 세팅</title>
      <link>https://deftkang.tistory.com/300</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;portal.azure.com 사이트로 로그인해서 Microsoft Entra ID 서비스로 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 왼쪽 탬 관리에서 앱등록 으로 들어가서 앱등록을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;905&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2sYM1/btsN5gAcF6p/kauskXUt8PjlziDBjL0ONk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2sYM1/btsN5gAcF6p/kauskXUt8PjlziDBjL0ONk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2sYM1/btsN5gAcF6p/kauskXUt8PjlziDBjL0ONk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2sYM1%2FbtsN5gAcF6p%2FkauskXUt8PjlziDBjL0ONk%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;905&quot; height=&quot;645&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들어진 앱에서 클라이언트 ID와 시크릿 키를 발급받으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 관리&amp;gt;인증에서 웹 리디렉션 URI를 기입하면된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GXs1i/btsN3s9KMr5/ekLtNvRwwWVrqK7ztJDukk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GXs1i/btsN3s9KMr5/ekLtNvRwwWVrqK7ztJDukk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GXs1i/btsN3s9KMr5/ekLtNvRwwWVrqK7ztJDukk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGXs1i%2FbtsN3s9KMr5%2FekLtNvRwwWVrqK7ztJDukk%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;1217&quot; height=&quot;631&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 왼쪽 탭 관리&amp;gt;API 사용권한 에서 사용자가 사용할수 있는 API 사용 권한을 추가 해줘야 한다. 여기서는 사용자가 API 를 사용요청을 하였을때 그 API에 대한 권한을 열어주기 위함이다.&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;1236&quot; data-origin-height=&quot;595&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Shj4X/btsN4oZNDOb/srhlbMbmDdOKwSCKvO7uW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Shj4X/btsN4oZNDOb/srhlbMbmDdOKwSCKvO7uW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Shj4X/btsN4oZNDOb/srhlbMbmDdOKwSCKvO7uW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FShj4X%2FbtsN4oZNDOb%2FsrhlbMbmDdOKwSCKvO7uW1%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;1236&quot; height=&quot;595&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;595&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한추가 &amp;gt; Microsoft Graph &amp;gt; 위임된 권한 에서 권한 선택 칸에서 검색하고 권한을 선택하고 추가하면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 Oauth2로 로그인을 하는데 사용자에 대한 email을 가져오려면 email을 추가해줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;987&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d90it0/btsN4AslJF0/iGfNhJgk3wcWkIrnvfwGcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d90it0/btsN4AslJF0/iGfNhJgk3wcWkIrnvfwGcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d90it0/btsN4AslJF0/iGfNhJgk3wcWkIrnvfwGcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd90it0%2FbtsN4AslJF0%2FiGfNhJgk3wcWkIrnvfwGcK%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;806&quot; height=&quot;987&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;987&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>기타/Oauth2</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/300</guid>
      <comments>https://deftkang.tistory.com/300#entry300comment</comments>
      <pubDate>Tue, 20 May 2025 15:07:10 +0900</pubDate>
    </item>
    <item>
      <title>SpringBoot 단일 모듈에서 소스 공통화를 위해 멀티모듈로 변경</title>
      <link>https://deftkang.tistory.com/299</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;회사에서 기존 B2B 소스를 기반으로 B2C 개발을 진행하기로 했다. B2B에서 쓰던 공통 소스들이 분명 있을거고, 또 B2B에서 적용될거와 B2C에 적용될게 동시에 있을게 분명하였다. 공통 부분을 관리해서 B2C와 B2B에 같이 적용시키기 위해서 멀티모듈을 하자고 제안했다.&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;우선 B2B 소스를 멀티모듈로 나눠야 했었는데 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Application, Service, Repository&lt;span&gt; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;레이어드 아키텍처 기반으로 분리를 하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lBjbk/btsNhtTXer8/YVxTVhixDBtk9y700xtgRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lBjbk/btsNhtTXer8/YVxTVhixDBtk9y700xtgRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lBjbk/btsNhtTXer8/YVxTVhixDBtk9y700xtgRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlBjbk%2FbtsNhtTXer8%2FYVxTVhixDBtk9y700xtgRK%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;546&quot; height=&quot;326&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;326&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;그리고 실제 작업의 모듈 이름은 아래와 같이 common, b2b-api, common-service, common-domain 으로 분리를 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setting.gradle.kts 파일에서 include 안에 모듈명을 인입하여 모듈을 생성하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBXAQC/btsNkn7c73Q/9dz2pd6CiOXpyO8biaPLk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBXAQC/btsNkn7c73Q/9dz2pd6CiOXpyO8biaPLk1/img.png&quot; data-origin-width=&quot;839&quot; data-origin-height=&quot;540&quot; data-is-animation=&quot;false&quot; style=&quot;width: 64.6475%; margin-right: 10px;&quot; data-widthpercent=&quot;65.41&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBXAQC/btsNkn7c73Q/9dz2pd6CiOXpyO8biaPLk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBXAQC%2FbtsNkn7c73Q%2F9dz2pd6CiOXpyO8biaPLk1%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;839&quot; height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ds6pUN/btsNisbHcud/rBexmgKurXi14NEkCe9GmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ds6pUN/btsNisbHcud/rBexmgKurXi14NEkCe9GmK/img.png&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;931&quot; data-is-animation=&quot;false&quot; style=&quot;width: 34.1897%;&quot; data-widthpercent=&quot;34.59&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ds6pUN/btsNisbHcud/rBexmgKurXi14NEkCe9GmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fds6pUN%2FbtsNisbHcud%2FrBexmgKurXi14NEkCe9GmK%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;765&quot; height=&quot;931&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;setting.gradle.kts 세팅&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구성 설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goyoai-backend-system&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;├── goyoai-web-common/&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;├── goyoai-web-common-domain&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;├── goyoai-web-common-service&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;├── goyoai-web-b2b&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;├── goyoai-web-b2c&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;b2b를 레이어드 아키텍쳐에 따라 application, service, domain 계층으로 나누었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application &amp;rarr; service &amp;rarr; domain 모듈로 나누고 경계를 구분하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application계층 : controller&lt;/li&gt;
&lt;li&gt;service계층: dto, service (비즈니스 로직)&lt;/li&gt;
&lt;li&gt;domain계층: entity, dao, enumeration (데이터 구조 정의)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goyoai-web-common&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goyoai-web-common-domain&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통으로 사용될 데이터관리 소스(entity, dao, enumeration)만 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;goyoai-web-common 만 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goyoai-web-common-service&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통으로 사용될 서비스 소스(service) 비즈니스 로직이다.&lt;/li&gt;
&lt;li&gt;goyoai-web-common, goyoai-web-common-domain 만 의존하여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goyoai-web-b2b&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;b2b의 controller와 공통에서 사용되지 않고 b2b용으로 커스텀돼서 사용될 소스이다.&lt;/li&gt;
&lt;li&gt;위에서 만든 common쪽 모두 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goyoai-web-b2b&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;b2c의 controller와 공통에서 사용되지 않고 b2c용으로 커스텀돼서 사용될 소스이다.&lt;/li&gt;
&lt;li&gt;위에서 만든 common쪽 모두 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Common쪽에서 관리되는 파일에서 b2b, b2c로 나뉘어졌을때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 접두사로 B2B, B2C 기입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) B2BUserService, B2CUserService&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;build.gradle.kts 세팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;루트 모듈에서의 build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트모듈에서는 allprojects와, subprojects를 설정 해줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;allprojects {} : (루트 + 하위) 모듈 plugin 및 공통 의존성 세팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;subprojects {} : 하위 모듈의 의존성 세팅&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;allporjedcts 설정에는 프로젝트에 대한 설명과 사용할 plugin들 설정 그리고 tasks 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application 계층의 모듈 빼고는 bootWar나 bootJar가 필요없고 jar만 있으면 되기 때문에 jar만 생성되도록 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;allprojects {
    description = &quot;GoyoAi Web Service&quot;
    group = &quot;io.goyoai.web&quot;
    version = &quot;2.0.0&quot;

    apply(plugin = &quot;java&quot;)
    apply(plugin = &quot;kotlin&quot;)
    apply(plugin = &quot;kotlin-spring&quot;)
    apply(plugin = &quot;kotlin-jpa&quot;)
    apply(plugin = &quot;kotlin-kapt&quot;)

    dependencyManagement {
        val springCloudVersion = &quot;2022.0.4&quot;
        imports {
            mavenBom(&quot;org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}&quot;)
        }
    }

    allOpen {
        annotation(&quot;jakarta.persistence.Entity&quot;)
        annotation(&quot;jakarta.persistence.MappedSuperclass&quot;)
        annotation(&quot;jakarta.persistence.Embeddable&quot;)
    }

    repositories {
        mavenCentral()
        maven { url = uri(&quot;https://repo.spring.io/milestone&quot;) }
        maven { url = uri(&quot;https://repo.spring.io/snapshot&quot;) }
    }

    // 라이브러리용 jar만 생성
    tasks.getByName&amp;lt;Jar&amp;gt;(&quot;jar&quot;) {
        enabled = true
    }

    tasks.getByName&amp;lt;War&amp;gt;(&quot;war&quot;) {
        enabled = false
    }

    tasks.bootJar {
        enabled = false
    }

    tasks.bootWar {
        enabled = false
    }


    tasks.test {
        enabled = false
    }
}&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: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 하위 모듈만 설정
subprojects {
    dependencies {
        // starter
        implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa:3.1.6&quot;)
        implementation(&quot;org.springframework.boot:spring-boot-starter-validation:3.1.6&quot;)
        implementation(&quot;org.springframework.boot:spring-boot-starter-web:3.2.0&quot;)
        implementation(&quot;org.springframework.boot:spring-boot-starter-mail:3.1.6&quot;)
        implementation(&quot;org.springframework.boot:spring-boot-starter-thymeleaf:3.1.6&quot;)
        implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis:3.1.6&quot;)
        implementation(&quot;org.springframework.session:spring-session-data-redis:3.1.6&quot;)
        implementation(&quot;org.springframework.cloud:spring-cloud-starter-openfeign:4.0.4&quot;)
        implementation(&quot;com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5&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;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;하위 모듈에서의 build.gradle.kts&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. application 계층에 서는 common과, common-service를 의존성 맺고 배포를 해야되기 때문에 bootWar를 만들어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 swagger같은 application 계층에서만 필요한 의존성들을 넣고 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;1070&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rx4M0/btsNjBSGf7d/qFtfk7pL3GV1TtaCSHYsgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rx4M0/btsNjBSGf7d/qFtfk7pL3GV1TtaCSHYsgK/img.png&quot; data-alt=&quot;application 계층의 build.gradle.kts&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rx4M0/btsNjBSGf7d/qFtfk7pL3GV1TtaCSHYsgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frx4M0%2FbtsNjBSGf7d%2FqFtfk7pL3GV1TtaCSHYsgK%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;487&quot; height=&quot;404&quot; data-origin-width=&quot;1291&quot; data-origin-height=&quot;1070&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;application 계층의 build.gradle.kts&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;domain 계층에서는 common인 공통 모듈만 의존하고, service에서는 common과 도메인계층만 의존한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zhRI3/btsNjEhu5nr/Z9DnkotkvuYDGwccW29rK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zhRI3/btsNjEhu5nr/Z9DnkotkvuYDGwccW29rK0/img.png&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;274&quot; data-is-animation=&quot;false&quot; style=&quot;width: 45.2372%; margin-right: 10px;&quot; data-widthpercent=&quot;45.77&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zhRI3/btsNjEhu5nr/Z9DnkotkvuYDGwccW29rK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzhRI3%2FbtsNjEhu5nr%2FZ9DnkotkvuYDGwccW29rK0%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;716&quot; height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v9wsH/btsNiYmwmSG/Ir3YOL6lsGKkpCCVZ62YKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v9wsH/btsNiYmwmSG/Ir3YOL6lsGKkpCCVZ62YKk/img.png&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;291&quot; data-is-animation=&quot;false&quot; style=&quot;width: 53.6%;&quot; data-widthpercent=&quot;54.23&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v9wsH/btsNiYmwmSG/Ir3YOL6lsGKkpCCVZ62YKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv9wsH%2FbtsNiYmwmSG%2FIr3YOL6lsGKkpCCVZ62YKk%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;901&quot; height=&quot;291&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;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;b&gt;1. 의존성 방향을 깔끔하게 유지하기 위해&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;application &amp;rarr; service &amp;rarr; domain 방향으로 의존을 맺기 때문에 서로 의존하지 못하게 강제할수있음 유지보수 확장성을 증대한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를들어서 사용자에게 보여줄 dto를 domain계층에서 사용되고 있다면 이것은 domain계층이 application 계층에 의존하고 있다 이것을 강제로 분리하게 해서 의존을 못맺게 하니 명확하게 구분지어줄수있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;공통 소스를 common 으로 분리해서 b2b와 b2c 동시 적용 가능하다(재사용성, 공통화 가능)&lt;/li&gt;
&lt;li&gt;domain, service 를 나눔으로써 다른 서비스가 모듈로 들어왔을때 domain만 의존해서 사용가능 하다.&lt;/li&gt;
&lt;/ul&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;아래 우아한 기술블로그에서도 보면은 우리와 동일하게 service에서 사용자에게 보여줄 dto를 만들고 요청데이터 그대로 service에서 받고 있었는데 이러면 service가 controller에 종속적이게 된다고해서 안좋다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;1140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M4qYo/btsNggIY57m/x7jNdHJoLIEdhuHfRZTNk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M4qYo/btsNggIY57m/x7jNdHJoLIEdhuHfRZTNk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M4qYo/btsNggIY57m/x7jNdHJoLIEdhuHfRZTNk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM4qYo%2FbtsNggIY57m%2Fx7jNdHJoLIEdhuHfRZTNk1%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;1408&quot; height=&quot;1140&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;1140&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;위 사례로 따지면 레이어드 아키텍쳐를 구분했을때 원래는 controller에서 service로 요청보내는 request dto와 service에서 controller로 보내는 response dto를 따로 만들어줘야 한다. 그래도 dto가 service에서 있고 application 계층에도 따로 있어야 하지만&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;지금은 service에서 모두 dto를 관리해서 service에서만 사용된다. dto를 service 계층에 맞게 다 따로 만들어줘야 되는데 너무 시간이 오래걸리고 앞으로도 그렇게 하자니 개발하는데 너무 힘들것같다. 우선은 dto를 service 계층에 넣었지만 지금당장은 바꾸기 힘들것같다. 나중에 꼭 필요하다고 생각될 이유가 있을때 바꿔야 할것같다.&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;a href=&quot;https://engineerinsight.tistory.com/63&quot;&gt;https://engineerinsight.tistory.com/63&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2711/&quot;&gt;https://techblog.woowahan.com/2711/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/299</guid>
      <comments>https://deftkang.tistory.com/299#entry299comment</comments>
      <pubDate>Thu, 10 Apr 2025 21:29:04 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot] 포트원 API 이용한 결제 기능 추가</title>
      <link>https://deftkang.tistory.com/297</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;결제 연동을 하기 위해서는 가장 간단한 방법으로 포트원 API를 이용하는 것이다. 각 PG사에서 제공하는 API를 이용하여 개발해야 하고 각 PG사마다 제공하는 API에 맞게 개발을 해야 하는데 포트원 API를 사용하면 모든 PG사와  결제수단을 쉽게 이용할 수 있다. 그리고 테스트 연동도 제공한다. 테스트 연동을 하면 자정 전에 결제 취소가 자동으로 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;PG는 Payment Gateway 약자로 거래 및 결제를 도와주는 결제 대행사이다. 포트원은 모든 PG 모아서 한 번에 제공해 준다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;포트원 API 결제 프로세스&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p2VMH/btsJJvhuDOf/uk5do3UBtj8xxlWQLBdjFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p2VMH/btsJJvhuDOf/uk5do3UBtj8xxlWQLBdjFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p2VMH/btsJJvhuDOf/uk5do3UBtj8xxlWQLBdjFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp2VMH%2FbtsJJvhuDOf%2Fuk5do3UBtj8xxlWQLBdjFK%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;667&quot; height=&quot;471&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;1054&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 프로세스는 간단하다. 구매 페이지에서 결제창을 호출하고 PG사에 결제 인증을 요청한 다음 PG사에서 받은 결제 키 전달받는다. 그리고 PG사에 결제 요청을 하고 카드사에 결제 요청을 한 다음 고객은 결제 결과를 받는다.&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;b&gt;결제 연동&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;포트원 관리자 콘솔에서 채널추가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 연동을 하기 위해서 먼저 &lt;a href=&quot;https://admin.portone.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;포트원 관리자 콘솔&lt;/a&gt;에서 채널을 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 탭 결제 연동 &amp;gt; 연동 정보에서 아래 정보대로 채널을 추가한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGq0WJ/btsJKdmN9Gd/QE5A3KBvKvwShUILVE8BLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGq0WJ/btsJKdmN9Gd/QE5A3KBvKvwShUILVE8BLK/img.png&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;1210&quot; data-is-animation=&quot;false&quot; style=&quot;width: 63.1836%; margin-right: 10px;&quot; data-widthpercent=&quot;63.93&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGq0WJ/btsJKdmN9Gd/QE5A3KBvKvwShUILVE8BLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGq0WJ%2FbtsJKdmN9Gd%2FQE5A3KBvKvwShUILVE8BLK%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;1566&quot; height=&quot;1210&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RFHC3/btsJKhig9Kx/HQqvDSDNQCYGikD6cOkzWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RFHC3/btsJKhig9Kx/HQqvDSDNQCYGikD6cOkzWk/img.png&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;1498&quot; data-is-animation=&quot;false&quot; style=&quot;width: 35.6536%;&quot; data-widthpercent=&quot;36.07&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RFHC3/btsJKhig9Kx/HQqvDSDNQCYGikD6cOkzWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRFHC3%2FbtsJKhig9Kx%2FHQqvDSDNQCYGikD6cOkzWk%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;1094&quot; height=&quot;1498&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;KG이니시스를 선택하고, 결제 모듈은 일반/정기결제 선택 후 다음 누르고 채널 추가 화면에서 채널이름을 기입하고 PG 상점아이디를 INIpayTest를 선택하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;의존성 추가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 build.gradle.kts 파일에 아래 의존성을 추가한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;repositories {
    mavenCentral()
    maven(url = &quot;https://jitpack.io&quot;)
}

dependencies {
	implementation(&quot;com.github.iamport:iamport-rest-client-java:0.2.23&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;repositores에 maven(url = &quot;https://jitpack.io&quot;)을 추가하여 GitHub 저장소에 있는 프로젝트를 의존성으로 추가할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결제창 호출&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 봤드시 결제를 하려면 결제창을 먼저 호출해야 한다. &lt;a href=&quot;https://developers.portone.io/opi/ko/integration/start/v1/auth?v=v1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;결제창 호출 가이드 문서&lt;/a&gt;를 보면 SDK를 추가하기 위해 script를 추가하고&lt;/p&gt;
&lt;pre class=&quot;xml&quot; style=&quot;background-color: #ffffff; color: #24292e; text-align: start;&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;https://cdn.iamport.kr/v1/iamport.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&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;그다음엔 포트원 SDK 초기화를 한다.&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;1634&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/toi6b/btsJIlmzQ1p/ZPB4n1KwUqknVHIyTkpnvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/toi6b/btsJIlmzQ1p/ZPB4n1KwUqknVHIyTkpnvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/toi6b/btsJIlmzQ1p/ZPB4n1KwUqknVHIyTkpnvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftoi6b%2FbtsJIlmzQ1p%2FZPB4n1KwUqknVHIyTkpnvk%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;1634&quot; height=&quot;152&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 고객사 식별코드는 연동정보 &amp;gt; 식별코드에서 고객사 식별코드 탭의 정보이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR2FTD/btsJJWeBJMb/aMvLOpMqmJxFnFKYfNPnT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR2FTD/btsJJWeBJMb/aMvLOpMqmJxFnFKYfNPnT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR2FTD/btsJJWeBJMb/aMvLOpMqmJxFnFKYfNPnT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR2FTD%2FbtsJJWeBJMb%2FaMvLOpMqmJxFnFKYfNPnT1%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;790&quot; height=&quot;420&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1218&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;그리고 결제창 불러오기를 IMP.request_pay() 함수를 호출해서 열 수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg: 정보 기입란에 {PG사 코드}.{상점 ID}를 넣어줘야 하는데 포트원 콘솔에서 채널정보에서 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PG사 코드값은 PG Provider의 값이고, 상점 ID는 PG상점 아이디의 값이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mupjd/btsJJDGANS5/ca7NonCG4M59dHMcWmme8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mupjd/btsJJDGANS5/ca7NonCG4M59dHMcWmme8K/img.png&quot; data-origin-width=&quot;1650&quot; data-origin-height=&quot;1232&quot; data-is-animation=&quot;false&quot; width=&quot;595&quot; height=&quot;444&quot; style=&quot;width: 63.7173%; margin-right: 10px;&quot; data-widthpercent=&quot;64.47&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mupjd/btsJJDGANS5/ca7NonCG4M59dHMcWmme8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMupjd%2FbtsJJDGANS5%2Fca7NonCG4M59dHMcWmme8K%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;1650&quot; height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NNJcG/btsJI1ueswU/5oRVyMBKQdreKFL1j94KcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NNJcG/btsJI1ueswU/5oRVyMBKQdreKFL1j94KcK/img.png&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;1482&quot; data-is-animation=&quot;false&quot; width=&quot;388&quot; height=&quot;526&quot; style=&quot;width: 35.1199%;&quot; data-widthpercent=&quot;35.53&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NNJcG/btsJI1ueswU/5oRVyMBKQdreKFL1j94KcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNNJcG%2FbtsJI1ueswU%2F5oRVyMBKQdreKFL1j94KcK%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;1094&quot; height=&quot;1482&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 정보를 토대로 하면 pg 값은 &quot;html5_inicis.INIpayTest&quot; 이고, 아래는 script 코드이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    var IMP = window.IMP;
    IMP.init(&quot;imp22283555&quot;);

    function requestPay() {
        const productId = document.getElementById(&quot;productId&quot;).value;
        const itemId = document.getElementById(&quot;itemId&quot;).value;
        const itemOption = document.getElementById(&quot;itemOption&quot;).value;
        const itemName = document.getElementById('itemName').value;
        const quantity = document.getElementById('quantity').value;
        const price = document.getElementById('price').value;
        const buyerEmail = document.getElementById('buyerEmail').value;
        const buyerName = document.getElementById('buyerName').value;
        const buyerTelNo = document.getElementById('buyerTelNo').value;
        const postCode = document.getElementById('sample4_postcode').value;
        const addr = document.getElementById('sample4_jibunAddress').value;

        IMP.request_pay(
            {
                pg: &quot;html5_inicis.INIpayTest&quot;,
                pay_method: &quot;card&quot;,
                merchant_uid: `payment-${crypto.randomUUID()}`, // 주문 고유 번호
                name: itemName,
                amount: price,
                buyer_email: buyerEmail,
                buyer_name: buyerName,
                buyer_tel: buyerTelNo,
                buyer_addr: addr,
                buyer_postcode: postCode,
            },
            function (response) {
                // 결제 종료 시 호출되는 콜백 함수
                // response.imp_uid 값으로 결제 단건조회 API를 호출하여 결제 결과를 확인하고,
                // 결제 결과를 처리하는 로직을 작성합니다.
                if (response.success) {
                    if (response.success) {
                        console.log(&quot;구매이력 저장 로직실행&quot;)
                    }
                } else {
                    console.log(response)
                    alert(response)
                }
            },
        );
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품명과 금액, 구매자 정보는 UI에서 가져와서 인입해줬다. 주소는 카카오지도 API를 이용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결제화면과 결제창 호출과 결제&lt;/b&gt;&lt;/h4&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;2358&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJj9O0/btsJIoKgpIy/cKu22FHZW0qfZYfUlVVYQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJj9O0/btsJIoKgpIy/cKu22FHZW0qfZYfUlVVYQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJj9O0/btsJIoKgpIy/cKu22FHZW0qfZYfUlVVYQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJj9O0%2FbtsJIoKgpIy%2FcKu22FHZW0qfZYfUlVVYQ0%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;724&quot; height=&quot;278&quot; data-origin-width=&quot;2358&quot; data-origin-height=&quot;904&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;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XaJ1y/btsJKcuM3LW/gdcKXGLUxsaVW6NMgUgzB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XaJ1y/btsJKcuM3LW/gdcKXGLUxsaVW6NMgUgzB1/img.png&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;1408&quot; data-is-animation=&quot;false&quot; width=&quot;614&quot; height=&quot;481&quot; style=&quot;width: 46.8882%; margin-right: 10px;&quot; data-widthpercent=&quot;47.44&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XaJ1y/btsJKcuM3LW/gdcKXGLUxsaVW6NMgUgzB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXaJ1y%2FbtsJKcuM3LW%2FgdcKXGLUxsaVW6NMgUgzB1%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;1796&quot; height=&quot;1408&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xhmnS/btsJIOhA1MZ/1zZCM5KkxZHEsAiUCmJlCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xhmnS/btsJIOhA1MZ/1zZCM5KkxZHEsAiUCmJlCK/img.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;876&quot; data-is-animation=&quot;false&quot; style=&quot;width: 51.949%;&quot; data-widthpercent=&quot;52.56&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xhmnS/btsJIOhA1MZ/1zZCM5KkxZHEsAiUCmJlCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxhmnS%2FbtsJIOhA1MZ%2F1zZCM5KkxZHEsAiUCmJlCK%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;1238&quot; height=&quot;876&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;p data-ke-size=&quot;size16&quot;&gt;html 화면을 간단하게 만들어서 KG 이니시스 결제창 불러오기를 성공하였고 결제를 하게 되면 포트원 콘솔 통합 결제 메뉴에서 확인이 가능하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;필터에서 테스트 결제를 체크해야만 한다.&lt;/blockquote&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;2488&quot; data-origin-height=&quot;1498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLCcyJ/btsJJMQOCvD/oqkYT3T7OTLuhskm6Ym8A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLCcyJ/btsJJMQOCvD/oqkYT3T7OTLuhskm6Ym8A1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLCcyJ/btsJJMQOCvD/oqkYT3T7OTLuhskm6Ym8A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLCcyJ%2FbtsJJMQOCvD%2FoqkYT3T7OTLuhskm6Ym8A1%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;2488&quot; height=&quot;1498&quot; data-origin-width=&quot;2488&quot; data-origin-height=&quot;1498&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;결제를 성공하면 아래 처럼 reponse가 오고 success필드는 true이다. 이 결제 내역의 imp_uid나 merchant_uid를 통해서 결제를 취소할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[response JSON 값]&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;response 예제
{
    &quot;success&quot;: true,
    &quot;imp_uid&quot;: &quot;imp_35168980131&quot;,
    &quot;pay_method&quot;: &quot;card&quot;,
    &quot;merchant_uid&quot;: &quot;payment-41693333-e87b-468e-be62-b4485123&quot;,
    &quot;name&quot;: &quot;오구 슬리퍼&quot;,
    &quot;paid_amount&quot;: 1990,
    &quot;currency&quot;: &quot;KRW&quot;,
    &quot;pg_provider&quot;: &quot;html5_inicis&quot;,
    &quot;pg_type&quot;: &quot;payment&quot;,
    &quot;pg_tid&quot;: &quot;StdpayCARDINIpayTest20240907044147199999&quot;,
    &quot;apply_num&quot;: &quot;123415&quot;,
    &quot;buyer_name&quot;: &quot;토비&quot;,
    &quot;buyer_email&quot;: &quot;test@naver.com&quot;,
    &quot;buyer_tel&quot;: &quot;010-1111-1111&quot;,
    &quot;buyer_addr&quot;: &quot;서울 노원구 공릉동&quot;,
    &quot;buyer_postcode&quot;: &quot;01111&quot;,
    &quot;custom_data&quot;: null,
    &quot;status&quot;: &quot;paid&quot;,
    &quot;paid_at&quot;: 1725651707,
    &quot;receipt_url&quot;: &quot;https://iniweb.inicis.com/DefaultWebApp/mall/cr/cm/mCmReceipt_head.jsp?noTid=StdpayCARDINIpayTest20240907044147194653&amp;amp;noMethod=1&quot;,
    &quot;card_name&quot;: &quot;BC카드&quot;,
    &quot;bank_name&quot;: null,
    &quot;card_quota&quot;: 0,
    &quot;card_number&quot;: &quot;910003*********5&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;p data-ke-size=&quot;size16&quot;&gt;결제가 성공하면 위 구매이력을 DB에 저장하고, 재고를 차감하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[script 코드]&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;if (response.success) {
    console.log(&quot;구매이력 저장과 재고차감 로직실행&quot;)
    fetch(&quot;/v1/payments&quot;, {
        method: &quot;POST&quot;,
        headers: {
            &quot;Content-Type&quot;: &quot;application/json&quot;,
        },
        body: JSON.stringify({
            productId: productId,
            itemId: itemId,
            impUid: response.imp_uid,
            merchantUid: response.merchant_uid,
            itemName: response.name,
            itemOption: itemOption,
            buyQuantity: quantity,
            paidAmount: response.paid_amount,
            buyerName: response.buyer_name,
            buyerEmail: response.buyer_email,
            buyerTelNo: response.buyer_tel,
            buyerAddr: response.buyer_addr,
            buyerPostCode: response.buyer_postcode,
        }),
    }).then((res) =&amp;gt; res.json())
        .then((res) =&amp;gt; {
            console.log(res)
            if (res.success) {
                alert(res.message);
            } else {
                alert(res.message);
            }
        }).catch(() =&amp;gt; {
        alert(&quot;ajax 호출 에러&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Kotlin 코드]&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/v1/payments&quot;)
fun savePayment(@RequestBody savePaymentRequest: SavePaymentRequest): ResponseEntity&amp;lt;MessageDTO&amp;gt; {
    log.info { &quot;/v1/payment post 요청 실행&quot; }

    paymentServiceApplication.savePayment(savePaymentRequest)

    val messageDTO = MessageDTO(
        HttpStatus.OK.value(),
        &quot;상품을 구매 완료 하였습니다.&quot;
    )

    return ResponseEntity(messageDTO, HttpStatus.OK)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional
fun savePayment(savePaymentRequest: SavePaymentRequest) {
    //재고차감
    log.info { &quot;재고차감 로직 실행&quot; }
    val item = itemService.getItemById(savePaymentRequest.itemId)
    itemService.deductItemStock(item, savePaymentRequest.buyQuantity)

    //구매이력 저장
    log.info { &quot;구매이력 저장 실행&quot; }
    paymentService.savePayment(savePaymentRequest)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Transactional
fun deductItemStock(item: Item, quantity: Int) {
    val updatedStock: Int = item.stock - quantity
    item.updateStock(updatedStock)
}

@Transactional
fun savePayment(savePaymentRequest: SavePaymentRequest) {
    val payment = savePaymentRequest.toPayment()
    paymentRepository.save(payment)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Project/B2C-Side-Project(third)</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/297</guid>
      <comments>https://deftkang.tistory.com/297#entry297comment</comments>
      <pubDate>Mon, 23 Sep 2024 19:39:06 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot] Kotlin 프로젝트에서 Vault 운영 서버 구축</title>
      <link>https://deftkang.tistory.com/296</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Vault를 적용하게된 계기는 application.yml 파일에 DB접속 URL 등 다양한 개인정보들이 git에 노출되는것이 문제여서 적용하게 되었다. 기존에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Jasypt 를 이용해서 암호화 하였는데 이것 역시 secret 키값이 프로젝트 코드속에 있어서 마음만 먹으면 secret 키값을 알아내서 암호화를 풀수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Vault는 분산시스템에서 사용되고 시크릿한 정보들을 스프링 클라우드에서 데이터를 가져온다. 지금 프로젝트에서는 분산 시스템이 아니지만 나중에 분산시스템으로 변경 했을때 사용될수 있을것 같아서 미리 도입해 보았다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 환경변수에 값을 인입하는 방법도 있지만 그 많은 개인정보들을 환경변수에서 관리하면 데이터들이 늘어날수록 관리의 복잡성이 늘어나서 한계가 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;환경변수에는 비밀정보가 아닌 애플리케이션 실행 환경값만을 사용하는것이 좋다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;ex) 스프링 프로필명, 포트번호, JVM 힙메모리 설정&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Cloud란&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Vault를 적용하기 앞서 Spring Cloud를 알아야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud란 스프링부트를 기반으로한 분산 시스템을 개발하기 위한 일련의 도구를 제공하는 프레임워크이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k7u1M/btsJF5woL66/WqlCVH8zxjqPkxjlZF8fA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k7u1M/btsJF5woL66/WqlCVH8zxjqPkxjlZF8fA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k7u1M/btsJF5woL66/WqlCVH8zxjqPkxjlZF8fA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk7u1M%2FbtsJF5woL66%2FWqlCVH8zxjqPkxjlZF8fA0%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;595&quot; height=&quot;1044&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Cloud Config는 분산 시스템중에서 외부화된 구성에 대한 서버측 및 클라이언트를 제공하는것이다. 대표적으로 구성정보를 관리하는 서버는 git과 vault가 있고 프로젝트에는 vault를 사용하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;BootStrap&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;application.yml에 비밀 구성정보를 기입하려면 서버가 application.yml 로드전에 Vault 정보를 불러와야 하는데 이것을 해주는게 bootstrap 이다. 아래 그림에서 보듯이. bootstrap.yml 정보를 가지고 Vault를 접속하여 정보를 가져오고 application.yml에 정보를 기입하는 구조이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDAo8p/btsJFG4DbTu/EFkGRDEvDanLuUNmK24VSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDAo8p/btsJFG4DbTu/EFkGRDEvDanLuUNmK24VSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDAo8p/btsJFG4DbTu/EFkGRDEvDanLuUNmK24VSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDAo8p%2FbtsJFG4DbTu%2FEFkGRDEvDanLuUNmK24VSk%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;665&quot; height=&quot;876&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Vault 적용&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;build.gradle.kts 파일에 의존성 추가.&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 설명한 Spring Cloud Config, Vault, Bootstrap을 사용하기위해서 3가지 의존성을 추가한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;//vault
implementation(&quot;org.springframework.cloud:spring-cloud-starter-bootstrap:4.1.4&quot;)
implementation(&quot;org.springframework.cloud:spring-cloud-config-server:4.1.3&quot;)
implementation(&quot;org.springframework.cloud:spring-cloud-starter-vault-config:4.1.3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Vault 설치&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AWS EC2 Ubuntu 서버에 설치하였다. &lt;a href=&quot;https://developer.hashicorp.com/vault/install&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식사이트&lt;/a&gt;에서 제공해주는 설치방법으로는 설치가 안돼서 wget 명령어로 설치파일을 가져와서 설치하는 방법으로 설치하였다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;wget으로 zip 파일을 가져오고 unzip으로 압축을 풀어서 /usr/local/bin/ 으로 이동시키면 전역에서 vault 명령어를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;wget https://releases.hashicorp.com/vault/1.14.1/vault_1.14.1_linux_amd64.zip
unzip vault_1.14.1_linux_amd64.zip
sudo mv &amp;lt;Vault 바이너리 경로&amp;gt; /usr/local/bin/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Vault 운영서버 구축&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;vault server -dev 명령어를 통해서 개발 서버로 구축하면 Valut 서버를 로컬서버에서 밖에 접속할수 밖에 없다 그리고 설정정보를 가진 데이터들이 인메모리에 저장되기 때문에 서버를 중지시키면 데이터들이 다 날라가는 단점이 있다. 그래서 운영으로 구축하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;운영서버로 구축하려면 설정정보가 있는 파일을 생성해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1726753918196&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo mkdir -p /etc/vault
$ sudo mkdir -p /srv/vault/data
$ sudo vi /etc/vault/config.hcl&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;[config.hcl 파일내용]&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 구성내용은 &lt;a href=&quot;https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-deploy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 홈페이지의 Vault 기본 구성 매뉴얼&lt;/a&gt;&amp;nbsp;을 참고하여 작성한것이다. 만약 https 를 사용한다면 이 &lt;a href=&quot;https://developer.hashicorp.com/vault/docs/configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Vault 공식 홈페이지를&lt;/a&gt; 참고하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1726753949800&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;storage &quot;raft&quot; {
  path    = &quot;/srv/vault/data&quot;
  node_id = &quot;node1&quot;
}

listener &quot;tcp&quot; {
  address     = &quot;0.0.0.0:8200&quot;
  tls_disable = &quot;true&quot;
}

api_addr     = &quot;http://127.0.0.1:8200&quot;
cluster_addr = &quot;https://127.0.0.1:8201&quot;
ui = true&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;storage &quot;raft&quot; 방식은 HA Cluster 방식 이라고도 하는데 하나의 서버가 다운되거나 장애가 생거더라도 자동으로 복구가 가능하다.&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;1492&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DkiY4/btsJGvaxz00/wTRivLWRfFcG419zaIHFGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DkiY4/btsJGvaxz00/wTRivLWRfFcG419zaIHFGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DkiY4/btsJGvaxz00/wTRivLWRfFcG419zaIHFGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDkiY4%2FbtsJGvaxz00%2FwTRivLWRfFcG419zaIHFGk%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;1492&quot; height=&quot;628&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;listener은 Vault가 API 요청을 수신하는 방식을 구성한다. address = &quot;0.0.0.0:8200&quot; 으로 세팅하여 모든 IP에서 요청이 가능하고 tls_disable 하여 https 가 아니더라도 요청을 수락한다. Vault는 기본적으로 보안상 tls를 활성화 하는데 tls를 활성화 하려면 키파일이 필요하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;api_addr은 로컬에서 API에 접속할수 있는 주소이고, cluster_addr은 노드간의 접속을 할수 있게 하는  주소이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ui = true 로 하면 Vault UI 웹으로 접속할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Vault 백그라운드 실행&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-config 로 설정파일 경로와 파일을 지정하고 백그라운드로 실행을 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1726754506512&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo sh -c 'vault server -config=/etc/vault/config.hcl &amp;gt;&amp;gt; /var/log/vault_sys.log 2&amp;gt;&amp;amp;1 &amp;amp;'&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 아래 명령어로 vault 최초 사용을 위해 초기화 시킨다.&lt;/p&gt;
&lt;pre id=&quot;code_1726808869898&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vault operator init&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 아래 처럼 unseal key 5개와, token 이 주어진다. 이거는 잊어버리지 않고 개인보관해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpWcxl/btsJGVtxec9/cLhMeumCSk37xtn6fOaQN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpWcxl/btsJGVtxec9/cLhMeumCSk37xtn6fOaQN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpWcxl/btsJGVtxec9/cLhMeumCSk37xtn6fOaQN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpWcxl%2FbtsJGVtxec9%2FcLhMeumCSk37xtn6fOaQN1%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;1384&quot; height=&quot;384&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Vault UI 접속&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 IP:8200번 포트로 접속하면 아래 UI 화면이 들어가진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Unseal Key Portion에 위에서 받은 Unseal key를 3번 입력하면 오른쪽 화면으로 변경되고&amp;nbsp;위에서 받은 Token 값을 인입해주고 Sign in 해주면된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFhl09/btsJGvoyUej/9yTUGkziGvi8AjL4IWEWlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFhl09/btsJGvoyUej/9yTUGkziGvi8AjL4IWEWlK/img.png&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;1078&quot; data-is-animation=&quot;false&quot; width=&quot;599&quot; height=&quot;433&quot; data-widthpercent=&quot;55.94&quot; style=&quot;width: 55.2911%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFhl09/btsJGvoyUej/9yTUGkziGvi8AjL4IWEWlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFhl09%2FbtsJGvoyUej%2F9yTUGkziGvi8AjL4IWEWlK%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;1490&quot; height=&quot;1078&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duHHQ7/btsJGmkS0pH/o7Mu7BIVxNmz4gVAJHcmb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duHHQ7/btsJGmkS0pH/o7Mu7BIVxNmz4gVAJHcmb0/img.png&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;1016&quot; data-is-animation=&quot;false&quot; width=&quot;600&quot; height=&quot;551&quot; style=&quot;width: 43.5461%;&quot; data-widthpercent=&quot;44.06&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duHHQ7/btsJGmkS0pH/o7Mu7BIVxNmz4gVAJHcmb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduHHQ7%2FbtsJGmkS0pH%2Fo7Mu7BIVxNmz4gVAJHcmb0%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;1106&quot; height=&quot;1016&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Vault KV 데이터 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨처음 접속된 Secrets Engines 에서 Enable new engine + 버튼을 클릭하고&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;2142&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZZALX/btsJFGRJFf5/Z7G2KbzMIBr8aryiN2BgK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZZALX/btsJFGRJFf5/Z7G2KbzMIBr8aryiN2BgK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZZALX/btsJFGRJFf5/Z7G2KbzMIBr8aryiN2BgK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZZALX%2FbtsJFGRJFf5%2FZ7G2KbzMIBr8aryiN2BgK0%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;2142&quot; height=&quot;610&quot; data-origin-width=&quot;2142&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KV를 만들어준다. 이름은 secret 으로 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;1260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbTtvT/btsJFGqDytp/KFNxKhQ6kXxrCXF8mkHD3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbTtvT/btsJFGqDytp/KFNxKhQ6kXxrCXF8mkHD3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbTtvT/btsJFGqDytp/KFNxKhQ6kXxrCXF8mkHD3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbTtvT%2FbtsJFGqDytp%2FKFNxKhQ6kXxrCXF8mkHD3K%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;685&quot; height=&quot;550&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;1260&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;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 application 이름으로 Create secret을 만들어주고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kU1mL/btsJGiC16KR/LZKyMDSv37N1nHAvkqhq3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kU1mL/btsJGiC16KR/LZKyMDSv37N1nHAvkqhq3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kU1mL/btsJGiC16KR/LZKyMDSv37N1nHAvkqhq3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkU1mL%2FbtsJGiC16KR%2FLZKyMDSv37N1nHAvkqhq3k%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;2138&quot; height=&quot;508&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;508&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;아래와 같이 application.yml 에 필요한 정보들을 기입해주면된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;1146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJ3BH/btsJF5cRKnr/M6sAN9rk8XZuDpzBhHXoh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJ3BH/btsJF5cRKnr/M6sAN9rk8XZuDpzBhHXoh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJ3BH/btsJF5cRKnr/M6sAN9rk8XZuDpzBhHXoh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJ3BH%2FbtsJF5cRKnr%2FM6sAN9rk8XZuDpzBhHXoh1%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;2224&quot; height=&quot;1146&quot; data-origin-width=&quot;2224&quot; data-origin-height=&quot;1146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스프링부트 Vault  연동&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트로 돌아가서 bootstrap.yml 파일을 만들고 아래 정보들을 기입해주면된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;spring.cloud.vault.uri에 주소를 넣어주고, spring.cloud.vault.token에 발급받은 token값을 기입해주면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;kv.backed: 값과, default-context 값으로 위에 Vault UI에서 만든 secret/application의 값들을 가져올수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5MvNB/btsJHpVidMr/PCvhEtg3MUFQL0xJmK0cck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5MvNB/btsJHpVidMr/PCvhEtg3MUFQL0xJmK0cck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5MvNB/btsJHpVidMr/PCvhEtg3MUFQL0xJmK0cck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5MvNB%2FbtsJHpVidMr%2FPCvhEtg3MUFQL0xJmK0cck%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;2012&quot; height=&quot;626&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Vault 데이터 적용&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 application.yml 에 application에 만든 값을 ${} 로 통해서 쓸수있다.&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;1318&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8LRRU/btsJF457SqK/hUzDUJIRGAn6yzP8AG3Pb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8LRRU/btsJF457SqK/hUzDUJIRGAn6yzP8AG3Pb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8LRRU/btsJF457SqK/hUzDUJIRGAn6yzP8AG3Pb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8LRRU%2FbtsJF457SqK%2FhUzDUJIRGAn6yzP8AG3Pb1%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;735&quot; height=&quot;388&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Vault 데이터 확인&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;값이 제대로 가져왔는지는 테스트 코드를 통해서 확인하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xbyZ0/btsJHAidv3q/eoJ9x7gThzpYjZcfKbk4Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xbyZ0/btsJHAidv3q/eoJ9x7gThzpYjZcfKbk4Tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xbyZ0/btsJHAidv3q/eoJ9x7gThzpYjZcfKbk4Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxbyZ0%2FbtsJHAidv3q%2FeoJ9x7gThzpYjZcfKbk4Tk%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;526&quot; height=&quot;350&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - &lt;a href=&quot;https://wlsdn3004.tistory.com/11&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wlsdn3004.tistory.com/11&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - &lt;a href=&quot;https://blog.naver.com/wideeyed/222099597474&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.naver.com/wideeyed/222099597474&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - &lt;a href=&quot;https://velog.io/@gweowe/Vault-%EC%84%A4%EC%B9%98-%EB%B0%8F-dev-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95-Ubuntu&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@gweowe/Vault-%EC%84%A4%EC%B9%98-%EB%B0%8F-dev-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95-Ubuntu&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - &lt;a href=&quot;https://ssdragon.tistory.com/163#%F-%-F%--%A-%--Vault%EB%A-%BC%--%EB%-F%--%EC%BB%A-%EB%A-%-C%--%EC%-B%A-%ED%--%--%ED%--%--%EA%B-%B-&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ssdragon.tistory.com/163#%F-%-F%--%A-%--Vault%EB%A-%BC%--%EB%-F%--%EC%BB%A-%EB%A-%-C%--%EC%-B%A-%ED%--%--%ED%--%--%EA%B-%B-&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - &lt;a href=&quot;https://www.elancer.co.kr/blog/view?seq=248&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.elancer.co.kr/blog/view?seq=248&lt;/a&gt;&lt;/p&gt;</description>
      <category>Project/B2C-Side-Project(third)</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/296</guid>
      <comments>https://deftkang.tistory.com/296#entry296comment</comments>
      <pubDate>Tue, 17 Sep 2024 17:48:38 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot] 회원가입 구조 퍼사드 패턴 적용과 SRP 원칙 적용</title>
      <link>https://deftkang.tistory.com/294</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입을 하기 위해서&amp;nbsp; 3가지 흐름이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;회원가입 로직&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 회원가입시 입력한 인증번호 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. member 테이블에 회원정보 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. account 테이블에 회원 인증정보 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;위 로직 설명&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;account 테이블이 따로 있는 이유는 회원 종류가 늘어날때 회원 종류의 테이블을 모두 돌아야 해서 회원 종류의 확장을 고려해 account 인증 테이블을 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;member테이블에 먼저 저장하는 이유는 member 테이블에 있는 전화번호 등 유효성검사를 하고 member 엔티티가 저장되고 생성된 id 값을 account 테이블에 넣기 위해서이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[기존 코드]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존코드에서는 아래 코드처럼 Service 하나에서 위 로직을 모두 넣었다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
override fun signUp(createMemberDTO: CreateMemberRequest): Long {
    //인증번호 확인
    val certifyNumFoundByRedis = redisUtil.getData(email)

    if (certifyNumFoundByRedis != certifyNum) {
        throw SignupCertifyNumAuthFailedException(ErrorCode.INVALID_AUTH_CERTIFY_NUM)
    }

    //인증정보 저장
    saveAccountValidCheck(createMemberDTO)
    
    val account = Account(
        email = createMemberDTO.email,
        password = passwordEncoder.encode(createMemberDTO.password),
        role = MemberRole.USER,
        userId = memberId,
    )

    accountRepository.save(account)
    
    //회원정보 저장
    signUpValidCheck(createMemberDTO)

    val member = Member(
        telNo = createMemberDTO.telNo,
        name = createMemberDTO.name,
        memberShoppingActivity = MemberShoppingActivity.init(),
    )

    memberRepository.save(member)

    return member.memberId!!
}&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;위 코드에서의 문제는 signUp() 하나의 메서드 안에서 인증번호 확인, 인증정보 저장, 회원정보 저장 3개의 기능이 분리 되지 않고 같이 있기 때문에 SOLID 원칙에서 SRP 규칙에 어긋난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래처럼 Controller와 Service 가운데에 ServiceApplication 클래스를 한개둬서 3개의 기능을 분리 했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;SRP를 지키지 않은 경우에 코드 크기가 커지면서 연관성이 적은 코드가 한 클래스에 위치할 가능성이 높아진다. 이는 결과적으로 코드를 이해하는데 방해가 된다.&amp;nbsp; 코드가 분리돼있지 않으면 인증번호 확인 로직만 수정하고 싶을때 인증정보 저장, 회원정보 저장 로직도 같이 볼수 밖에 없기 때문이다. 이것은 코드를 점점 얽히게 만들어서 유지보수와 확장성에 좋지 않다.&lt;/blockquote&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;1584&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJihj7/btsJEQlrn2W/JoHz2fjyfQdCko3ommt9p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJihj7/btsJEQlrn2W/JoHz2fjyfQdCko3ommt9p1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJihj7/btsJEQlrn2W/JoHz2fjyfQdCko3ommt9p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJihj7%2FbtsJEQlrn2W%2FJoHz2fjyfQdCko3ommt9p1%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;1584&quot; height=&quot;372&quot; data-origin-width=&quot;1584&quot; data-origin-height=&quot;372&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;하나의 서비스에서 여러개의 서비스를 나눴을때 여러개의 서비스를 실행시켜주는 매개체 역할이 필요한데 그것이 위에서 MemberServiceApplication이다. 이것이 퍼사드와 같은 역할을 해줘서 퍼사드 패턴이 될수있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;퍼사드 패턴이란 여러 요소들로 구성된 복잡한 시스템을 서브시스템으로 분리하고 하나의 시스템(퍼사드 객체) 에서 처리할수 있게 해주는 패턴이다. 퍼사드 패턴은 복잡한 시스템을 분리시켜서 유지보수와 확장성에 용이하기 해주게 때문에 위 문제를 해결하기 위해서 가장 적합한 디자인 패턴이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 기능들을 서브시스템으로 나누기 위해 인증번호를 확인하는 AuthenticationService, 인증정보를 저장하는 AccountService, 회원정보를 저장하는 MemberService를 따로 따로 분리 하였다. 이 3개의 Service를 나누면 각각의 서브시스템으로 나뉘어 진것이다.&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;[AuthenticationService]&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun verifyEmailCertifyNum(email: String, certifyNum: String) {
    val certifyNumFoundByRedis = redisUtil.getData(email)

    if (certifyNumFoundByRedis != certifyNum) {
        throw SignupCertifyNumAuthFailedException(ErrorCode.INVALID_AUTH_CERTIFY_NUM)
    }
}&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;&lt;b&gt;[AccountService]&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Transactional
override fun saveAccount(memberId: Long, createMemberDTO: CreateMemberRequest): Account {
    saveAccountValidCheck(createMemberDTO)

    val account = Account(
        email = createMemberDTO.email,
        password = passwordEncoder.encode(createMemberDTO.password),
        role = MemberRole.USER,
        userId = memberId,
    )

    return accountRepository.save(account)
}&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;&lt;b&gt;[MemberService]&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Transactional
override fun saveMember(createMemberDTO: CreateMemberRequest): Long {
    signUpValidCheck(createMemberDTO)

    val member = Member(
        telNo = createMemberDTO.telNo,
        name = createMemberDTO.name,
        memberShoppingActivity = MemberShoppingActivity.init(),
    )

    memberRepository.save(member)

    return member.memberId!!
}&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;그리고 퍼사드 객체 역할을 해주는 MemberServiceApplication 클래스를 만들고 에 위 3개의 Service를 의존하고 각각의 기능을 적용시켰다. 그러면 복잡한 서브 시스템들을 하나의 객체에서 처리할수 있고 이것이 퍼사드 패턴이다.&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;[MemberServiceApplication]&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class MemberServiceApplication(
    private val memberService: MemberService,
    private val accountService: AccountService,
    private val authenticationService: AuthenticationService,
) {
    @Transactional
    fun signUp(createMemberRequest: CreateMemberRequest): Long {
        //인증번호 확인
        authenticationService.verifyEmailCertifyNum(createMemberRequest.email, createMemberRequest.certifyNum)

        val memberId = memberService.saveMember(createMemberRequest)
        accountService.saveAccount(memberId, createMemberRequest)

        return memberId
    }
}&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;참고 : &lt;a href=&quot;https://velog.io/@bagt/Design-Pattern-Facade-Pattern-%ED%8D%BC%EC%82%AC%EB%93%9C-%ED%8C%A8%ED%84%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@bagt/Design-Pattern-Facade-Pattern-%ED%8D%BC%EC%82%AC%EB%93%9C-%ED%8C%A8%ED%84%B4&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/B2C-Side-Project(third)</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/294</guid>
      <comments>https://deftkang.tistory.com/294#entry294comment</comments>
      <pubDate>Tue, 17 Sep 2024 17:33:48 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot] 3차 프로젝트 자바에서 코틀린 전환 회고</title>
      <link>https://deftkang.tistory.com/295</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존 2차 프로젝트에서 기준에서 3차 프로젝트를 하면서 2차 프로젝트의 자바 코드를 코틀린 코드로 변환을 완료하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/ClothingStoreService/clothstar_v2.0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2차 프로젝트 깃허브 링크&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/ClothingStoreService/clothstar_v3.0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3차 프로젝트 깃허브 링크&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;Spring Boot 3.3.2&lt;/li&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코틀린 사용하면서 느낀 장단점&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Lombok 제거&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전환과정에서 자바와 가장 컸던 점은 코틀린에서는 Lombok을 안쓴다는 점이다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Lombok을 안쓰는 이유&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;코틀린에서는 타입추론 기능이 있어서 변수앞에 타입을 안붙인다. 변수 값에 의해서 타입이 정해진다. 그래서 변수 앞에는 val이나 var을 붙이는데 val은 Getter 기능이 포함되어 있고 var은 Getter, Setter가 포함되어 있다.&lt;/li&gt;
&lt;li&gt;@Data 대신에 Data Class를 사용하면 equals(), hashcode(), toString()를 컴파일러 시점에 자동으로 생성한다.&lt;/li&gt;
&lt;li&gt;Named Arguments 문법적 특징으로 @Builder 를 안써도 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 불편했던 점은 Lombok의 @&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;Slf4j 어노테이션을 쓸수 없다보니&amp;nbsp;&lt;/span&gt;코틀린에서는 로그를 작성하려면 아래 코드를 이용해서 작성해야 한다. ${}는 변수값을 사용한다는 뜻이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;//의존성 추가
implementation(&quot;io.github.oshai:kotlin-logging-jvm:5.1.4&quot;)

//클래스 내부에 추가
private val log = KotlinLogging.logger {}

//로그 사용 예시
log.info { &quot;회원 상세 조회 memberId = ${memberId}&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Optional 제거&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 ? 를 사용해서 이 값이 null이 될수 있는지 없는지를 항상 생각하고 붙여줘야 한다. 그래서 코틀린에서는 optional을 안쓴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실 자바코드를 짜면서 null 허용 여부를 신경 안쓰고 무조건 Optional을 써서 작업하였는데 코틀린에서는 ? 여부를 항상 붙여줄수 있다보니 null 허용 여부를 항상 신경쓰면서 개발하였다. 그래서 코드가 더 깔끔해졌다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA를 사용할때 findById() 함수를 사용할때 Optional을 강요하여서 코틀린에서도 어쩔수 없이 사용될수 밖에 없다고 생각하였지만&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;1210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NbW7U/btsJDe68IbK/VMCL4965XAU6bvrNzEKOwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NbW7U/btsJDe68IbK/VMCL4965XAU6bvrNzEKOwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NbW7U/btsJDe68IbK/VMCL4965XAU6bvrNzEKOwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNbW7U%2FbtsJDe68IbK%2FVMCL4965XAU6bvrNzEKOwK%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;1346&quot; height=&quot;1210&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;1210&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;코틀린에서 반환 타입에 ? 를 붙이면 nullable 타입이 되고 findByIdOrNull 확장함수를 사용할 수 있어서 Optional을 사용하지 않아도 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2076&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u1K1Y/btsJBojHmT0/hOkj39yiBZHcFtkDKp3ddK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u1K1Y/btsJBojHmT0/hOkj39yiBZHcFtkDKp3ddK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u1K1Y/btsJBojHmT0/hOkj39yiBZHcFtkDKp3ddK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu1K1Y%2FbtsJBojHmT0%2FhOkj39yiBZHcFtkDKp3ddK%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;2076&quot; height=&quot;296&quot; data-origin-width=&quot;2076&quot; data-origin-height=&quot;296&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W5YEZ/btsJBNwJ4Ql/pTbrBSgO8XLCF0WUkGTZaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W5YEZ/btsJBNwJ4Ql/pTbrBSgO8XLCF0WUkGTZaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W5YEZ/btsJBNwJ4Ql/pTbrBSgO8XLCF0WUkGTZaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW5YEZ%2FbtsJBNwJ4Ql%2FpTbrBSgO8XLCF0WUkGTZaK%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;1106&quot; height=&quot;120&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;120&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;확장함수 ?.let 사용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린에서는 다양한 확장함수가 있는데 이중에서 let을 가장 많이 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;let 확장함수는 아래처럼 T 객체 타입에서 R 객체타입으로 변환할 수 있는 함수인데&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun &amp;lt;T, R&amp;gt; T.let(block: (T) -&amp;gt; R): R
&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;nullable 타입에서 사용하게 된다면&amp;nbsp; T?.let{ } 에서 {} 안에는 non-null 값만 들어올수 있어서 ?: 와 같이 사용했을때 굉장히 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 memberRepository 에서 Id값으로 객체를 불러왔을때 객체 값이 없으면 MemberResponse로 타입을 변환시키고 그게 아니면 NotFoundMemberExcetpion을 발생시키는 코드이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btwI0S/btsJBLTf1n9/FOuP6vHugynauyJkyU7kFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btwI0S/btsJBLTf1n9/FOuP6vHugynauyJkyU7kFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btwI0S/btsJBLTf1n9/FOuP6vHugynauyJkyU7kFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtwI0S%2FbtsJBLTf1n9%2FFOuP6vHugynauyJkyU7kFK%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;1242&quot; height=&quot;406&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 프로젝트에서 3차 프로젝트로 넘어가면서 코틀린 전환을 하였는데 첨에는 속도가 많이 더디고 느렸지만 점차 하면서 코드가 간결해지는것을 느꼈다. 새로운 언어를 배운다는것은 쉽지 않았지만 왜 다양한 기업에서 코틀린으로 전환하려고 하는지 느낄수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/B2C-Side-Project(third)</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/295</guid>
      <comments>https://deftkang.tistory.com/295#entry295comment</comments>
      <pubDate>Tue, 20 Aug 2024 12:58:46 +0900</pubDate>
    </item>
    <item>
      <title>[SpringBoot] Redis 활용한 회원가입 이메일 인증 방법(개선후)</title>
      <link>https://deftkang.tistory.com/293</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://deftkang.tistory.com/292&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;회원가입 이메일 인증 방법 개선전&lt;/a&gt;에는 MemberEntity에 enabled 필드를 추가하여 회원가입시 DB에 가입정보를 insert할때 enabled 필드에 false값을 넣어서 회원을 비활성화 하고 전송된 인증메일에 링크를 누르면 enabled 값을 true로 변경하여 회원을 활성화 시켰다. 하지만 이 방법은 회원이 인증을 하지 않으면 DB에 쓰레기 값만 남게 되고 회원이 다시 똑같은 아이디(email)로 회원가입을 못한다.&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;인증번호 유효성은 Redis를 활용하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB를 사용안하고 Redis를 활용한 이유는 DB에 직접 접근하지 않고 Redis 캐싱을 활용하여 인증을 빨리 할수있고 데이터에 시간을 설정하여 자동으로 삭제되게 할수있는 휘발성 데이터라는 점에서 선택을 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;0-프로젝트-환경&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;0. 프로젝트 환경&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #555555; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;Spring Boot 3.2.5&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;Spring Boot Data Redis 3.2.5&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #666666;&quot;&gt;Lettuce 6.3.2&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Redis 의존성 추가와 Redis Server 설치&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle에 redis 의존성을 추가한다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;//Redis 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-redis'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 의존성을 추가하면 Redis Client인 Lettuce가 설치된다. Redis Client는 대표적으로 Jedis와 Lettuce가 있는데 SpringBoot 2.0 부터는 기본적으로 Lettuce가 사용된다.&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;862&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7mhsR/btsIpMyFiW1/kmjTSMljMt5KQbnGKLM9z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7mhsR/btsIpMyFiW1/kmjTSMljMt5KQbnGKLM9z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7mhsR/btsIpMyFiW1/kmjTSMljMt5KQbnGKLM9z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7mhsR%2FbtsIpMyFiW1%2FkmjTSMljMt5KQbnGKLM9z1%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;862&quot; height=&quot;176&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce는 동기, 비동기및 반응형 API를 제공하는 확장 가능하고 thread-safe한 Redis 클라이언트 이다. 반면에 Jedis는 사용법이 간단하지만 thread-safe 하지 않다. 그래서 멀티 스레드 환경에서 적합하지 않다.&amp;nbsp;&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;b&gt;2. Redis Server 설치&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥북을 사용하면 brew로 쉽게 설치가 가능하다. redis-cli로 server로 접속이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;set [key] [value] 로 데이터 인입, get [key]로 데이터 호출, del [key] 로 데이터 삭제가 가능하다. 모든 데이터를 보려면 keys * 를 사용하면된다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgb1eq/btsIpLfqglB/2jfZCnK0whkEBdz048IbDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgb1eq/btsIpLfqglB/2jfZCnK0whkEBdz048IbDk/img.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;934&quot; data-is-animation=&quot;false&quot; width=&quot;270&quot; height=&quot;257&quot; style=&quot;width: 42.4135%; margin-right: 10px;&quot; data-widthpercent=&quot;42.91&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgb1eq/btsIpLfqglB/2jfZCnK0whkEBdz048IbDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgb1eq%2FbtsIpLfqglB%2F2jfZCnK0whkEBdz048IbDk%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;982&quot; height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkAeyg/btsIre1LgAM/8TesbDN2x3VU7ecD5jb2h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkAeyg/btsIre1LgAM/8TesbDN2x3VU7ecD5jb2h1/img.png&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;306&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;57.09&quot; style=&quot;width: 56.4237%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkAeyg/btsIre1LgAM/8TesbDN2x3VU7ecD5jb2h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkAeyg%2FbtsIre1LgAM%2F8TesbDN2x3VU7ecD5jb2h1%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;428&quot; height=&quot;306&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Redis 설정 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml에 아래 설정을 추가한다. Redis Server는 기본적으로 6379 포트 번호를 사용한다. duration은 600 기입하여 10분으로 기입하였다. 10분이 지나면 redis data는 자동으로 삭제된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  # Redis
  data:
    redis:
      host: localhost
      port: 6379
      duration: 600&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;b&gt;4. Redis Server 연결 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redisConnectionFactory() 함수로 application.yml에 기입한 host와 port로&amp;nbsp; Redis Server와 연결한 Lettuce 클라이언트 객체를 생성한다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
public class RedisConfig {
    @Value(&quot;${spring.data.redis.host}&quot;)
    private String host;

    @Value(&quot;${spring.data.redis.port}&quot;)
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;b&gt;5. Redis Util 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Server에 데이터를 생성하고 삭제하고 인증번호를 만드는 기능이 있는 Util 클래스이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Random;

@RequiredArgsConstructor
@Service
public class RedisUtil {
    private final StringRedisTemplate template;

    @Value(&quot;${spring.data.redis.duration}&quot;)
    private int duration;

    public String getData(String key) {
        ValueOperations&amp;lt;String, String&amp;gt; valueOperations = template.opsForValue();
        return valueOperations.get(key);
    }

    public boolean existData(String key) {
        return Boolean.TRUE.equals(template.hasKey(key));
    }

    public void setDataExpire(String key, String value) {
        ValueOperations&amp;lt;String, String&amp;gt; valueOperations = template.opsForValue();
        Duration expireDuration = Duration.ofSeconds(duration);
        valueOperations.set(key, value, expireDuration);
    }

    public void deleteData(String key) {
        template.delete(key);
    }

    public void createRedisData(String toEmail, String code) {
        if (existData(toEmail)) {
            deleteData(toEmail);
        }

        setDataExpire(toEmail, code);
    }

    public String createdCertifyNum() {
        int leftLimit = 48; // number '0'
        int rightLimit = 122; // alphabet 'z'
        int targetStringLength = 6;
        Random random = new Random();

        return random.ints(leftLimit, rightLimit + 1)
                .filter(i -&amp;gt; (i &amp;lt;= 57 || i &amp;gt;= 65) &amp;amp;&amp;amp; (i &amp;lt;= 90 || i &amp;gt;= 97))
                .limit(targetStringLength)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StringRedisTemplate 클래스는 &lt;a href=&quot;https://spring.io/projects/spring-data-redis#overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Redis Data&lt;/a&gt; 에서 제공하는 클래스이다. RedisTemplate&amp;lt;String,String&amp;gt; 클래스를 상속받았고 StringRedisTemplate의 defaultSerializer는 StringRedisSerializer이다. StringRedisTemplate bean은 일반적인 String 값을 key, value로 사용하는 경우 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setDataExpire() 함수로 Redis Server에 데이터를 인입하며, application.yml에 설정한 duration값을 이용하여 10분후에 데이터가 삭제되게 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createdCertifyNum() 함수는 숫자 영문자 합쳐서 랜덤으로 인증번호 6자리를 만든다.&amp;nbsp;&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;b&gt;6. mailTemplate.html 생성과 MailBuilder 클래스 생성&lt;/b&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div style=&quot;margin:120px&quot;&amp;gt;
    &amp;lt;div style=&quot;margin-bottom: 10px&quot;&amp;gt;
        &amp;lt;h1&amp;gt;인증 코드 메일입니다.&amp;lt;/h1&amp;gt;
        &amp;lt;br/&amp;gt;
        &amp;lt;h3&amp;gt; 아래 코드를 사이트에 입력해주십시오&amp;lt;/h3&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;h2 style=&quot;color: crimson;&quot; th:text=&quot;${certifyNum}&quot;&amp;gt;&amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;br/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;


@Service
@RequiredArgsConstructor
public class MailContentBuilder {
    private final TemplateEngine templateEngine;

    public String build(String certifyNum) {
        Context context = new Context();
        context.setVariable(&quot;certifyNum&quot;, certifyNum);
        return templateEngine.process(&quot;mailTemplate&quot;, context);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;Template Engine은 템플릿 양식과 데이터를 합쳐서 HTML을 보여준다. context.setVariable(&quot;certifyNum&quot;, certifyNum) 를 통해서 위에서 만든 html에서 ${certifyNum} 에 대한 값을 매핑해 줄 수 있다.&lt;/span&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;b&gt;7. email 전송 Controller, Service 생성&lt;/b&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Operation(summary = &quot;이메일로 인증번호 전송&quot;, description = &quot;기입한 이메일로 인증번호를 전송합니다.&quot;)
@PostMapping(&quot;/v1/members/auth&quot;)
public void signupEmailAuthentication(@Validated @RequestBody CertifyNumRequest certifyNumRequest) {
    memberServiceApplication.signupCertifyNumEmailSend(certifyNumRequest.getEmail());
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public void signupCertifyNumEmailSend(String email) {
    sendEmailAuthentication(email);
    log.info(&quot;인증번호 전송 완료, email = {}&quot;, email);
}

private void sendEmailAuthentication(String toEmail) {
    String certifyNum = redisUtil.createdCertifyNum();
    String message = mailContentBuilder.build(certifyNum);
    MailSendDTO mailSendDTO = new MailSendDTO(toEmail, &quot;clothstar 회원가입 인증 메일 입니다.&quot;, message);

    mailService.sendMail(mailSendDTO);

    //메일 전송에 성공하면 redis에 key = email, value = 인증번호를 생성한다.
    //지속시간은 10분
    redisUtil.createRedisData(toEmail, certifyNum);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createdCertifyNum() 함수로 인증번호를 만들고 템플릿엔진 html 데이터를 message에 담아 인증번호 메일을 전송한다. 그리고 key는 email, value는 인증번호인 redis 데이터를 생성한다.&amp;nbsp;&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;b&gt;8. 회원가입 Service로직에 인증번호 확인 로직 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRpEFE/btsIpi5XVme/Z3wECCylCETydcCg0nkkg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRpEFE/btsIpi5XVme/Z3wECCylCETydcCg0nkkg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRpEFE/btsIpi5XVme/Z3wECCylCETydcCg0nkkg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRpEFE%2FbtsIpi5XVme%2FZ3wECCylCETydcCg0nkkg1%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;1734&quot; height=&quot;838&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public Boolean verifyEmailCertifyNum(String email, String certifyNum) {
    String certifyNumFoundByRedis = redisUtil.getData(email);
    if (certifyNumFoundByRedis == null) {
        return false;
    }

    return certifyNumFoundByRedis.equals(certifyNum);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증번호가 틀리면 예외처리를 하고 맞으면 회원데이터를 DB에 insert 한다.&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;9. 회원가입 html에서 인증번호 전송 버튼을 추가하고 인증번호 메일 전송 js 추가&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdgCuN/btsIpjqf7SN/iYpJyFnRbghgH1G3n48nEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdgCuN/btsIpjqf7SN/iYpJyFnRbghgH1G3n48nEK/img.png&quot; data-alt=&quot;signup.html에서 인증번호 버튼&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdgCuN/btsIpjqf7SN/iYpJyFnRbghgH1G3n48nEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdgCuN%2FbtsIpjqf7SN%2FiYpJyFnRbghgH1G3n48nEK%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;1454&quot; height=&quot;318&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;signup.html에서 인증번호 버튼&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const certifyNumEmailSendButton = document.getElementById(&quot;certifyNum-btn&quot;);

if (certifyNumEmailSendButton) {
    certifyNumEmailSendButton.addEventListener(&quot;click&quot;, (event) =&amp;gt; {
        const emailValue = document.getElementById(&quot;email&quot;).value;
        if (emailValue == null || emailValue == &quot;&quot;) {
            alert(&quot;이메일을 입력해 주세요&quot;);
        } else {
            fetch(`/v1/members/auth`, {
                method: &quot;POST&quot;,
                headers: {
                    &quot;Content-Type&quot;: &quot;application/json&quot;,
                },
                body: JSON.stringify({
                    email: emailValue
                }),
            }).then((res) =&amp;gt; {
                if (res.ok) {
                    alert(&quot;인증번호가 전송 되었습니다.&quot;)
                }
            }).catch(() =&amp;gt; {
                console.log(&quot;catch&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;10. 화면 테스트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일을 기입하고 인증번호 전송을 누르면 기입한 이메일로 인증 코드가 전송된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vdn68/btsIqxHzJ7o/kTnn8vzv1mcTycg4Zy1f81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vdn68/btsIqxHzJ7o/kTnn8vzv1mcTycg4Zy1f81/img.png&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;1678&quot; data-is-animation=&quot;false&quot; width=&quot;737&quot; height=&quot;1278&quot; data-widthpercent=&quot;37.29&quot; style=&quot;width: 36.8528%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vdn68/btsIqxHzJ7o/kTnn8vzv1mcTycg4Zy1f81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvdn68%2FbtsIqxHzJ7o%2FkTnn8vzv1mcTycg4Zy1f81%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;968&quot; height=&quot;1678&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bknSrO/btsIp8OMvjG/0oX8vKLiOy4mpamsMknFkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bknSrO/btsIp8OMvjG/0oX8vKLiOy4mpamsMknFkk/img.png&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;942&quot; data-is-animation=&quot;false&quot; style=&quot;width: 61.9844%;&quot; data-widthpercent=&quot;62.71&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bknSrO/btsIp8OMvjG/0oX8vKLiOy4mpamsMknFkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbknSrO%2FbtsIp8OMvjG%2F0oX8vKLiOy4mpamsMknFkk%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;914&quot; height=&quot;942&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;p data-ke-size=&quot;size16&quot;&gt;만약에 인증번호가 잘못되면 인증번호가 잘못 되었다고 alert창을 보여주고 인증번호를 제대로 기입하면 회원가입을 완료 시킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjd4Py/btsIrsesvF0/MXTh1xcZaZ3ZFIAhRgKiTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjd4Py/btsIrsesvF0/MXTh1xcZaZ3ZFIAhRgKiTK/img.png&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;1660&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.3934%; margin-right: 10px;&quot; data-widthpercent=&quot;49.97&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjd4Py/btsIrsesvF0/MXTh1xcZaZ3ZFIAhRgKiTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcjd4Py%2FbtsIrsesvF0%2FMXTh1xcZaZ3ZFIAhRgKiTK%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;898&quot; height=&quot;1660&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HpDe2/btsIqCIRTz4/pXTKwQlxKG3R6rkC1TVfu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HpDe2/btsIqCIRTz4/pXTKwQlxKG3R6rkC1TVfu0/img.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;1662&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4438%;&quot; data-widthpercent=&quot;50.03&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HpDe2/btsIqCIRTz4/pXTKwQlxKG3R6rkC1TVfu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHpDe2%2FbtsIqCIRTz4%2FpXTKwQlxKG3R6rkC1TVfu0%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;900&quot; height=&quot;1662&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;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://velog.io/@juno0713/Spring-Redis-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@juno0713/Spring-Redis-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/B2C-Side-Project(second)</category>
      <author>deftkang</author>
      <guid isPermaLink="true">https://deftkang.tistory.com/293</guid>
      <comments>https://deftkang.tistory.com/293#entry293comment</comments>
      <pubDate>Sat, 6 Jul 2024 23:48:52 +0900</pubDate>
    </item>
  </channel>
</rss>