<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발하는 자몽</title>
    <link>https://backend-jaamong.tistory.com/</link>
    <description>개발 관련 포스팅</description>
    <language>ko</language>
    <pubDate>Wed, 15 Apr 2026 01:50:04 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jaamong</managingEditor>
    <image>
      <title>개발하는 자몽</title>
      <url>https://tistory1.daumcdn.net/tistory/4999953/attach/51989356c6fc4d1e9adb19599a9101b6</url>
      <link>https://backend-jaamong.tistory.com</link>
    </image>
    <item>
      <title>[AWS] CloudWatch 사용해보기 2: 커스텀 지표 수집 및 커스텀 대시보드 생성</title>
      <link>https://backend-jaamong.tistory.com/202</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/201&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 글&lt;/a&gt;에서는 AWS가 기본적으로 여러 네임스페이스와 지표를 제공하는 것을 확인했었다. 기본 제공 데이터 외에도 사용자가 커스텀 지표를 생성하여 이 지표가 속하는 네임스페이스를 생성할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;커스텀 지표 수집&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;커스텀 지표를 생성하는 이유는 AWS에서 기본 제공하는 지표로는 확인할 수 없는 항목들이 있기 때문이다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;EC2 메모리 및 디스크와 관련된 지표들은 AWS에서 기본 제공하지 않고, EC2 인스턴스에 CloudWatch agent를 설치하여 커스텀 지표를 수집하도록 설정해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;진행하는 환경은 EC2 - Ubuntu 24.04 LTS이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt; &lt;a href=&quot;https://everenew.tistory.com/464&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;기본 제공하지 않고 OS에 설치해야 하는 이유&lt;/a&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Agent 설치&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM 역할 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2에게 부여할 IAM 역할부터 생성하자. 이 역할은 EC2가 지표를 수집하고, 로그를 포함하여 CloudWatch로 보낼 수 있는 권한을 갖는다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;IAM &amp;gt; 역할&quot;로 이동하여 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;역할 생성&lt;/b&gt;&lt;/span&gt;을 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1단계 - 신뢰할 수 있는 엔터티&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;신뢰할 수 있는 엔터티 유형: AWS 서비스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용 사례: EC2&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2단계 - 권한 추가&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검색창에 `CloudWatchAgentServerPolicy`를 입력 및 선택한다. 다음으로 넘어간다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3단계 - &lt;a style=&quot;background-color: #fcfcfd; color: #000000; text-align: start;&quot;&gt;이름 지정, 검토 및 생성&lt;/a&gt; &lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할 이름과 설명은 자유롭게 입력하되, 어떤 목적으로 생성되었는지 드러나도록 작성한다. 이제 완료.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 생성한 역할을 EC2에 부여해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;EC2 &amp;gt; 인스턴스&quot;로 이동하고 역할을 부여해야 하는 EC2를 선택하자. 선택하고 &quot;작업 &amp;gt; 보안 &amp;gt; IAM 역할 수정&quot;을 수행한다. 이동한 화면에서 방금 생성한 역할을 선택하고 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;에이전트 설치 (Ubuntu 기준)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2에 접속한 상태에서 진행한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에이전트 패키지를 다운로드한다.&lt;/span&gt;
&lt;pre id=&quot;code_1764316049588&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다운로드한 패키지를 설치한다.&lt;/span&gt;
&lt;pre id=&quot;code_1764316059900&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo dpkg -i -E ./amazon-cloudwatch-agent.deb&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설치 파일 위치로 이동한다.&amp;nbsp;&lt;/span&gt;
&lt;pre id=&quot;code_1764316070921&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd /opt/aws/amazon-cloudwatch-agent/bin&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에이전트를 실행한다. &lt;/span&gt;
&lt;pre id=&quot;code_1764316080267&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./amazon-cloudwatch-agent-config-wizard&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행하면 다음의 질문들이 나온다. (작성할 때 라이브로 진행한 게 아니라서 조금 다를 수 있지만, 큰 틀은 벗어나지 않는다)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764318151574&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. On which OS are you planning to use the agent?
 =&amp;gt; linux 
2. Are you using EC2 or On-premises hosts?
 =&amp;gt; EC2
3. Which user are you planning to run the agent?
 =&amp;gt; root
4. Do you want to turn on StatsD daemon?
 =&amp;gt; no
5. Do you want to monitor metrics from CollectD?
 =&amp;gt; no
6. Do you want to monitor any host metrics? e.g. CPU, memory, etc.
 =&amp;gt; yes
7. Do you want to monitor cpu metrics per core?
 =&amp;gt; no
8. Do you want to add ec2 dimensions (ImageId, InstanceId, InstanceType, AutoScalingGroupName) into all of your metrics if the info is available?
 =&amp;gt; yes
9. Do you want to aggregate ec2 dimensions (InstanceId)?
 =&amp;gt; yes
10. Would you like to collect your metrics at high resolution (sub-minute resolution)? This enables sub-minute resolution for all metrics, but you can customize for specific metrics in the output json file.
 =&amp;gt; 60s 
11. Which default metrics config do you want?
 =&amp;gt; Basic (각 옵션마다 수집할 수 있는 지표가 다르며, Basic은 가장 적다.)

---json 파일로 된 configration---

12. Are you satisfied with the above config? Note: it can be manually customized after the wizard completes to add additional items.
 =&amp;gt; yes
13. Do you have any existing CloudWatch Log Agent(&amp;lt;http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html?) configuration file to import for migration?
 =&amp;gt; no
14. Do you want to monitor any log files?
 =&amp;gt; no 
15. Do you want to specify any additional log files to monitor?
 =&amp;gt; no
16. Do you want to store the config in the SSM parameter store?
 =&amp;gt; no&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;Program exits now&quot;라는 문장이 출력되면 설정이 완료됐다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;설정(config)을 변경하고 싶다면 /opt/aws/amazon-cloudwatch-agent/bin/config.json 파일을 수정하자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 에이전트를 실행하자. (설정 파일을 수정한 뒤에도 이 명령을 실행하면 된다.)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764318246938&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json -s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정이 잘 되었다면 5분 뒤에 &quot;CloudWatch &amp;gt; Metrics&quot;에서 사용자 지정 네임스페이스 하위에 있는 `CWAgent` 네임스페이스를 확인할 수 있다. 이 네임스페이스에서 메모리 및 디스크와 관련된 지표를 확인할 수 있다.&amp;nbsp;&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;900&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rshQx/dJMcaaRhGot/8JeNka2SbdMU4l9ABlhf41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rshQx/dJMcaaRhGot/8JeNka2SbdMU4l9ABlhf41/img.png&quot; data-alt=&quot;CWAgent 네임스페이스 &amp;amp;gt; InstanceId (dimension &amp;amp;gt; disk, mem 지표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rshQx/dJMcaaRhGot/8JeNka2SbdMU4l9ABlhf41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrshQx%2FdJMcaaRhGot%2F8JeNka2SbdMU4l9ABlhf41%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;600&quot; height=&quot;245&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CWAgent 네임스페이스 &amp;gt; InstanceId (dimension &amp;gt; disk, mem 지표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;추가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에이전트 상태 확인 명령어&lt;/span&gt;
&lt;pre id=&quot;code_1764318481896&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent.log&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에이전트 에러 관련 로그 위치&lt;/span&gt;
&lt;pre id=&quot;code_1764396349835&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;amazon-cloudwatch-agent-ctl -m ec2 -a status&lt;/code&gt;&lt;/pre&gt;
&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;커스텀 대시보드 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CWAgent 네임스페이스에서 메모리 및 디스크 관련 지표를 확인했다면 이번에는 해당 지표들을 하나의 위젯으로 묶어 대시보드에서 확인할 수 있도록 하자. 만들고자 하는 대시보드에는 두 개의 위젯을 담을 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 상태를 알 수 있는 위젯&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메모리, 디스크 및 CPU 사용률 현황을 알 수 있는 위젯&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 상태 확인을 위한 지표를 간단히 알아보자. 기본 제공되는 지표는 세 가지로 볼 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;StatusCheckFailed_Instance&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잘못 구성된 네트워킹 또는 시작구성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소진된 메모리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;손상된 파일 시스템&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;호환되지 않는 커널&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;StatusCheckFailed_System&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 연결 문제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시스템 전원 문제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;물리 호스트의 소프트웨어 문제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;물리 호스트의 하드웨어 문제&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;StatusCheckFailed&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 2개 CheckFailed의 OR 조건&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 지표들은 AWS/EC2 네임스페이스의 인스턴스별 지표 차원에서 확인할 수 있다. 해당 지표들을 선택하고 &quot;작업 &amp;gt; 대시보드에 추가&quot;를 클릭한다.&amp;nbsp;&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;936&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCN41j/dJMcacauo4R/GHkxkfz3j0O6Fj3XxjE0j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCN41j/dJMcacauo4R/GHkxkfz3j0O6Fj3XxjE0j0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCN41j/dJMcacauo4R/GHkxkfz3j0O6Fj3XxjE0j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCN41j%2FdJMcacauo4R%2FGHkxkfz3j0O6Fj3XxjE0j0%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;600&quot; height=&quot;399&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나타나는 화면에서 요구하는 정보들을 입력한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대시보드 선택: 별도로 생성해 둔 대시보드가 없다면&lt;/span&gt; &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;새로 생성&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 선택&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;새로 생성을 선택하면 '새 대시보드 생성'란이 나타난다. 생성할 대시보드 이름을 입력하고 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위젯 유형: 자유롭게 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위젯 제목 사용자 지정: 선택한 지표들은 하나의 위젯으로 대시보드에 속하게 된다. 이 위젯을 뭐라고 칭할지 정하면 된다. 지금 선택한 지표들은 EC2의 상태를 나타내므로 간단히 `EC2 status`라고 명명했다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;대시보드에 추가&lt;/b&gt;&lt;/span&gt;를 선택하여 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 &quot;대시보드 &amp;gt; 사용자 지정 대시보드&quot;로 이동하면 방금 생성한 대시보드 이름을 확인할 수 있다. 이름을 클릭하면 지표들을 묶은 위젯도 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위젯은 생성 이후에도 편집할 수 있으므로 상황에 따라 지표 추가 및 제거를 편리하게 할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 번째 위젯도 만들어 보자. 우선 `CWAgent` 네임스페이스에서 `mem_used_percent`, `disk_used_percent` 지표를 선택하여 생성한 대시보드에 추가하자. 그다음 생성한 위젯에서 편집을 클릭하여 `CPUUtilization` 지표를 선택하면 두 번째 위젯도 완성이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;요금&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 지정 네임스페이스 - 지표&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 지정 네임스페이스에 속한 지표는 개당 월별 0.30 USD가 청구된다. (전체 지표수가 1만 개를 초과하는 경우 볼륨 요금 티어가 적용된다.) Resolution을 '60초'로 설정하고, 지표 설정을 'Basic'으로 설정했을 때 생성되는 `CWAgent` 네임스페이스에서 23개의 지표를 수집할 수 있다. 따라서 23개의 지표에 대한 월별 비용은 23 metrics &amp;times; 0.30 USD = 6.90 USD가 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커스텀 대시보드&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 AWS에 의해 자동으로 생성되는 대시보드 외에 커스텀 대시보드를 사용하는 경우, 대시보드당 3.00 USD가 청구된다. 지금처럼 한 개의 커스텀 대시보드가 있는 경우에는 월별 비용이 1 dashboard &amp;times; 3.00 USD = 3.00 USD가 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;경보(Alarm)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 글에서 다루지는 않았지만 특정 지표에 대한 CloudWatch 알람을 설정하면, 알람이 트리거 되지 않아도 생성 즉시 비용이 청구된다. 해당 비용은 알람 당 월별 0.10 USD이며, 실제로 알람이 트리거 되어 SNS 알림이 발생하면 별도 비용이 청구된다.&amp;nbsp;&lt;/span&gt;&lt;/span&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;i&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;끝!&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;/span&gt;&lt;/i&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudWatch에 관하여 간단하게 알아보았다. 더 많은, 복잡한 기능들이 있지만, 우선 이 정도만 알아둬도 나머지는 좀 더 수월하게 알아갈 수 있다고 생각한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;참고로 요금에서 언급한 경보는 설정하는 방법이 복잡하거나 어렵지 않으므로 시도해 보면 좋을 것 같다. `CPUUtilization`이 70%를 초과하면 알림이 오도록 설정해 보는 건 어떨까. 알기로는 이메일뿐만 아니라 Slack으로도 알림을 받을 수 있다. 처음에는 이메일로 설정해 보고 Slack으로도 받아보도록 해보자. (이 부분은 추가 요금이 발생할 수도 있다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로, 언제나 요금 폭탄을 조심하자. 안 쓰는 대시보드나 알람은 바로바로 삭제하기!&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;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>agent</category>
      <category>AWS</category>
      <category>CloudWatch</category>
      <category>대시보드</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/202</guid>
      <comments>https://backend-jaamong.tistory.com/202#entry202comment</comments>
      <pubDate>Sat, 6 Dec 2025 10:03:52 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] CloudWatch 사용해보기 1: 용어 소개</title>
      <link>https://backend-jaamong.tistory.com/201</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Amazon CloudWatch는 AWS, 온프레미스 및 기타 클라우드에서 리소스 및 애플리케이션을 관측하고 모니터링할 수 있는 서비스이다. 이를 이용하여 성능 변화에 대응하고 리소스 사용을 최적화하고, 운영 상태에 대한 인사이트를 얻을 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudWatch는 로그(수집, 보관 등) 및 수집하는 커스텀 지표 등이 적을 때는 비용이 크게 청구되지 않지만, 잘 모르고 사용하면 예상하지 못한 금액을 보게 되는 서비스라고 생각한다. 비용에 관한 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자세한 내용은 &lt;a title=&quot;Amazon CloudWatch 요금&quot; href=&quot;https://aws.amazon.com/ko/cloudwatch/pricing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;를 참고하자.&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;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;나도 많이 아는 건 아니지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;알아두면 좋은 내용들을&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기록 및 공유를 위해 작성하려고 한다.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음 내용을 다룰 예정이며, 이 포스팅에서는 우선 용어 소개만 다루려고 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;알아두면 좋은 CloudWatch 용어 소개&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 커스텀 지표 및 수집 방법&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Amazon CloudWatch agent 설치 방법&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 커스텀 대시보드 생성 방법&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;지표 관련 용어&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudWatch의 모든 지표 메뉴로 가면 사용하고 있는 리소스에 관한 다양한 정보를 얻을 수 있다. 이에 관하여 CloudWatch에서 사용하는 단위는 네임스페이스(Namespace), 지표(Metric), 차원(Dimension) 등이 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;네임스페이스(Namespace)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네임스페이스는 특정 서비스 또는 리소스 그룹에 대한 지표들을 &lt;span style=&quot;text-align: start;&quot;&gt;묶어서 분류하는 단위로 EC2, S3, ELB 등이 해당된다. 각 리소스는 서로 다른 네임스페이스에 속하며, 각 네임 스페이스는 리소스의 상태를 나타내는 다양한 지표를 제공한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;AWS에서 기본적으로 제공되는 네임스페이스는 ` &lt;span style=&quot;text-align: start;&quot;&gt;AWS/service`로 명명된다. 예를 들어, EC2 네임스페이스는 `AWS/EC2`라는 이름을 갖는다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BTEJL/dJMcacVQ9Yh/I5d8rQdd09HK0OqKUmXMoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BTEJL/dJMcacVQ9Yh/I5d8rQdd09HK0OqKUmXMoK/img.png&quot; data-alt=&quot;AWS에서 기본적으로 제공하는 네임스페이스 일부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BTEJL/dJMcacVQ9Yh/I5d8rQdd09HK0OqKUmXMoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBTEJL%2FdJMcacVQ9Yh%2FI5d8rQdd09HK0OqKUmXMoK%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;600&quot; height=&quot;301&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AWS에서 기본적으로 제공하는 네임스페이스 일부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;지표(Metric)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;지표는 AWS 서비스가 보내는(publish) 시계열 데이터 포인트 집합으로 구성된다. 데이터 포인트(data points)는 특정 시점에서의 단일 지표 값 또는 관측값으로, 단순하게 말하자면&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;시간 순으로 정렬된&lt;/span&gt;&amp;nbsp;'시간-값'으로 표현되는 데이터라는 뜻이다. 아래 사진에서 볼 수 있듯이 AWS/EC2 네임스페이스에서 &lt;span style=&quot;text-align: start;&quot;&gt;CPU 사용률을 나타내는&amp;nbsp;&lt;/span&gt;`CPUUtilization` 지표를 선택하면 시간별 CPU 사용률(값)을 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 포인트에는 Resolution와 Period이라는 개념이 있다. &lt;b&gt;Resolution&lt;/b&gt;은 데이터가 얼마나 자주 수집되는지 의미한다. 주요 서비스에 대한 지표는 기본적으로 5분 또는 1분/60초 마다 수집되며(&lt;span style=&quot;text-align: start;&quot;&gt;Standard resolution&lt;/span&gt;) 무료로 제공된다. 1초 단위 이상으로 수집할 수 있는 High resolution 모드는 별도로 요금이 부과된다. (참고로 Resolution은 해상도로 번역하는 것이 의미상 가깝다고 한다.)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Period&lt;/b&gt;는 데이터 포인트가 얼마만큼의 시간을 기준으로 묶여서 보내지는지를 의미한다. 예를 들어, &quot;매 60초 간격으로 묶어 보겠다.&quot; 하면 '매 60초'가 period가 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;참고로 period가 짧을 수록 보관 기간(retention period)도 짧아진다. 작은 단위의 보관 기간은 큰 단위로 합쳐지므로, 데이터가 사라지는 것은 아니다.&amp;nbsp;&lt;br /&gt; &lt;a href=&quot;https://docs.aws.amazon.com/cli/latest/reference/cloudwatch/get-metric-data.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;관련 자료&lt;/a&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;777&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qt5Fz/dJMcagDX5Zj/pjTyiFkx13MlmvoZ3zRcw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qt5Fz/dJMcagDX5Zj/pjTyiFkx13MlmvoZ3zRcw1/img.png&quot; data-alt=&quot;AWS/EC2 네임스페이스의 지표들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qt5Fz/dJMcagDX5Zj/pjTyiFkx13MlmvoZ3zRcw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqt5Fz%2FdJMcagDX5Zj%2FpjTyiFkx13MlmvoZ3zRcw1%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;600&quot; height=&quot;246&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;777&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AWS/EC2 네임스페이스의 지표들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;우리가 위 사진을 통해 &quot;EC2의 CPU 사용률을 확인한다&quot;라고 했을 때 EC2가 네임스페이스가 되고, CPU 사용률은 지표가 된다.&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;차원(Dimension)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;차원은 지표를 더 세부적으로 분류할 수 있도록 하는 이름/값 쌍이다. 따라서 차원으로 각 지표를 식별할 수 있다. 아래 사진에서 `InstanceId`가 차원이다. 아래의 화면에서 특정 `InstanceId`에 마우스 커서를 대면 검색에 추가/제외할 수 있는 버튼이 생긴다(필터링). 이런 식으로 차원을 통해 지표를 식별할 수 있게 된다.&amp;nbsp;&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;873&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOWPGJ/dJMcaaX2I1v/vwDzaUVD9vq3f67DZbF150/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOWPGJ/dJMcaaX2I1v/vwDzaUVD9vq3f67DZbF150/img.png&quot; data-alt=&quot;AWS/EC2 차원 - InstanceId&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOWPGJ/dJMcaaX2I1v/vwDzaUVD9vq3f67DZbF150/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOWPGJ%2FdJMcaaX2I1v%2FvwDzaUVD9vq3f67DZbF150%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;600&quot; height=&quot;307&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AWS/EC2 차원 - InstanceId&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래처럼 형광색으로 표시된 항목들도 차원이다. `AWS/ApplicationELB` 네임스페이스 하위에 있는 지표들을 특정 기준에 따라 분류해 둔 것이다. &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;1888&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brCbqn/dJMcad1tWN9/aNiLd1knQsjdJKCElsHXXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brCbqn/dJMcad1tWN9/aNiLd1knQsjdJKCElsHXXK/img.png&quot; data-alt=&quot;AWS/ApplicationELB 차원&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brCbqn/dJMcad1tWN9/aNiLd1knQsjdJKCElsHXXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrCbqn%2FdJMcad1tWN9%2FaNiLd1knQsjdJKCElsHXXK%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;600&quot; height=&quot;126&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AWS/ApplicationELB 차원&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;용어 소개는 여기까지이다. 자세히 다루지는 않았지만, 이 정도만 알아도 CloudWatch를 처음 다룰 때 당황스럽지는 않다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음 글에서는 CloudWatch agent 설치와 커스텀 지표 수집 및 커스텀 대시보드 생성을 다룬다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>AWS</category>
      <category>CloudWatch</category>
      <category>dimension</category>
      <category>metrics</category>
      <category>namespace</category>
      <category>period</category>
      <category>resolution</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/201</guid>
      <comments>https://backend-jaamong.tistory.com/201#entry201comment</comments>
      <pubDate>Fri, 5 Dec 2025 22:00:36 +0900</pubDate>
    </item>
    <item>
      <title>[CI/CD] AWS + Docker + GitHub Actions Self-hosted runner</title>
      <link>https://backend-jaamong.tistory.com/200</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions Self-hosted runner 기능을 사용하여 AWS EC2 인스턴스에 배포되고 있는 FastAPI 애플리케이션에 대해 CI/CD를 적용하자. 또한 &lt;span style=&quot;text-align: start;&quot;&gt;환경 변수(.env) 적용 및 간단하고 편리한 빌드를 위해 Docker를 함께 사용할 것이다.&amp;nbsp;&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보통 GitHub Actions를 사용하여 구축하는 CI/CD의 빌드는 깃허브에서 진행하게 된다(GitHub-hosted runner). Self-hosted runner는 깃허브가 아닌 사용자가 지정하는 컴퓨팅 자원에서 빌드를 진행하도록 하는 시스템이다. 이 글에서는 EC2에서 빌드를 진행하도록 할 것이다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;이 과정에서는 다음의 기능들을 사용하여 CI/CD를 구축한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions Self-hosted runner&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;Docker&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dockerfile&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker-compose.yml: 환경 변수 적용 및 로깅 드라이버 설정을 위함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECR : Docker 이미지 저장 및 관리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM OIDC, Role&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OIDC: GitHub Actions에서 AWS ECR로 이미지 업로드를 할 때 필요함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Role: EC2가 ECR에 접근하여 이미지를 가져오기 위해 필요함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AMI는 Ubuntu 24.04 LTS으로 진행한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EIP&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도메인없이 외부에서 EC2(애플리케이션)로 접근하기 위해 IP 할당&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; Notice&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지금은 EC2 인스턴스가 퍼블릭 서브넷에 위치하므로 EIP를 사용하지만, 보안을 고려한다면 프라이빗 서브넷에 두어야 한다. 그리고 인터넷 통신을 위해 앞에 NAT Gateway나 ELB 같은 서비스를 사용해야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일단 이 글의 목적은 GitHub Actions Self-hosted runner &amp;amp; Docker &amp;amp; AWS를 사용하여 CI/CD를 구축하는 것이기 때문에 이 부분은 넘어간다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;과정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 CI/CD 방법보다 진행하는 과정이 어렵거나 복잡하지 않기 때문에 간단하게 작성한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 리소스부터 생성한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 보안 그룹 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;EC2 &amp;gt; 네트워크 및 보안 &amp;gt; 보안 그룹&quot;으로 이동 후 &lt;b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;보안 그룹 생성&lt;/span&gt;&lt;/b&gt; 클릭&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 세부 정보&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안 그룹 이름: 보통 프로젝트 명 + 'sg' 접미사를 합쳐서 작성 (예: todolist_be_sg, todolist_app_sg,...)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명: 무엇을 위한 보안 그룹인지 간단히 작성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC: 애플리케이션을 배포하고 있는 EC2가 위치한 VPC 선택&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인바운드 규칙 1&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형: SSH&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SSH를 통해 EC2 접속을 하기 위함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스 유형: Anywhere-IPv4&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SSH에 한하여 모든 곳에서 요청받음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명: 이 규칙이 어떤 용도인지 간단히 작성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인바운드 규칙 2&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형: HTTP (HTTPS 프로토콜은 이 글에서 다루지 않음)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스 유형: Anywhere-IPv4&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTP에 한하여 모든 곳에서 요청받음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;설명: 이 규칙이 어떤 용도인지 간단히 작성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아웃바운드 규칙은 건들지 않고 완료. &lt;b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;보안 그룹 생성&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;클릭&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. EC2 인스턴스 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;EC2 &amp;gt; 인스턴스 &amp;gt; 인스턴스&quot;로 이동 후 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;인스턴스 시작&lt;/b&gt;&lt;/span&gt; 클릭&amp;nbsp;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이름 및 태그: 보통 프로젝트 이름을 작성하고 여기에 파트(be, fe,...), 'app' 등을 덧붙임 (예: todolist-be-app)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션 및 OS 이미지(AMI): 이 글은 우분투(Ubuntu 24.04 LTS)로 진행한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스 유형: 인스턴스 내부에 runner와 docker를 설치 및 실행해야 하므로 `micro`보다 큰 `small`부터 선택하는 것을 추천한다. 단순 연습용이라면 `t3.small`이 좋다. (micro는 도커 돌리다가 CI/CD 하기도 전에 서버가 다운될 수도.. 경험담이다)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;키 페어(로그인): 사용하던 키 페어를 사용해도 좋고, 별도로 없다면 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;새&lt;/b&gt; &lt;b&gt;키 페어 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;키 페어 이름: 어떤 키 페어인지 식별할 수 있도록 작성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;키 페어 유형: RSA&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프라이빗 키 파일 형식: .pem&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 설정&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방화벽(보안 그룹): 기존 보안 그룹 선택&amp;nbsp;&amp;rarr; 1번에서 생성한 보안 그룹 선택&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(Optional) 고급 세부 정보: 연습용이라면 스팟 인스턴스를 추천.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구매 옵션: 스팟 인스턴스&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용 중 예상치 못하게 자원이 반납될 수도 있지만, 대신 저렴하게 인스턴스를 대여할 수 있다. 연습 목적으로 적절하다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 기본 상태로 두고 완료. &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;인스턴스 시작&lt;/b&gt;&lt;/span&gt; 클릭.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잠시 기다리면 인스턴스 상태가 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;실행 중&lt;/b&gt;&lt;/span&gt;으로 나타남&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. EIP 할당 및 연결&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;EC2 &amp;gt; 네트워크 및 보안 &amp;gt; 탄력적 IP&quot;로 이동 후 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;탄력적 IP 주소 할당&lt;/b&gt;&lt;/span&gt; 클릭.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나타난 화면에서 틀린 내용이 없다면 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;할당&lt;/b&gt;&lt;/span&gt; 클릭.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성 완료 후 할당한 EIP를 선택하고 &quot;작업 &amp;gt; 탄력적 IP 주소 연결&quot; 선택&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스: 2번에서 생성한 인스턴스 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프라이빗 IP 주소: 자동으로 선택된 IP 주소가 정확한지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 기본 상태로 두고 완료.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 EC2 인스턴스에 퍼블릭 IPv4 주소가 할당되었다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;참고로 24.02.01부터 사용 중인 EIP에도 시간당 IP당 0.005 USD가 부과되기 시작했다. 정확히는 퍼블릭 IPv4 주소에 대해 요금을 부과하는 것이다. 이는 IPv4 주소가 부족해지고 이로 인해 취득 비용이 증가하여, IPv6 선택을 장려하기 위함이라고 한다.&amp;nbsp;&lt;br /&gt; &lt;i&gt;&lt;a title=&quot;공지 &amp;ndash; AWS Public IPv4 주소 요금 변경 및 Public IP Insights 기능 출시&quot; href=&quot;https://aws.amazon.com/ko/blogs/korea/new-aws-public-ipv4-address-charge-public-ip-insights/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서 주소&lt;/a&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;4. AWS ECR 저장소 만들기&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS ECR(Elastic Container Registry)은 Docker 컨테이너 이미지를 저장 및 관리할 수 있는 서비스로, S3를 저장소로 사용한다. 대용량으로 저장되지 않는 이상 보관 비용은 월별 USD 0.10/GB가 청구된다. 대용량이라고 방치하면 안 되고, 생각보다 금방 용량이 차기 때문에 항상 잘 관리해 주는 것이 좋다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인터넷이나 동일 리전으로 수신하는 인바운드 데이터 전송에 대해서는 무료이며, 아웃바운드 데이터 전송에는 지역별로 GB 당 요금이 다르게 청구된다. 자세한 건 이 &lt;a title=&quot;AWS ECR 요금&quot; href=&quot;https://aws.amazon.com/ko/ecr/pricing/?nc1=h_ls&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;주소&lt;/a&gt;를 참고하자.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;데이터 전송 요금이 나오는 이유는 &lt;b&gt;ECR이나 S3가 VPC 밖에 위치&lt;/b&gt;하기 때문이다. EC2 인스턴스나 RDS 등의 리소스는 VPC&amp;gt;서브넷 내부에 위치하기 때문에 VPC 밖과 데이터를 주고받는 것에 대한 요금을 청구하는 것이다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;ECR &amp;gt; 리포지토리 생성&quot;으로 이동 후 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;리포지토리 생성&lt;/b&gt;&lt;/span&gt; 클릭.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리포지토리 이름: 다른 리소스를 생성할 때처럼 자유롭게 작성하되, 어떤 목적으로 생성하는지 잘 드러나게 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지 태그 설정이미지 버전에 상관없이 태그가 덮어 씌워져도 상관없다면 `Mutable`, 버전 관리가 중요하다면 `Immutable`을 선택하면 된다. `Mutable`을 선택하더라도 &quot;변경 가능한 태그 제외&quot; 란에서 필터를 설정하여 변경할 수 없는 태그를 추가할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;암호화 설정: 이 글에서는 `AES-256`을 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;생성&lt;/b&gt;&lt;/span&gt; 클릭.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;5. IAM OIDC 공급자 추가 및 IAM 역할 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;IAM &amp;gt; 액세스 관리 &amp;gt; ID 제공 업체&quot;로 이동 후, &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;공급자 추가&lt;/b&gt;&lt;/span&gt; 클릭&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공급자 세부 정보&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공급자 유형: OpenID Connect&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공급자 이름: https://token.actions.githubusercontent.com&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상: sts.amazonaws.com &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;공급자 추가&lt;/b&gt;&lt;/span&gt; 클릭하여 공급자 추가 완료&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;IAM &amp;gt; 액세스 관리 &amp;gt; 역할&quot;로 이동 후, &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;역할 생성&lt;/b&gt;&lt;/span&gt; 클릭&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1단계 - 신뢰할 수 있는 엔터티 선택(신뢰 관계, trust policy 설정)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;신뢰할 수 있는 엔터티 유형: 웹 자격 증명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ID 제공업체: token.actions.githubusercontent.com &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Audience: &lt;span style=&quot;text-align: start;&quot;&gt;sts.amazonaws.com&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub organization: 배포할 레포지토리가 위치한 조직명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Repository: 배포할 레포지토리 이름&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub branch: 배포할 브랜치 이름&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2단계 - 권한 추가&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아무것도 선택하지 않고 다음으로 이동&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3단계 - 이름 지정, 검토 및 생성&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할 이름: 해당 역할이 무엇을 하는지 드러나도록 작성. 보통 방향(AToB)을 드러내거나 어떤 서비스를 위한 것인지 (AForB) 작성하고 마지막에 'Role'을 붙이는 것 같다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명: 빈칸 말고, 무엇을 위한 역할인지 '역할 이름'을 풀어서 작성하면 된다. &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;역할 생성&lt;/b&gt;&lt;/span&gt; 클릭&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2번에서 생성한 역할을 클릭하고 &quot;권한 &amp;gt; 권한 정책&quot;으로 이동하여 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;권한 추가 &amp;gt; 인라인 정책 생성&lt;/span&gt;&lt;/b&gt;을 선택한다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JSON을 선택하고 정책 편집기에 아래와 같이 작성한다.&amp;nbsp;&lt;/span&gt;
&lt;pre id=&quot;code_1764218519679&quot; class=&quot;json&quot; data-ke-language=&quot;JSON&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Sid&quot;: &quot;GetAuthorizationToken&quot;,
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: [
                &quot;ecr:GetAuthorizationToken&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        },
        {
            &quot;Sid&quot;: &quot;ManageRepositoryContents&quot;,
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: [
                &quot;ecr:BatchCheckLayerAvailability&quot;,
                &quot;ecr:GetDownloadUrlForLayer&quot;,
                &quot;ecr:GetRepositoryPolicy&quot;,
                &quot;ecr:DescribeRepositories&quot;,
                &quot;ecr:ListImages&quot;,
                &quot;ecr:DescribeImages&quot;,
                &quot;ecr:BatchGetImage&quot;,
                &quot;ecr:InitiateLayerUpload&quot;,
                &quot;ecr:UploadLayerPart&quot;,
                &quot;ecr:CompleteLayerUpload&quot;,
                &quot;ecr:PutImage&quot;
            ],
            &quot;Resource&quot;: &quot;arn:aws:ecr:ap-northeast-2:{aws-12-digit-account-id}:repository/{ecr-repo-name}&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&lt;/span&gt;을 클릭하고, 나타나는 화면에서 정책이 어떤 일을 수행하는지 드러나도록 이름을 작성하고 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방금 생성한 정책은 4번에서 만든 ECR 저장소에 Docker 컨테이너 이미지를 업로드하고, 가져오는 것을 허용하는 내용이 담겨있다. 예를 들어, 이 정책을 갖고 있는 역할을 EC2에 연결하면 EC2가 해당 권한(정책에 작성된 Action)을 갖게 된다. 이 과정에서는&amp;nbsp; GitHub Actions(OIDC 공급자)가 해당 권한들을 갖도록 설정했기 때문에 이 역할을 다른 리소스에 연결하지 않는다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;신뢰할 수 있는 엔터티&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리는 5-1에서&amp;nbsp; &lt;span style=&quot;text-align: start;&quot;&gt;GitHub Actions가 맡은(assume role) IAM 역할의 '신뢰 관계(Trust Policy)'를 설정하여 이 &lt;/span&gt;OIDC 공급자를 신뢰할 수 있는 엔터티로 지정했다. 이를 통해 GitHub Actions 워크플로우 실행 시 GitHub Actions는 AWS Security Token Service(STS)로부터 임지 자격 증명을 발급받고, 이를 통해 AWS 내 필요한 리소스에 접근하여 작업을 수행할 수 있다.&amp;nbsp;&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;6. Self-hosted runner 및 GitHub Secrets, Variables 설정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 runner를 설치해야 하므로 EC2 인스턴스에 접속할 준비를 하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Self-hosted runner 설치&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;GitHub 레포지토리 &amp;gt; Settings &amp;gt; Actions &amp;gt; Runners&quot;로 이동 후 &lt;span style=&quot;background-color: #9feec3;&quot;&gt;&lt;b&gt;New self-hosted runner&lt;/b&gt;&lt;/span&gt; 클릭&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Runner image: Linux&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Architecture: x64&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Download` 스크립트를 EC2 인스턴스에 접속하여 수행한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`label`은 Optional이지만, 프로젝트 또는 서비스 명 등으로 작성하면 실행해야 하는 runner를 식별하기 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;`Download`&lt;/span&gt; 이후 `Configure`의 `./run.sh`은 지금 실행하지 않는다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 명령어로 runner를 실행하게 된다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 `actions-runner` 디렉토리 밖에서 다음 명령어를 차례대로 실행한다. 성공적으로 완료하면 runner가 실행되면서 build job을 감지하게 된다.&amp;nbsp;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`run.sh`를 `nohup`으로 실행하기 위한 스크립트를 생성하자. 여기서는 `vi`를 사용했는데 본인에게 맞는 편집기를 사용하면 된다.&amp;nbsp;&lt;/span&gt;
&lt;pre id=&quot;code_1764224528293&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vi run-bg.sh&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`vi` 창이 나타나면 `esc` &amp;rarr; `i` 키를 순서대로 입력하고, 입력창이 활성화되면 아래 내용을 작성한다.&amp;nbsp;&lt;/span&gt;
&lt;pre id=&quot;code_1764224553273&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

nohup ./actions-runner/run.sh &amp;amp; &amp;gt; nohup.out  # nohup.out 파일이 생성되고, 여기에 run.sh 실행 로그 쌓임&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작성 완료 후, `esc`&amp;nbsp; &lt;span style=&quot;text-align: start;&quot;&gt;&amp;rarr; `:wq`&amp;nbsp; &amp;rarr; 엔터를 순서대로 입력한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;터미널로 돌아오면 `./run-gb.sh` 명령어를 실행한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이때 권한이 없어서 실행되지 않는다는 에러가 뜨면, 아래 명령어로 실행 권한을 추가하고 다시 시도한다. &lt;/span&gt;
&lt;pre id=&quot;code_1764224576555&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ chmod u+x run-bg.sh&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 후 runner 상태를 확인하기 위해 다음으로 이동하자; &quot;GitHub 레포지토리 &amp;gt; Actions &amp;gt; Runner &amp;gt; Self-hosted runners&quot;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Secrets, Variables 설정&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;GitHub 레포지토리 &amp;gt; Settings &amp;gt; Secrets and variables &amp;gt; Actions&quot;로 이동한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Repository Secrets&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS_ROLE_ARN: 5번에서 생성한 IAM Role의 ARN 값 (IAM 역할 정보에서 확인할 수 있다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Repository Variables&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS_ACCOUNT_ID: 12자리 AWS 계정 ID&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS_REGION: ap-northeast-2&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECR_REPOSITORY: 4번에서 생성한 ECR 레포지토리 이름&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;7. Dockerfile, docker-compose.yml 및 워크플로우 스크립트 작성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 다음 파일들은 프로젝트 루트에 생성한다.&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기에서는 FastAPI 애플리케이션 버전으로 작성한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 파일의 내용은 본인의 상황에 맞춰 수정해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Dockerfile&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764225294692&quot; class=&quot;dockerfile, docker&quot; data-ke-language=&quot;Dockerfile&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM python:3.11-slim

WORKDIR /app

# 의존성 파일 복사 및 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 애플리케이션 코드 복사
COPY . .

# uvicorn 실행
CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;docker-compose.yml&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지금 단계에서는 로깅 드라이버 설정을 포함하지 않는다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764225397920&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;YAML&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  fastapi-app:
    image: &quot;${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}&quot;
    container_name: fastapi-app
    ports:
      - &quot;80:8000&quot;
    restart: unless-stopped
    env_file:
      - .env&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;.github/workflows/deploy.yml&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음 스크립트에서 `label`(runs-on)에는 `fastapi-app`가 추가되어 있다.&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Notice&lt;/b&gt;&lt;/span&gt; 아래 내용은 사용하는 언어, 프레임워크, 도커 컨테이너 및 이름, 깃허브 변수 등에 따라 달라지므로 복붙하고 반드시 검토해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764226006373&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;YAML&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Build and Deploy FastAPI to ECR

on:
  push:
    branches:
      - release
  workflow_dispatch:

permissions:
  id-token: write # Required for OIDC
  contents: read

jobs:
  build-and-push:
    runs-on: [self-hosted, fastapi-app]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials using OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ vars.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Generate timestamp
        id: timestamp
        run: echo &quot;timestamp=$(date +%Y%m%d-%H%M%S)&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT

      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
          IMAGE_TAG: ${{ steps.timestamp.outputs.timestamp }}-${{ github.sha }}
        run: |
          docker build -t fastapi-app:$IMAGE_TAG .
          docker tag fastapi-app:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo &quot;Pushed image: $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG&quot;

      - name: Run new container using docker-compose
        env:
          # 여기에 .env 변수에 넣어야 하는 값(github secrets, variables)를 작성하면 된다. (예: DB 변수)
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
          IMAGE_TAG: ${{ steps.timestamp.outputs.timestamp }}-${{ github.sha }}
          DB_HOST: ${{ secrets.DB_HOST }} # 예시
          
        run: |
          # ensure env file exists
          # 여기에서 .env 파일에 github secrets, variables 값들을 넣는다. 
          echo &quot;ECR_REGISTRY=${ECR_REGISTRY}&quot; &amp;gt;&amp;gt; .env
          echo &quot;ECR_REPOSITORY=${ECR_REPOSITORY}&quot; &amp;gt;&amp;gt; .env
          echo &quot;IMAGE_TAG=${IMAGE_TAG}&quot; &amp;gt;&amp;gt; .env
          echo &quot;DB_HOST=${DB_HOST}&quot; &amp;gt;&amp;gt; .env
  
          docker compose down
          docker compose pull
          docker compose up -d

      - name: Verify container is running
        run: |
          sleep 3
          if docker ps | grep -q fastapi-app; then
            echo &quot;Container is running successfully&quot;
            docker ps --filter name=fastapi-app
          else
            echo &quot;Container failed to start&quot;
            docker logs fastapi-app
            exit 1
          fi

      - name: Prune reclaimable Docker images and builder caches
        run: |
          sleep 10
          docker image prune -af 
          echo &quot;Cleaned up reclaimable images...&quot;
          docker builder prune -af
          echo &quot;Cleaned up reclaimable builder caches...&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;워크플로우의 마지막 단계는 사용하지 않는 이미지 및 빌드 캐시를 일괄 삭제한다. 진행하는 프로젝트의 도커 이미지 용량이 꽤나 커서 해당 단계를 넣었다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;8. GitHub 레포지토리에 Push&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Commit &amp;amp; Push를 진행하고 워크플로우가 잘 동작하는지 확인하기 위해 다음으로 이동하자.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;GitHub Repository &amp;gt; Actions&quot;로 이동&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;빌드가 성공적으로 완료되었다면 초록색 체크 표시를 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;+ Docker 로깅 드라이버 설정: AWS CloudWatch로 포워딩&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스에서 실행되는 Docker 컨테이너의 로그는 보통 /var/lib/docker/containers 하위에 생성되고 쌓이게 된다. 로그는 생각보다 빠르게 용량을 잡아먹어서 관리하지 않으면 용량 부족으로 서버가 뻗어버릴 수 있다. 따라서 쌓인 로그를 주기적으로 관리해야 하며, 이 관리를 자동화하는 것을 추천한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방법에는 로그 로테이션 설정, 자동화 스크립트 w/cron, 로그 드라이버 변경 등이 있다. 여기에서는 로그 드라이버를 변경하여 AWS CloudWatch로 로그를 전송하도록 한다. 이 방법을 사용하면 EC2 인스턴스에 애플리케이션을 실행하는 Docker 컨테이너의 로그가 더 이상 쌓이지 않으므로, 로그로 인한 용량 문제는 걱정하지 않아도 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 방법은 이전에 소개한 적이 있으므로 이&lt;/span&gt; &lt;a title=&quot;[AWS &amp;amp; Docker] Docker 컨테이너 로그를 AWS CloudWatch로 보내기&quot; href=&quot;https://backend-jaamong.tistory.com/185&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;글&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 참고하면 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로 로그를 CloudWatch로 전송할 때 GB 당 0.76 USD 비용이 청구된다. 이는 기본 로그 클래스(Standard log class)에서 커스텀 로그를 수집하는데 드는 비용이다. 또한 로그 데이터 양과 저장 기간에 대한 비용도 청구된다. GB 당 월별 0.0314 USD가 청구된다. 겉보기엔 크게 비용이 들진 않지만, 쌓이다 보면 비용이 급증하므로 로그 보존 기간을 적절히 설정하는 것이 좋다.&amp;nbsp;&lt;/span&gt;&lt;/span&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;i&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;끝!&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI/CD 파이프라인 구축에 성공했다! 이제&amp;nbsp;EC2 인스턴스를 퍼블릭 서브넷에서 프라이빗 서브넷으로 옮기는 것을 시도하면 어떨까. 앞서 언급한 것처럼 프라이빗 서브넷으로 옮기면 외부와 소통하기 위한 추가적인 리소스가 필요하다. NAT Gateway나 ELB와 같은 서비스를 사용하면 되는데 이 둘은 생각보다 요금이 비싸다. 어떻게 하면 비용을 줄일 수 있을지 고민해 보는 것도 좋을 것 같다. (사람들은 어떻게든 길을 찾아낸다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 CI/CD 파이프라인을 구성하는 리소스들의 비용은 다음처럼 예상할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions: 무료&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS ECR: 월별 USD 0.10/GB (공식 문서 참고)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS EIP: 시간당 USD 0.005&amp;nbsp;&amp;rarr; 3.65$/mo (USD 0.005 &amp;times; 730 hr)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS EC2: t3.small을 선택했다면 0.025 USD/hr (온디맨드), 0.00925 USD/hr (스팟) &amp;rarr; 18.25$/mo (USD 0.025 &amp;times; 730 hr) , 6.7525$/mo (USD 0.00925 &amp;times; 730 hr)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EBS: 볼륨 요금도 잊지 말자. gp3 스토리지를 사용한다면 월별 GB당 USD 0.0912가 청구된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(Optional) AWS CloudWatch&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;물론 AWS는 다양한 방면으로 돈을 가져가기 때문에 더 청구될 가능성이 높다. 아니면 생각보다 덜 나갈 수도 있다! 비용이 너무 많이 청구되었다면 &quot;결제 및 비용 관리&quot;의 &lt;b&gt;Cost Explorer&lt;/b&gt;와 &lt;b&gt;청구서&lt;/b&gt;를 확인해 보자. 비용이 청구된 항목을 자세하게 분석할 수 있다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 보통 더 나가게 된다면 다음 항목일 가능성이 높으므로 확인해 보는 것을 추천하고 싶다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; VPC 관련 설정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 이동/처리 비용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudWatch 로그 전송 및 보관 비용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool</category>
      <category>AWS</category>
      <category>ci/cd</category>
      <category>Docker</category>
      <category>GitHub Actions Self-hosted runner</category>
      <category>oidc</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/200</guid>
      <comments>https://backend-jaamong.tistory.com/200#entry200comment</comments>
      <pubDate>Fri, 28 Nov 2025 10:34:52 +0900</pubDate>
    </item>
    <item>
      <title>CI/CD에 블루-그린 무중단 배포 적용하기: AWS ELB, AWS Auto Scaling Groups(ASG)</title>
      <link>https://backend-jaamong.tistory.com/199</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;꽤... 늦었는데, 지난번에 구축한 CI/CD에 블루-그린 무중단 배포를 적용해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 지난 글:&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/197&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.07.28 - [Architecture &amp;amp; Tool/AWS] - CI/CD 구축하기: GitHub Actions (OIDC) + Amazon S3 + Amazon CodeDeploy&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;보안 그룹&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ALB 보안 그룹 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALB를 생성하기 전에 보안 그룹을 만들자. &lt;b&gt;EC2 &amp;gt; 보안 그룹&lt;/b&gt;으로 이동하여 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;보안 그룹 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아웃바운드 규칙은 건들지 않고 인바운드 규칙만 편집한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형: HTTP&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스 유형: 사용자 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스: 0.0.0.0/0&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EC2 보안 그룹 수정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 애플리케이션에 대한 요청을 ALB를 통해서만 받을 수 있도록 이전 포스트에서 생성했던 EC2 보안그룹을 수정해야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이전 포스트에서 두 개의 인바운드 규칙을 추가했었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부에서 EC2 내부로 접속하기 위한 SSH 22번 포트에 관한 규칙&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부에서 EC2 내부에서 실행되고 있는 애플리케이션에게 요청을 보내기 위한 사용자 지정 TCP 8080(애플리케이션 별 다름)번 포트에 관한 규칙&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 애플리케이션 실행 포트 번호에 대해서 수정해야 한다. 아래와 같이 수정하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형: 사용자 지정 TCP&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;포트 범위: 8080 (애플리케이션이 실행되는 포트 번호)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스 유형: 사용자 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스: 앞서 생성한 ALB 보안 그룹을 선택 (ALB를 통해서만 요청을 받을 수 있게 됨)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SSH 관련 인바운드 규칙은 유지한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;대상 그룹&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안 그룹은 말 그대로 보안을 위한, 리소스를 둘러싼 벽(방화벽, firewall)같은 거라 ALB가 받은 요청이 마법처럼 EC2에게 전달될 수 없다. 따라서 ALB와 EC2 인스턴스를 이어주는 다리가 필요하다. 이 다리 역할을 대상 그룹(Target Group)이 수행한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;대상 그룹 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EC2 &amp;gt; 대상 그룹&lt;/b&gt;으로 이동하여 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;대상 그룹 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1단계: 그룹 세부 정보 지정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 구성: 인스턴스 ; ALB가 받은 트래픽을 인스턴스 단위로 전달(분산)할 것이므로&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상 그룹 이름: 자유롭게 입력(어떤 서비스를 위한 대상 그룹인지, 그리고 대상 그룹임을 명시하는 이름이 좋은 것 같다)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로토콜: HTTP&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;포트: 8080 ; 앞서 수정한 EC2의 보안그룹의 포트와 동일해야 한다. 이 포트를 통해 ALB가 받은 트래픽이 라우팅 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC: 트래픽을 받을 EC2 인스턴스가 존재하는 VPC를 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상태 검사: 애플리케이션에 헬스 체크 API가 별도로 있다면, 상태 검사 경로에 해당 API의 엔드포인트를 입력한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;언급하지 않은 나머지 항목들은 기본값을 유지하고, &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&lt;/span&gt;으로 넘어간다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2단계: 대상 등록&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용 가능한 인스턴스: 트래픽을 받을 인스턴스 선택 (앞서 보안그룹을 수정한 EC2를 선택하면 된다)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;선택한 인스턴스를 위한 포트: 8080&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;아래에 보류 중인 것으로 포함&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 클릭&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px; color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;대상 그룹 생성&lt;/b&gt;&lt;/span&gt; 클릭&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Application Load Balancer&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스 앞에서 트래픽을 받아줄 로드밸런서를 생성할 차례다. &lt;b&gt;EC2 &amp;gt; 로드 밸런서&lt;/b&gt;로 이동하고, &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;로드 밸런서 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다. 로드 밸런서 유형은 &lt;b&gt;Application Load Balancer&lt;/b&gt;를 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런서 이름: 자유롭게 입력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;체계(schema): 인터넷 경계(internet-facing)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC: 대상 그룹과 EC2 인스턴스가 놓인 VPC와 동일해야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가용 영역 및 서브넷: 2개 이상 선택해야 한다. EC2 인스턴스와 다른 서브넷에 위치해도 상관없다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 이런 경우에서의 ALB 사용 사례를 잘 생각해보자.&amp;nbsp; 보통 EC2 인스턴스를 프라이빗 서브넷에 놓는데(보안), 트래픽을 앞에서 먼저 받아 전달해 주는 ALB는 퍼블릭 서브넷에 놓는 편 &amp;rarr; 둘은 다른 서브넷에 위치해도 OK.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안 그룹: 처음에 ALB를 위해 생성했던 보안그룹을 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리스너 및 라우팅(다음 프로토콜 및 포트로 트래픽을 받아 대상 그룹으로 트래픽을 전달(라우팅))&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로토콜 - 포트: HTTP - 80&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상 그룹: 앞서 만들었던 대상 그룹을 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;언급하지 않은건 기본값으로 남겨둔다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 요약을 잘 확인해 보고 이상이 없다면 생성을 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;AMI&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;AMI(&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Amazon Machin Image)&lt;/span&gt;는 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;인스턴스를 생성할 때 사용되는 이미지로, OS처럼 생각하면 될 것 같다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;AMI가 있으면 다른 인스턴스를 생성해도 동일한 OS 및 환경을 구성할 수 있다. 우리는 이미 생성된 인스턴스가 있으므로, 그 인스턴스를 이용해 AMI를 생성할 것이다.&amp;nbsp;&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #666666; font-size: 13px;&quot;&gt;오토 스케일링은 인스턴스를 트래픽에 따라 자동으로 생성 및 삭제를 수행하는 기능으로, &lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;이 기능을 사용하려면 시작 템플릿(Launch Template)이 필요하고, 이를 생성하려면 AMI가 필요하다. &lt;/span&gt;우리가 새로운 인스턴스 만들 때 AMI를 선택하는 것처럼 자동으로 인스턴스 생성 시 사용될 AMI가 있어야 한다.&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EC2 &amp;gt; 인스턴스&lt;/b&gt;로 이동하여 오토 스케일링을 적용할 인스턴스를 선택하고, &lt;b&gt;작업 &amp;gt; 이미지 및 템플릿 &amp;gt; 이미지 생성&lt;/b&gt;을 클릭한다.&amp;nbsp;&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;1571&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mOqVL/btsPLIbzwgx/1a5x3Ukv9oENXs45mi54fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mOqVL/btsPLIbzwgx/1a5x3Ukv9oENXs45mi54fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mOqVL/btsPLIbzwgx/1a5x3Ukv9oENXs45mi54fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmOqVL%2FbtsPLIbzwgx%2F1a5x3Ukv9oENXs45mi54fk%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;700&quot; height=&quot;152&quot; data-origin-width=&quot;1571&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지 이름: 자유롭게 입력하나, 어떤 이미지인지 잘 식별되도록 하자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미지 설명: 넘기지 말고, 어떤 목적으로 생성했는지 작성하자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 기본값으로 두고 이미지 생성을 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;바로 생성이 완료되지는 않고, 몇 분 기다리다보면 &lt;span style=&quot;background-color: #9feec3;&quot;&gt;&lt;b&gt;사용 가능&lt;/b&gt;&lt;/span&gt;이라고 상태가 뜬다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;시작 템플릿&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리는 인스턴스를 생성할 때 AMI 선택 외에도 인스턴스에 관한 스펙(인스턴스 유형, 네트워크 설정 등)을 설정한다. 오토 스케일링으로 만들어질 인스턴스의 스펙은 시작 템플릿(Launch Template)을 통해 설정된다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EC2 &amp;gt; 시작 템플릿&lt;/b&gt;으로 이동하여 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;시작 템플릿 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시작 템플릿 이름: 자유롭게 입력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;템플릿 버전 설명: 보통 애플리케이션 이름과 함께 템플릿(Template)을 같이 적어주는 것 같다&lt;/span&gt; (예시: HelloWorld Web Server Template)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션 및 OS 이미지: &lt;b&gt;내 AMI &amp;gt; 내 소유 &amp;gt; 앞서 생성한 AMI&lt;/b&gt; 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스 유형: 애플리케이션 스펙에 맞춰서 선택하기. 연습용이거나 작은 프로젝트라면 `t3.micro`나 `t3.small`로 충분하다(docker로 서버를 띄운다면 small 권장)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;키 페어 이름: 시작 템플릿에 포함하지 않음&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서브넷/가용 영역: 시작 템플릿에 포함하지 않음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방화벽(보안 그룹): 오토 스케일링을 적용할(현재 CI/CD 되고 있는) EC2 인스턴스와 동일한 보안 그룹을 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;고급 세부 정보 &amp;gt; 사용자 데이터: 여기에 Java17 및 CodeDeploy 에이전트 설치와 애플리케이션을 실행할 디렉토리를 생성하는 명령어를 작성한다. 이 과정에서 인스턴스의 AMI는 Ubuntu라서 이 OS에 맞춰서 명령어를 사용했다. (AMI에서 하는 것이 더 적절해 보이는데 일단 여기에서)&lt;/span&gt;
&lt;pre id=&quot;code_1754742672548&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash
sudo apt update -y
sudo apt install -y openjdk-17-jdk

sudo apt install ruby-full
sudo apt install wget 
cd /home/ubuntu
wget https://bucket-name.s3.region-identifier.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto

mkdir -p /home/ec2-user/hello&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;언급하지 않은 나머지는 기본값으로 두고 생성을 완료한다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;오토 스케일링 그룹(ASG)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS ASG는 &lt;span style=&quot;background-color: #fcfcfd; color: #000000; text-align: start;&quot;&gt;인스턴스를 그룹 단위로 관리하며, &lt;/span&gt;트래픽 변화에 따라 스케일링 정책을 기반으로 자동으로 인스턴스 수를 증가시키거나 감소시킬 수 있다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EC2 &amp;gt; Auto Scaling 그룹&lt;/b&gt;으로 이동, &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Auto Scaling 그룹 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1단계: 시작 템플릿 선택&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이름: 자유롭게 입력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시작 템플릿: 앞서 생성한 템플리 선택&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&lt;/span&gt; 클릭&lt;/span&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2단계: 인스턴스 시작 옵션 선택&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC: EC2 인스턴스와 동일한 VPC 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가용 영역 및 서브넷: 자유롭게 선택. 모두 선택해도 상관없다. 단순하게만 보자면 많이 선택할 수록 가용성이 높아진다고 생각하면 될 것 같다. 최소한의 가용성을 고려했을 때 적어도 두 개는 선택하는 게 좋다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가용 영역 배포: 균형&amp;nbsp;잡힌&amp;nbsp;최선&amp;nbsp;노력&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;이후&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;클릭&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3단계: &lt;span style=&quot;background-color: #fcfcfd; text-align: start;&quot;&gt;다른 서비스와 통합&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런싱: 기존 로드 밸런서에 연결&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 로드 밸런서에 연결: 로드 밸런서 대상 그룹에서 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 로드밸런서 대상 그룹: 앞서 만든 대상 그룹 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상태 확인: &lt;b&gt;Elastic Load Balancer 상태 확인 켜기&lt;/b&gt; 활성화&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런서가 인스턴스의 상태를 확인하고, 문제가 있는 인스턴스가 있으면 오토 스케일링이 이를 확인하고 인스턴스를 교체함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;4단계:&amp;nbsp;&lt;span style=&quot;background-color: #fcfcfd; text-align: start;&quot;&gt;그룹 크기 및 크기 조정 구성&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원하는 용량(Desired capacity): 1 (상황에 맞춰서 진행, 여기서는 간단히 기본값으로 진행)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성 시에 만들어지는 인스턴스 개수로, ASG는 이 수를 유지하려고 한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1로 설정한 경우를 예시로 보자. 실행되고 있는 한 개의 인스턴스가 예상치 못하게 종료되면, &lt;span style=&quot;text-align: left;&quot;&gt;원하는 용량값인 1을 유지하기 위해&lt;/span&gt; ASG가 한 개를 새롭게 생성한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;*&lt;/b&gt;원하는 최소 용량 &amp;le; 원하는 용량 &amp;le; 원하는 최대 용량&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원하는 최소 용량: 1 &lt;span style=&quot;text-align: left;&quot;&gt;(상황에 맞춰서 진행)&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케일링 정책이 설정된 경우, 그룹의 원하는 용량을 이 값보다 더 작게 감소시키지 못함&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원하는 최대 용량: 2 &lt;span style=&quot;text-align: left;&quot;&gt;(상황에 맞춰서 진행)&lt;/span&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케일링 정책이 설정된 경우, 그룹의 원하는 용량을 이 값보다 더 크게 증가시키지 못함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Automatic scaling: 크기 조정 정책 없음 &lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 기능이 실제로 &quot;오토 스케일링&quot;이라고 부르는 부분이다. 지금은 정책 없음을 선택해서&lt;span style=&quot;text-align: left;&quot;&gt; ASG가 트래픽 변화에 따라 자동으로 스케일링 할 수 없다. 우&lt;span style=&quot;text-align: left;&quot;&gt;리가 수동으로 원하는 용량을 변경하여 그룹 크기를 변경해야 한다. 따라서 `원하는 최소 용량`으로만 유지된다.&amp;nbsp;&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;지금은 인스턴스 개수가 유지만 되도록 정책 없음을 선택했다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스 유지 관리 정책: 정책 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 손대지 않고 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&lt;/span&gt;으로 넘어간다.&amp;nbsp;&lt;/span&gt;&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;5단계: &lt;span style=&quot;background-color: #fcfcfd;&quot;&gt;알림 추가&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfd;&quot;&gt;이 기능을 사용하려면 AWS의 다른 기능도 설정해야 해서 이 글에서는 넘어간다.&lt;/span&gt;&lt;/span&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;6단계:&amp;nbsp;&lt;span style=&quot;background-color: #fcfcfd;&quot;&gt;태그 추가&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfd;&quot;&gt;이 단계도 그냥 넘어가도 된다. 태그를 설정하면 새로 생성되는 인스턴스의 이름을 지정할 수 있다. 보통 키는 `Name`을 유지하고 값에 애플리케이션/프로젝트 이름을 넣는 것 같다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;7단계: &lt;span style=&quot;background-color: #fcfcfd;&quot;&gt;검토&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfd;&quot;&gt;설정값들을 마지막으로 확인하고 생성을 완료한다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfd;&quot;&gt;ASG 생성이 완료되면 원하는 용량을 유지하기 위해 새 인스턴스를 생성한다. &amp;rarr; 지금까지의 과정을 통해 생성된 인스턴스는 총 2개(이전 글에서 처음에 1개만 생성했다면)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CodeDeploy&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CodeDeploy도 수정해야 한다. 우선 CodeDeploy의 IAM 역할부터 수정한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM 역할 - 권한 추가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM &amp;gt; 역할&lt;/b&gt;로 이동해서 전에 CodeDeploy를 위해 생성한 IAM 역할을 클릭한다. 이전 글에서는&amp;nbsp; `CodeDepoyServiceRoleForEC2`라는 이름으로 생성했다. 이동한 화면에서&amp;nbsp;&lt;b&gt;권한 &amp;gt; 권한 추가 &amp;gt; 인라인 정책 생성&lt;/b&gt;을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3TDy8/btsPLKf5EO1/n5MQw2W9O9D8jy0PfNuf4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3TDy8/btsPLKf5EO1/n5MQw2W9O9D8jy0PfNuf4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3TDy8/btsPLKf5EO1/n5MQw2W9O9D8jy0PfNuf4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3TDy8%2FbtsPLKf5EO1%2Fn5MQw2W9O9D8jy0PfNuf4k%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;700&quot; height=&quot;283&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;752&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;span style=&quot;color: #000000;&quot;&gt;정책 편집기에서 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;JSON&lt;/b&gt;&lt;/span&gt;을 선택하고 아래와 같이 작성한다. (시작 템플릿으로 ASG을 생성한 경우, 다음 권한들이 필요함)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754716230191&quot; class=&quot;JSON&quot; data-ke-language=&quot;json, jsonc, json5&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
	&quot;Version&quot;: &quot;2012-10-17&quot;,
	&quot;Statement&quot;: [
		{
			&quot;Sid&quot;: &quot;VisualEditor0&quot;,
			&quot;Effect&quot;: &quot;Allow&quot;,
			&quot;Action&quot;: [
				&quot;iam:PassRole&quot;,
				&quot;ec2:CreateTags&quot;,
				&quot;ec2:RunInstances&quot;
			],
			&quot;Resource&quot;: &quot;*&quot;
		}
	]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작성 후 다음으로 넘어가고, 권한 추가를 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CodeDeploy 배포 그룹 편집&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CodeDeploy &amp;gt; 애플리케이션 &amp;gt; 앞서 생성한 애플리케이션(SpringBootApp) &amp;gt; 앞서 생성한 배포 그룹(production)까지&lt;/b&gt; 이동하&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자. 이동한 화면에서 &lt;b&gt;편집&lt;/b&gt;을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbve4L/btsPL0JUG45/GuQQFzGAmCsDUOqvFXzYC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbve4L/btsPL0JUG45/GuQQFzGAmCsDUOqvFXzYC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbve4L/btsPL0JUG45/GuQQFzGAmCsDUOqvFXzYC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbve4L%2FbtsPL0JUG45%2FGuQQFzGAmCsDUOqvFXzYC1%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;700&quot; height=&quot;240&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;309&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;span style=&quot;color: #000000;&quot;&gt;이동하면 이전에 배포 그룹을 생성했을 때 보았던 화면이 나온다. &lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 유형: 블루/그린 (현재 위치&amp;nbsp;&amp;rarr; 블루/그린)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;환경 구성: Amazon EC2 Auto Scaling 그룹 자동 복사&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ASG 선택: 앞서 생성한 ASG 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 설정 - 새 배포가 생성되면 바로 트래픽을 라우팅 하고, 기존 인스턴스는 15분 후 종료되게 설정한다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;트래픽 재 라우팅: 즉시 트래픽 라우팅&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 성공 후 원본 환경 인스턴스 종료 여부 및 종료 전 대기 시간 선택: 배포 그룹의 원본 인스턴스 종료&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일: 0&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시간: 0&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;분: 15&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 구성: CodeDeployDefault.AllAtOnce&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런서 유형: &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;b&gt;Application Load Balancer 또는 Network Load Balancer&lt;/b&gt; 활성화&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;대상 그룹 선택: 앞서 생성한 대상 그룹 선택&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;변경 사항 저장&lt;/b&gt;&lt;/span&gt;을 클릭하여 편집을 완료한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;이제 CodeDeploy를 사용하여 ASG의 EC2 인스턴스에 배포할 수 있다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;블루/그린 무중단 배포 과정 정리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;새로운 버전을 배포하기 전 상태&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스 총 개수: 직접 생성한 EC2 인스턴스 1개(A라고 지칭) + ASG로 생성된 EC2 인스턴스 1개(B라고 지칭)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 둘을 Blue 그룹이라고 하자.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 과정(단순하게 표현함, 실제로는 더 많은 단계가 있음)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드 변경 사항이 발생하여 GitHub 레포지토리에 commit/push&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions에서 감지(push trigger)&amp;nbsp;&amp;rarr; 배포 스크립트(.github/workflows/deploy.yml) 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions에서 CI를 진행하고 CodeDeploy를 통해 CD 진행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CodeDeploy를 통해 다음의 과정이 일어난다.&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Green 그룹 생성: 2개의 인스턴스를 새롭게 생성&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 ASG 그룹이 하나 더 늘어나고, 인스턴스 목록도 보면 2개에서 3개로 늘어나있는 것을 볼 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Green 그룹에 새로운 버전의 배포를 진행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Green 그룹으로 트래픽 라우팅&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포가 성공하고 15분 후에 Blue 그룹 제거&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;새로 생겼던 ASG 그룹이 유지되고, 기존 그룹은 제거된다. (그룹 수 다시 1개)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ASG로 인해 생겼던 기존 인스턴스도 제거되고, 새로운 인스턴스가 유지된다. (인스턴스 수 다시 2개: 수동 생성했던 인스턴스 1 + 지금 새로 생긴 인스턴스 1)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 완료&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;후기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어느 리소스를 생성하든, 그 리소스가 명시되도록 이름을 잘 짓는게 중요하다. 어떤 목적으로 리소스를 생성했는지 설명을 잘 작성하는 것도 무척 중요하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-service-role.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-service-role.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-enable-disable-scaling-policy.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-enable-disable-scaling-policy.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/autoscaling/ec2/userguide/asg-capacity-limits.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/autoscaling/ec2/userguide/asg-capacity-limits.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@pmthk__/Github-Actions-%EA%B3%BC-AWS-CodeDeploy%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-BlueGreen-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94#codedeploy-%EC%83%9D%EC%84%B1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@pmthk__/Github-Actions-%EA%B3%BC-AWS-CodeDeploy%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-BlueGreen-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94#codedeploy-%EC%83%9D%EC%84%B1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://happy-jjang-a.tistory.com/94&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://happy-jjang-a.tistory.com/94&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>auto scaling group</category>
      <category>AWS ELB</category>
      <category>Blue-Green Deployment</category>
      <category>ci/cd</category>
      <category>CodeDeploy</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/199</guid>
      <comments>https://backend-jaamong.tistory.com/199#entry199comment</comments>
      <pubDate>Sat, 9 Aug 2025 21:44:18 +0900</pubDate>
    </item>
    <item>
      <title>[TIL / AWS] SpringBoot 서버리스로 배포하기: ECR + Fargate(ECS) + ALB + Private Link</title>
      <link>https://backend-jaamong.tistory.com/198</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 공유도 있지만, 기록이 더 큰 목적...&amp;nbsp; Windows 11, Gradle/SpringBoot.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS에서 &quot;아.. &lt;span style=&quot;text-align: start;&quot;&gt;Fargate 좋은데... 안쓰나... 진짜 좋은데...&quot; 라고 계속 홍보하는 것 같아서 이번 프로젝트에서는 도대체 얼마나 좋길래? 하고 사용해봤다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;후기! &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;EC2 보다 비용이 좀(꽤) 나가지만, 서버 설정이 편리하다. &lt;/span&gt;서버 설정 자체는 신경을 안쓰게 되긴하는데 인프라는 건들게 너무... 많다...&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자세히는, 퍼블릭 서브넷에 배포한다면 정말 금방 끝낼 수 있다. 하지만 프라이빗 서브넷에 배포한다면 많은 설정들이 기다리고 있다. (이때 &lt;span style=&quot;text-align: start;&quot;&gt;배포하는 과정에서 Private Link 관련으로 삽집을 많이해서 기록용으로 작성하게 됐다)&amp;nbsp;&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;불편한 점이 꽤 있다. EC2로 배포하면 당연했던 것들이 서버리스라서 그런지 안되는 게 꽤 있다. 그 중 하나는 보통 private subnet에 위치한 RDS에 EC2를 터널링으로 사용하여 접근했는데, Fargate는 이게 안된다.(서버 접근할 때 사용하는 키페어가 없음) 그래서 Bastion Host 용도로 nano 인스턴스 타입의 EC2를 하나 생성했다 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그 설정이나 확인이 EC2보다 어려운 것 같다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너 오케스트레이션 목적이 크지 않다면, 다음에도 Fargate를 또 쓸지는 모르겠다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;사전 준비&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker 계정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS CLI 설치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS CLI를 사용할 수 있는 AWS IAM 계정 (충분히 권한이 있는지)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Fargate&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Fargate는 서버리스 컴퓨팅 서비스로 컨테이너를 실행하기 위해 사용할 수 있다. &lt;span style=&quot;text-align: left;&quot;&gt;ECS와 함께 사용해야 한다(단독 사용 X)&lt;/span&gt;.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECS는 Elastic Container Service의 줄임말로, 이름대로 완전 관리형 컨테이너 오케스트레이션 서비스이다. 쿠버네티스가 아닌 도커 컨테이너 관리를 위한 서비스이다. 쿠버네티스는 EKS라고 따로 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Fargate는 EC2와 달리 사용자가 서버나 클러스터를 관리할 필요가 없지만, 실제로는 EC2 위에서 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2와 동일 스펙으로 두고 보았을 때 요금이 더 비싸다. (솔직히 EC2에 비해 조금... 부담될 수도 있다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudWatch와 자동 연동 된다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;리소스(Task) 생성 시 VPC 내부 서브넷에 위치한다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ECR&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECR은 컨테이너 이미지를 저장 및 관리할 수 있는 서비스이다. 여기에 빌드한 Docker 이미지를 올려서 ECS(Fargate)를 통해 배포할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;컨테이너 이미지 레이어를 S3에 저장한다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;VPC 밖에 위치한다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;따라서 Fargate(VPC 내부)에서 ECR(S3)에 저장된 컨테이너 이미지를 가져오려면 인터넷 통신이 필요함&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Fargate의 위치가 퍼블릭 서브넷이고, 생성 시 공인 IP가 자동 할당되도록 설정했다면 문제없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프라이빗 서브넷이면 공인 IP가 할당되어도 인터넷 통신이 불가능하므로 ECR(S3)에서 컨테이너 이미지를 가져올 수 없음 &amp;rarr; &lt;b&gt;NAT Gateway 사용&lt;/b&gt; 또는 &lt;b&gt;Private Link 구성&lt;/b&gt;으로 해결해야 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Private Link&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC 엔드포인트, Gateway Endpoints(S3 또는 DynamoDB)를 사용해서 VPC 밖에 있는 두 서비스 사이에서 인터넷이 아닌 사설(private) 통신이 가능하도록 하는 서비스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;둘 중에 무엇을 선택할지는 비용을 따지는 편. 발생하는 비용은 각자 서비스의 데이터 처리량 등을 고려하여 계산.&lt;/span&gt;&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;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Fargate로 SpringBoot 애플리케이션 배포하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dockerfile 작성하기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도커 이미지 빌드 및 도커 허브에 push (이미지 이름 기억해 두기)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECR에서 Private Repository 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성한 레포지토리를 선택하고 &lt;b&gt;푸시 명령 보기&lt;/b&gt; 클릭, 가이드 보면서 순차적으로 진행&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECR 레지스트리에 Docker 클라이언트 인증 시 AWS CLI default 프로필 외에 다른 프로필 사용 시 아래와 같이 진행&lt;/span&gt;
&lt;pre id=&quot;code_1754565787489&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aws ecr get-login-password --region ap-northeast-2 --profile {profile_alias}| docker login --username AWS --password-stdin {account_id}.dkr.ecr.ap-northeast-2.amazonaws.com&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;태깅할 때 태그는 앞으로 생성될 이미지 태그와 겹치지 않게 고유한 태그로 생성할 것&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클러스터 생성: 클러스터는 서비스나 태스크의 논리적인 그룹이다.&amp;nbsp;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ECS &amp;gt; 클러스터 &amp;gt; 클러스터 생성&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인프라&lt;/b&gt;: AWS Fargate&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클러스터 이름 작성이랑 인프라 선택만 하고 완료하기&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작업(Task) 정의: &lt;span style=&quot;text-align: left;&quot;&gt;Task에 필요한 명세를 정의한다.&lt;/span&gt; Amazon ECS에서 Docker 컨테이너를 실행하려면 작업 정의(Task Definition)가 필요함. 이전에 생성한 도커 이미지를 참조하는 작업 정의를 생성한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ECS &amp;gt; 태스크 정의 &amp;gt; 새&amp;nbsp;태스크&amp;nbsp;정의&amp;nbsp;생성&amp;nbsp;&amp;gt;&amp;nbsp;새&amp;nbsp;태스크&amp;nbsp;정의&amp;nbsp;생성&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;태스크 정의 패밀리: 고유한 이름 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #9feec3;&quot;&gt;인프라&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;시작 유형&lt;/b&gt;: AWS Fargate&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;태스크 역할&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;태스크 역할: 건들지 않음&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;태스크 실행 역할&lt;/b&gt;: Amazon ECS를 처음 사용하는 경우 `ecsTaskExcutionRole IAM` 역할이 존재 X. 이 경우는 Task execution role 설정에서 &lt;b&gt;Create new role&lt;/b&gt;을 선택하면 자동으로 `ecsTaskExcutionRole` IAM 역할이 생성됨.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 애플리케이션에 필요한 사양에 맞춰서 진행 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #9feec3;&quot;&gt;컨테이너&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이름: 임의 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;이미지 URI&lt;/b&gt;: ECR에 올린 도커 이미지의 URI로 &lt;b&gt;ECR &amp;gt; 프라이빗 레지스트리 &amp;gt; 리포지토리 &amp;gt; {생성한 리포지토리}&lt;/b&gt;에서 이미지 URI를 복사할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필수 컨테이너: 예&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프라이빗 레지스트리: 비활성화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;포트 매핑&lt;/b&gt;: 여기에서는 스프링부트의 8080번 포트로 지정. 이 부분은 각자의 환경에 맞춰 설정. 로드밸런싱을 사용한다면 여기에서 로드밸런서의 요청을 받을 포트를 설정.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #9feec3;&quot;&gt;로깅&lt;/span&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1483&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/evvxzo/btsPLhYqB6Y/UUHvmTrmPgLX7kHPAorGwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/evvxzo/btsPLhYqB6Y/UUHvmTrmPgLX7kHPAorGwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/evvxzo/btsPLhYqB6Y/UUHvmTrmPgLX7kHPAorGwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fevvxzo%2FbtsPLhYqB6Y%2FUUHvmTrmPgLX7kHPAorGwk%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;1483&quot; height=&quot;497&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1483&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`awslogs-region`을 잘 확인하고, 나머지는 위와 같이 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 기본 상태로 두고 작업 정의를 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECS 작업 IAM 역할 수정: 앞 단계에서 자동 생성된 ECS 작업의 IAM 역할인 `ecsInstanceRole`에 `CloudWatchLogsFullAccess`를 추가&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM &amp;gt; 역할 &amp;gt; ecs 검색 &amp;gt; `&lt;span style=&quot;text-align: left;&quot;&gt;ecsInstanceRole` &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;선택&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;권한 탭 &amp;gt; 우측 권한 추가 메뉴 &amp;gt; 정책 연결 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;클릭&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;b&gt;검색창에서 `CloudWatchFullAcess` 검색 &amp;gt; ` &lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;b&gt;CloudWatchFullAcessV2` 선택 &amp;gt; 권한 추가&lt;/b&gt; 클릭&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가된 정책 확인&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALB 구성 및 생성: private subnet에 위치한 ECS와 연결하여 외부 트래픽 진입점이 될 ALB를 생성한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #9feec3;&quot;&gt;대상 그룹 생성&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EC2 &amp;gt; 대상 그룹 &amp;gt; 대상 그룹 생성&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상 유형: IP address&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;타겟 그룹 이름: 자유롭게 설정(보통 tg를 이름에 넣어서 대상 그룹임을 명시하는 편)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로토콜 : 포트: HTTP : 8080(ALB가 요청을 보내줄 포트, 배포할 컨테이너(ECS)의 실행 포트로 입력. 모르겠다면 애플리케이션의 실행포트로 입력)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC: 로드 밸런서를 호스팅 할 VPC 선택(ECS가 있는 VPC로)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상태 검사: 애플리케이션에 헬스 체크 API가 있다면 여기에 입력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 기본값으로 놔두고 &lt;b&gt;다음&lt;/b&gt; 클릭&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상 등록: 아무것도 건들지 않고, 대상도 등록하지 않고 생성 완료하기. 나중에 ECS가 생성될 때 생성된 태스크가 자동으로 등록됨.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #9feec3;&quot;&gt;ALB 생성&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EC2 &amp;gt; 로드 밸런서 &amp;gt; 로드 밸런서 생성&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ALB 생성&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런서 이름: 자유롭게 설정(tg처럼 이름에 alb를 추가해서 명시하는 편)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;체계(schema): 인터넷 경계(internet-facing)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 매핑: 대상 그룹과 동일한 VPC&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;선택 후, 가용 영역 및 서브넷: 2개 이상 선택&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안 그룹: 이 ALB를 위해 생성해 둔 것이 없다면 새롭게 생성. 보안 그룹은 아래 조건을 만족하면 됨.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;새로 생성 시 아웃바운드는 건들지 않고, 외부 트래픽을 받을 수 있도록 인바운드만 설정&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형: HTTP&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;포트: 80&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스: 0.0.0.0/0&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리스너 및 라우팅: 외부 트래픽을 어떤 프로토콜과 포트로 받을지 결정하여 이 통로만 열어두고&lt;span style=&quot;text-align: left;&quot;&gt;(리스너)&lt;/span&gt; , 받은 요청을 어디로 보낼지(라우팅) 설정&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로토콜: HTTP&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;포트: 80&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상 그룹: 방금 생성한 대상 그룹 선택. 여기로 요청이 라우팅 된다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요약 정보를 확인하고, 모두 맞다면 완료하기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ECS 서비스 생성: Amazon ECS를 사용하여 Amazon ECS 클러스터에서 지정된 작업 정의 인스턴스를 동시에 실행하고 관리할 수 있다. 이를 서비스라고 한다.&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ECS &amp;gt; 생성한 클러스터 선택 &amp;gt; 서비스 탭 &amp;gt; 생성 버튼&lt;/b&gt; 클릭&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스 세부 정보 설정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;환경 &amp;gt; 컴퓨팅 구성&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컴퓨팅 옵션: 시작 유형&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시작 유형: FARGATE&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;플랫폼 버전: LATEST&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 구성&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케줄링 전략: 복제본&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원하는 태스크: 서비스(여기서는 개발하는 프로덕트를 의미) 상황에 맞게 (여기서는 1개만 함)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Task: 특정 애플리케이션 기능을 수행하는 하나 이상의 컨테이너&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Service: 동일한 다수의 Task 그룹&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 기본값으로 두기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워킹 설정&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC: VPC는 이미 생성된 것을 사용하거나 다른 서비스/프로젝트와 구분 지어 사용하고 싶다면 새롭게 생성. 잘 모르겠다면 default VPC 사용도 상관 X.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서브넷: 프라이빗 서브넷(private subnet)으로 선택.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;퍼블릭 서브넷(public subnet)도 가능함. 이 경우 다음을 참고.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALB &amp;amp; VPC 엔드포인트나 NAT 없이도 외부 트래픽을 받을 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;퍼블릭 IP 옵션을 활성화 &amp;rarr; 하나의 &lt;span style=&quot;text-align: left;&quot;&gt;퍼블릭 IP에 대해서 시간당 0.005 USD의 요금이 부과됨&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALB와 VPC 엔드포인트 설정이 별도로 필요 없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;퍼블릭 서브넷에 두면 보안 및 확장성 측면에서 좋지 않음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안 그룹: 위에서 생성한 ALB를 고려해서 &lt;span style=&quot;text-align: left;&quot;&gt;ECS&lt;/span&gt;를 위해 미리 만들어둔 보안 그룹이 없다면, 새로 생성하기&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안 그룹 이름: 여기에서도 sg를 이름에 추가해서 보안 그룹임을 명시하는 편&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설명: 보통 어떤 목적의 보안 그룹인지 작성하는 편&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인바운드 규칙&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형: 사용자 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;포트 범위: 8080 (대상 그룹에서 입력한 포트와 동일하게)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스: ALB를 위해 만들었던 보안그룹을 선택 (ALB를 통해서만 외부 트래픽이 들어올 수 있게 됨)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런싱&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런싱 사용: 활성화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC: 로드 밸런싱이 위치한 VPC로 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드 밸런서 유형: ALB&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Application Load Balancer: 기존 로드 밸런서 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨테이너: 기본적으로 잘 선택되어 있을 테니, 가만히 내버려 두거나 아래를 참고해서 틀렸다면 수정하기&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;포트가 `8080:8080` 이렇게 되어 있을 텐데, 이런 의미다. `{컨테이너가 실행되는 호스트 머신의 포트}:{컨테이너 내부 포트}` &amp;rarr; a:b 라면 a번 포트를 컨테이너의 b 포트로 연결하여 외부에서 a번 포트로 접근하면 컨테이너의 b번 포트로 연결됨(포트 포워딩)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt; 로드 밸런서: 위에서 생성했던 로드 밸런서 선택 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리스너: 8080-HTTP (대상 그룹에서 입력한 포트와 동일하게)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상 그룹: 기존 대상 그룹 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상 그룹 이름: 위에서 생성한 대상 그룹 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상태 확인 경로: 헬스 체크 API 있으면 작성&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 기본값으로 놔두고 완료하기&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Private Link: 이대로 배포하면 ECR에서 이미지를 가져올 수 없다고 하는 통신/연결 에러가 발생할 것이다. 해결을 위해 다음 설정을 진행한다. 여기서는 3개의 VPC Endpoint(Interface형 VPC Endpoint 개당 0.013 USD / hour)와 S3 Gateway Endpoint(무료)를 생성한다. 참고로, 엔드포인트 사용료 외에도 데이터 처리량에 따른 요금이 추가로 부과된다. 자세한 건 &lt;a title=&quot;aws vpc pricing&quot; href=&quot;https://aws.amazon.com/ko/vpc/pricing/#:~:text=There%20are%20no%20data%20processing,please%20visit%20VPC%20Endpoints%20Documentation.&amp;amp;text=Use%20the%20Free%20Tier%20of,single%20AWS%20Region%20and%20account.&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt; 참고하기!&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #9feec3;&quot;&gt;VPC Endpoint&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;VPC &amp;gt; PrivateLink 및 Lattice &amp;gt; 엔드포인트&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;엔드 포인트 생성&lt;/b&gt; 클릭&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형: AWS 서비스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스 &amp;gt; 검색창: `ecr` &lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검색 시 `ecr.api`와 `ecr.dkr`이 나타남. 모두 필요하지만, 한 번에 여러 개 생성 불가능. 우선 둘 중 하나 선택하여 진행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 설정: ECS가 있는 VPC 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서브넷: ECS가 있는 서브넷&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안그룹: VPC 엔드포인트용 보안 그룹 생성&amp;nbsp;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 &amp;gt; 보안 그룹 &amp;gt; 보안 그룹 생성 - 인바운드 규칙만 추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형: HTTPS&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스 유형: 사용자 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소스: ECS가 속한 VPC의 IP 범위 (IPv4 CIDR)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정책: 전체 액세스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;완료 (다음 셋 중 하나라도 안 하면 배포 실패)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동일한 과정으로 총 3번 반복: `api`, `dkr`, `logs` &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #9feec3;&quot;&gt;VPC Gateway&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC 엔드포인트의 3번까지 동일&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스 &amp;gt; 검색창: `s3`&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유형이 `Gateway`인 항목으로 선택&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 설정: ECS가 있는 VPC 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;라우팅 테이블: ECS가 있는 서브넷과 연결된 라우팅 테이블 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정책: 전체 액세스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;완료&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;드디어 모든 설정 끝!!! ECS 배포 상태를 확인해 보기&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ECS &amp;gt; 클러스터 &amp;gt; 생성한 클러스터 클릭 &amp;gt; 서비스 탭 &amp;gt; 생성한 서비스 선택 &amp;gt; 배포 탭&lt;/b&gt; 클릭&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포가 정해진 횟수만큼 실패하다 보면, 자동으로 배포가 실행되지 않는다. 이때는 &lt;b&gt;서비스 업데이트&lt;/b&gt;를 클릭.&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 구성 &amp;gt; 새 배포 강제 적용: 활성화&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 손대지 말고 업데이트 완료하기&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 배포가 진행된다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포가 성공했다면, ALB의 DNS를 통해 접근할 수 있다. (HTTPS 미적용 상태임을 잊지 말기~)&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;선택된 서비스에서 &lt;b&gt;구성 및 네트워킹 &amp;gt; DNS 이름&lt;/b&gt;이 해당 정보이다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CoudWatch&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;별도로 설정을 안 했으면 기본 옵션으로 되어있다. 기본적으로 CloudWatch가 ECS에서 수집하는 지표는 ECS 클러스터 내 전체 자원의 평균 CPU 사용률, 평균 메모리 사용률 등 간단한 지표만 제공한다. 클러스터 수준의 지표만 수집하므로 세부 수준의 지표는 모니터링할 수 없다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Container Insight를 사용하면 클러스터 내에서 더 세부적인 지표를 수집할 수 있다. 두 가지가 있다.&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px; background-color: #ffffff;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;일반 컨테이너 인사이트&lt;/b&gt;: ECS 클러스터 및 ECS 서비스 수준에 대해 지표를 수집&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반 CloudWatch 지표 수집 비용과 동일. 수집한 지표 당 0.30 USD /month&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;향상된 관찰 기능을 갖춘 컨테이너 인사이트&lt;/b&gt;: ECS 클러스터, ECS 서비스, ECS 태스크 정의, ECS 태스크, 실행 중인 컨테이너 수준&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;에서 더 많은 지표를 수집&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;수집한 지표 당 0.07 USD / month&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;후자가 더 많은 지표를 수집하여 자세한 정보를 알 수 있긴 하지만, 지표 수가 많을수록 비용이 많이 부과되므로 잘 생각해 보고 선택해야 한다. 또한 서비스나 태스크는 트래픽 상황에 따라 얼마든지 증가할 수 있는데, 지표도 그만큼 많이 수집되므로 비용이 생각보다 많이 부과될 수도 있다. 주의하자!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt; 비용 관련 공식 정보: &lt;a href=&quot;https://aws.amazon.com/ko/cloudwatch/pricing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aws.amazon.com/ko/cloudwatch/pricing/&lt;/a&gt;&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;&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-best-practices.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-best-practices.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://catalog.us-east-1.prod.workshops.aws/workshops/8c9036a7-7564-434c-b558-3588754e21f5/ko-KR/01-intro&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://catalog.us-east-1.prod.workshops.aws/workshops/8c9036a7-7564-434c-b558-3588754e21f5/ko-KR/01-intro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/AmazonECR/latest/userguide/vpc-endpoints.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/ko_kr/AmazonECR/latest/userguide/vpc-endpoints.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.kyobodts.co.kr/2023/08/18/hands-on-alb-amazon-ecs-on-fargate-%EB%A9%80%ED%8B%B0-%ED%83%80%EA%B2%9F%EA%B7%B8%EB%A3%B9-%EC%97%B0%EA%B2%B0%EA%B9%8C%EC%A7%80/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.kyobodts.co.kr/2023/08/18/hands-on-alb-amazon-ecs-on-fargate-%EB%A9%80%ED%8B%B0-%ED%83%80%EA%B2%9F%EA%B7%B8%EB%A3%B9-%EC%97%B0%EA%B2%B0%EA%B9%8C%EC%A7%80/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://goodahn.tistory.com/220&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://goodahn.tistory.com/220&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sharplee7.tistory.com/125&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sharplee7.tistory.com/125&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>ALB</category>
      <category>AWS</category>
      <category>Docker</category>
      <category>ECR</category>
      <category>ECS</category>
      <category>Fargate</category>
      <category>Gateway Endpoint</category>
      <category>Private Link</category>
      <category>VPC Endpoint</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/198</guid>
      <comments>https://backend-jaamong.tistory.com/198#entry198comment</comments>
      <pubDate>Thu, 7 Aug 2025 23:54:48 +0900</pubDate>
    </item>
    <item>
      <title>CI/CD 구축하기: GitHub Actions (OIDC) + Amazon S3 + Amazon  CodeDeploy</title>
      <link>https://backend-jaamong.tistory.com/197</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS EC2 인스턴스(Ubuntu 24.04) 한 대에 배포되고 있는 Gradle/SpringBoot 서버에 대해서 OIDC를 이용해서 CI/CD를 구축해 보는 글입니다. EC2 인스턴스 생성은 다루지 않습니다.&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;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;⏭️다음 글: &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://backend-jaamong.tistory.com/199&quot;&gt;CI/CD에 블루-그린 무중단 배포 적용하기: AWS ELB, AWS Auto Scaling Groups(ASG)&lt;/a&gt; &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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;⭐항상 AWS 리소스를 다룰 땐 참고하고 있는 글이 최신 버전인지 확인하기&lt;span style=&quot;text-align: start;&quot;&gt;⭐&lt;/span&gt; &lt;/b&gt;&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;작성 계기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions를 CI 툴로 사용하는 대부분의 소개글은 GitHub Actions에서 AWS로 접근하기 위해 IAM 액세스 키를 많이 사용한다. 이때 여러 가지를 고려했을 때 IAM 액세스 키보다는 OpenID Connect(OIDC) 사용을 더 권장한다고 한다. 그래서 도입하기 위해 찾아보면, OIDC를 구성하는 글과 이를 CI/CD에 적용하는 글은 다 별개로 작성되어 있어 답답했다. 그래서 그냥 내가 해보면서 성공하면 작성하자!라는 생각으로 시작했고, 다행히 성공해서 이렇게 쓰게 되었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;사전 준비&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 루트 계정 말고, 필요한 권한을 다 갖고 있는 IAM 사용자; 개인 AWS 계정이더라도, IAM 사용자를 생성해서 진행하자.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 과정을 위해 IAM 사용자에게 어떤 권한을 부여해야 할지 모르겠고, 찾기도 힘들다면 `AdministratorAccess` 정책(policy)을 갖게 하자. 거의 대부분의 권한을 갖고 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI/CD 배포 대상인 EC2 인스턴스와 프로젝트 실행에 필요한 언어 설치&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스의 OS(AMI)는 Ubuntu 24.04&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VPC나 서브넷(subnet)은 AWS에서 기본적으로 제공해 주는 것을 사용해도 문제없다. 직접 생성하고 사용하려면 그것 자체로 포스트 하나가 나오므로.. 그냥 디폴트 VPC와 서브넷을 사용하자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서는 EC2 인스턴스에 Java 17 / SpringBoot / Gradle 프로젝트를 운영한다. 하지만 개발 언어나 프레임워크가 무엇인지는 크게 중요하지 않다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;언어 설치는 사실 미리 해두지 않아도 괜찮은 것 같다. 편한대로 하면 된다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포할 프로젝트가 올라가있는 GitHub 레포지토리&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레포지토리가 속한 조직(organization)과 어떤 브랜치를 대상으로 배포할지 미리 알아두기!&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: left;&quot;&gt;개인 계정인 경우, &lt;/span&gt;별도로 생성해둔 조직이 없다면 보통 계정명이 조직명이 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;브랜치도 특별히 정해진 브랜치 배포 규칙이 없다면 보통 `main` 브랜치가 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트에 미리 헬스체크 API 만들어두기 (당장 필요하진 않지만, 다음 글에서 필요)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTP 메소드는 `GET`, 반환은 `200 OK`만 해줘도 충분하다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;본론&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;전체 흐름&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI/CD 파이프라인에서 GitHub Actions는 CI를 담당한다. 개발자가 GitHub에 push 등의 지정한 동작을 수행하면 GitHub Actions이 트리거 되어 `.github/workflows/{수행할_파일}.yml`에 적힌 내용을 수행한다. 이 파일은 다음 워크플로우를 수행한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;[Build] 애플리케이션 빌드 (이 결과물을 아티팩트라고 함)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;[Build] 아티팩트를&lt;/span&gt; `&lt;a href=&quot;https://github.com/actions/upload-artifact?tab=readme-ov-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@actions/upload-artifact&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`를 이용하여 GitHub에 업로드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;[Deploy] GitHub에 업로드한 아티팩트를 `&lt;/span&gt;&lt;a href=&quot;https://github.com/actions/download-artifact&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@actions/download-artifact&lt;/a&gt;` &lt;span style=&quot;color: #000000;&quot;&gt;다운로드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;[Deploy] GitHub Actions OIDC를 이용하여 AWS credentials 설정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; [Deploy] 아티팩트 패키징(압축) 및 S3에 업로드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; [Deploy] CodeDeploy를 이용하여 S3에 업로드된 패키징을 EC2 인스턴스에 배포&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; [Deploy] 배포 완료 대기&lt;/span&gt;&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 보안그룹(Security Group) 설정&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스가 준비되었다면, EC2 보안그룹을 설정해보자. 이미 해두었을 것 같은데, 혹시 모르니 추가.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS는 &lt;span style=&quot;text-align: start;&quot;&gt;보안그룹에 대해 &lt;/span&gt;기본적으로 아웃바운드는 다 허용하고 인바운드는 다 막아둔다. 즉, 서버로 들어오는 요청은 다 막고, 나가는 것은 다 허용한다는 뜻이다. 따라서 필요한 요청은 받을 수 있게 적절하게 인바운드 규칙을 설정해야 한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 메뉴 하위에 위치한 보안그룹을 찾아서 이동한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b05JC8/btsPAkaM1qf/0HeGqKK4sxXIBRoaql2HJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b05JC8/btsPAkaM1qf/0HeGqKK4sxXIBRoaql2HJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b05JC8/btsPAkaM1qf/0HeGqKK4sxXIBRoaql2HJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb05JC8%2FbtsPAkaM1qf%2F0HeGqKK4sxXIBRoaql2HJk%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;200&quot; height=&quot;384&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;511&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;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스를 생성할 때 보안그룹을 새로 생성하는 옵션을 선택했다면, `launch-wizard-1` 이름을 가진 보안그룹이 있을 것이다. 이것을 선택하고, 인바운드 규칙 편집을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1547&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ejQRZI/btsPAUvRY1P/mvLbPvSx7b5s2zIvWwvmy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ejQRZI/btsPAUvRY1P/mvLbPvSx7b5s2zIvWwvmy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ejQRZI/btsPAUvRY1P/mvLbPvSx7b5s2zIvWwvmy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FejQRZI%2FbtsPAUvRY1P%2FmvLbPvSx7b5s2zIvWwvmy0%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;700&quot; height=&quot;325&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1547&quot; data-origin-height=&quot;718&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;span style=&quot;color: #000000;&quot;&gt;다음과 같이 설정하고 마치면 된다.&amp;nbsp;&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;1757&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7EDnt/btsPBInVz8r/S91bAra2up8yz2Po7pfypK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7EDnt/btsPBInVz8r/S91bAra2up8yz2Po7pfypK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7EDnt/btsPBInVz8r/S91bAra2up8yz2Po7pfypK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7EDnt%2FbtsPBInVz8r%2FS91bAra2up8yz2Po7pfypK%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;700&quot; height=&quot;120&quot; data-origin-width=&quot;1757&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스 생성 시 발급한 `.pem` 파일로 EC2에 접근하려면 SSH를 열어야 한다. 알고 있겠지만, 소스를 저렇게 `0.0.0.0/0`으로 다 열어두는 것보단 접속하는 곳의 IP만 지정하는 것이 보안상 좋다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서는 스프링 부트 애플리케이션을 실행하므로, `사용자 지정 TCP`로 유형을 선택하고 포트를 `8080`으로 입력한다. 8080번 포트가 아닌 다른 포트로 애플리케이션을 실행한다면 그 포트를 입력하면 된다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Nginx 같은 걸로 HTTP 80 요청을 받아서 애플리케이션에게 전달하는 상황이 아니라서, HTTP 80은 추가하지 않았다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 보안그룹 설정은 완료됐다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스에 CodeDeploy의 접근을 위한 IAM 역할(role) 생성 및 부여하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 리소스와 외부에서 서로 접근하듯이, AWS 리소스끼리도 서로 접근할 수 있다. 이를 위해 적절한 권한을 가진 IAM 역할이 리소스에게 부여되어야 한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;여기서는 다음을 고려해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스 to S3 버킷에 대한 접근 권한&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2나 온프레미스 환경에서 CodeDeploy를 사용하려면 CodeDeploy 에이전트가 필요하다. 그리고 이 에이전트가 필요로 하는 권한이 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 IAM 역할을 생성하고 부여해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM 역할 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM 메뉴 하위에 있는 역할을 클릭하여 이동한다. 이동한 대시보드에서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;역할 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&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;1865&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/md8Uq/btsPB4K5hNg/GMITF9LIdHSKeuVY31nTvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/md8Uq/btsPB4K5hNg/GMITF9LIdHSKeuVY31nTvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/md8Uq/btsPB4K5hNg/GMITF9LIdHSKeuVY31nTvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmd8Uq%2FbtsPB4K5hNg%2FGMITF9LIdHSKeuVY31nTvk%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;700&quot; height=&quot;50&quot; data-origin-width=&quot;1865&quot; data-origin-height=&quot;133&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;color: #000000;&quot;&gt;&lt;b&gt;1단계: 신뢰할 수 있는 엔터티 선택&lt;/b&gt;에서 &lt;b&gt;AWS 서비스&lt;/b&gt;를 선택하고, &lt;b&gt;사용 사례&lt;/b&gt;로 EC2를 선택한다. 선택하면 다양한 사용 사례가 아래 나열되는데, &lt;b&gt;EC2 만&lt;/b&gt; 적힌 것을 선택하면 된다. 그리고 넘어간다.&amp;nbsp;&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;1352&quot; data-origin-height=&quot;723&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHVmZs/btsPBgeth19/5WZUTA8g47UkWe4BDR2Erk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHVmZs/btsPBgeth19/5WZUTA8g47UkWe4BDR2Erk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHVmZs/btsPBgeth19/5WZUTA8g47UkWe4BDR2Erk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHVmZs%2FbtsPBgeth19%2F5WZUTA8g47UkWe4BDR2Erk%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;700&quot; height=&quot;374&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;723&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2단계: 권한 추가&lt;/b&gt;의 검색창에서 다음 두 개를 검색해서 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/aws-managed-policy/latest/reference/AmazonEC2RoleforAWSCodeDeploy.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AmazonEC2RoleforAWSCodeDeploy&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;: 이 권한은 EC2가 S3 버킷에 제한적으로 접근할 수 있도록 하며, EC2에 설치된 CodeDeploy 에이전트에게 필요하다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/aws-managed-policy/latest/reference/AmazonS3ReadOnlyAccess.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AmazonS3ReadOnlyAccess&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;: 이 권한은 모든 S3 버킷에 접근할 수 있도록 한다. 위 권한만을 가지고 S3 버킷에 접근할 수 있지만, CodeDeploy가 사용할 S3 생성 시 태그 설정(UseWithCodeDeploy=true)이 필요하다. 일단 이 권한을 추가해서 넓게 접근할 수 있도록 한다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실 이 부분을 CI/CD 구축한 후에 알아서 일단 이 버전으로 작성한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;권한 경계 설정&lt;/b&gt;은 손대지 않고 다음으로 넘어간다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3단계: 이름 지정, 검토 및 생성&lt;/b&gt;에서 이 역할의 이름과 설명을 작성한다. 이름은 어떤 역할을 수행하는지 나타낼 수 있도록 명확하게 작성한다. 나머지는 그대로 두고, &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;역할 생성&lt;/b&gt;&lt;/span&gt;을 클릭하여 완료한다.&amp;nbsp;&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;1002&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsw8p0/btsPzWabt4s/AHCp2lWrOXzUyp6nk5FxVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsw8p0/btsPzWabt4s/AHCp2lWrOXzUyp6nk5FxVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsw8p0/btsPzWabt4s/AHCp2lWrOXzUyp6nk5FxVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdsw8p0%2FbtsPzWabt4s%2FAHCp2lWrOXzUyp6nk5FxVK%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;700&quot; height=&quot;319&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;457&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM 역할 부여&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 EC2 인스턴스 대시보드로 돌아오자. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CI/CD를 적용할 인스턴스를 선택하고, &lt;b&gt;작업 &amp;gt; 보안 &amp;gt; IAM 역할 수정&lt;/b&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1869&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bppeXJ/btsPANKo76P/u4RO4u0mC0ohj85Kz3Dff1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bppeXJ/btsPANKo76P/u4RO4u0mC0ohj85Kz3Dff1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bppeXJ/btsPANKo76P/u4RO4u0mC0ohj85Kz3Dff1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbppeXJ%2FbtsPANKo76P%2Fu4RO4u0mC0ohj85Kz3Dff1%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;700&quot; height=&quot;134&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1869&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방금 만든 IAM 역할을 선택하고 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;IAM 역할 업데이트&lt;/b&gt;&lt;/span&gt;를 클릭하여 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1855&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tpP84/btsPzUcqK9E/ETq1JmZGHS6uAsATaKxt6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tpP84/btsPzUcqK9E/ETq1JmZGHS6uAsATaKxt6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tpP84/btsPzUcqK9E/ETq1JmZGHS6uAsATaKxt6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtpP84%2FbtsPzUcqK9E%2FETq1JmZGHS6uAsATaKxt6k%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;700&quot; height=&quot;190&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1855&quot; data-origin-height=&quot;503&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;EC2 인스턴스 태그(tag) 설정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CodeDeploy로 배포를 할 때는 이 리소스에게 정보를 제공해줘야 한다. 여기서&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;태그는 CodeDeploy 배포 그룹에서 배포할 EC2 인스턴스를 식별하고 타겟팅하는 용도로 사용된다. 또한 태그를 이용해서 `prod`나 `dev` 등의 배포 환경을 식별할 수도 있다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;다시 인스턴스 대시보드로 와서, 배포 대상인 인스턴스를 선택하자. 그리고 아래 태그 탭에서 태그 관리를 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1565&quot; data-origin-height=&quot;581&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EjRNk/btsPAYLBZI4/90CM4l7i6MNcN2IM4MTtv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EjRNk/btsPAYLBZI4/90CM4l7i6MNcN2IM4MTtv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EjRNk/btsPAYLBZI4/90CM4l7i6MNcN2IM4MTtv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEjRNk%2FbtsPAYLBZI4%2F90CM4l7i6MNcN2IM4MTtv1%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;700&quot; height=&quot;260&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1565&quot; data-origin-height=&quot;581&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;span style=&quot;color: #000000;&quot;&gt;`Environment` 키에는 배포 환경을 입력하고, `Application` 키는 원하는 대로 입력해도 된다. 입력하고 잘 기억해 두자. (두 키 값의 이름도 잘 기억만 해두면 상관없으니, 편한 대로 입력해도 상관없다)&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;1827&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eAmJRL/btsPBJAodrL/FHiZMydsdTLMXK2KWkfCmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eAmJRL/btsPBJAodrL/FHiZMydsdTLMXK2KWkfCmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eAmJRL/btsPBJAodrL/FHiZMydsdTLMXK2KWkfCmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeAmJRL%2FbtsPBJAodrL%2FFHiZMydsdTLMXK2KWkfCmK%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;700&quot; height=&quot;180&quot; data-origin-width=&quot;1827&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CodeDeploy 에이전트 설치 (Ubuntu 서버 버전)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CodeDeploy를 사용하여 EC2에 배포하려면 위 에이전트를 설치해야 한다. 터미널에서 아래 명령어로 EC2에 접속하자. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753540331526&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i {ec2 생성 시 발급받은 .pem이 위치한 경로 + 이름}.pem ubuntu@{ec2 퍼블릭 DNS 주소}

# 예시 1) pem 키가 위치한 곳에서 진행중일때
ssh -i example_key.pem ubuntu@ec2-dns-address  

# 예시 2) pem 키가 위치한 곳이 아닐때 (Windows cmd 이용 시)
ssh -i C:\Users\user\Desktop\...\example_key.pem ubuntu@ec2-dns-address&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 대상인 EC2 인스턴스의 AMI가 Ubuntu 이므로, ` &lt;span style=&quot;text-align: start;&quot;&gt;ubuntu`가 기본 사용자 이름이 된다.&lt;/span&gt; AMI에 따라 기본 사용자 이름이 다르므로 &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/managing-users.html#ami-default-user-names&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고.&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;&lt;span style=&quot;color: #000000;&quot;&gt;접속을 성공했다면 아래 명령어를 순서대로 입력하여 최신 버전의 CodeDeploy 에이전트를 설치한다. 혹시 모르니 &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;a style=&quot;color: #0593d3;&quot; href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-ubuntu.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;&lt;/span&gt;를 참고하면서 진행하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753540873890&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install ruby-full
sudo apt install wget
cd /home/ubuntu
wget https://bucket-name.s3.region-identifier.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잘 진행되었다면 아래 명령어를 입력하여 실행 상태를 확인한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753541011989&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;systemctl status codedeploy-agent&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정상적으로 실행되고 있다면 아래와 같이 `active (running)`를 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jp736/btsPAtyVQje/9FcATpzy55Ho5Pp57GlX1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jp736/btsPAtyVQje/9FcATpzy55Ho5Pp57GlX1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jp736/btsPAtyVQje/9FcATpzy55Ho5Pp57GlX1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJp736%2FbtsPAtyVQje%2F9FcATpzy55Ho5Pp57GlX1K%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;700&quot; height=&quot;66&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;72&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;OIDC 설정 및 &lt;/b&gt;&lt;b&gt;GitHub Actions OIDC를 위한&amp;nbsp;&lt;/b&gt;&lt;b&gt;IAM 역할 생성&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 OIDC 공급 업체를 등록하고 이 OIDC를 통해 GitHub Actions이 AWS 리소스에 접근할 수 있도록 역할을 생성 및 부여한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM &amp;gt; 제공업체&lt;/b&gt;로 이동하고 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;공급자 추가&lt;/b&gt;&lt;/span&gt;를 클릭한다.&amp;nbsp;&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;1887&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFDhg8/btsPAy0TZSu/h2j8JkeUZt7VE4U9sK1yP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFDhg8/btsPAy0TZSu/h2j8JkeUZt7VE4U9sK1yP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFDhg8/btsPAy0TZSu/h2j8JkeUZt7VE4U9sK1yP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFDhg8%2FbtsPAy0TZSu%2Fh2j8JkeUZt7VE4U9sK1yP1%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;700&quot; height=&quot;166&quot; data-origin-width=&quot;1887&quot; data-origin-height=&quot;447&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;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ID 제공업체 추가&lt;/b&gt; 화면에서 아래와 같이 입력하고, 하단에 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;공급자 추가&lt;/b&gt;&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;1848&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LfNaE/btsPAjb8gZg/gk9k49G8F21iujQZrxfYCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LfNaE/btsPAjb8gZg/gk9k49G8F21iujQZrxfYCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LfNaE/btsPAjb8gZg/gk9k49G8F21iujQZrxfYCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLfNaE%2FbtsPAjb8gZg%2Fgk9k49G8F21iujQZrxfYCK%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;700&quot; height=&quot;200&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공급자 유형:&lt;/span&gt; `OpenID Connect`&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공급자 URL:&lt;/span&gt; `https://token.actions.githubusercontent.com`&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대상:&lt;/span&gt; `sts.amazonaws.com`&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM &amp;gt; 역할&lt;/b&gt;로 이동하고 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;역할 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;b&gt;1단계: 신뢰할 수 있는 엔터티 선택&lt;/b&gt;에서 &lt;b&gt;웹 자격 증명&lt;/b&gt;을 선택한다.&amp;nbsp;&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;1016&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1E1Rg/btsPB5iWWqu/x84JlGO6Ks9nbiszQFJUU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1E1Rg/btsPB5iWWqu/x84JlGO6Ks9nbiszQFJUU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1E1Rg/btsPB5iWWqu/x84JlGO6Ks9nbiszQFJUU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1E1Rg%2FbtsPB5iWWqu%2Fx84JlGO6Ks9nbiszQFJUU1%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;700&quot; height=&quot;276&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;401&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;span style=&quot;color: #000000;&quot;&gt;선택하면 아래와 같은 화면이 나온다. 모두 입력하고 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&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;1497&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfZMgY/btsPAEtiPsQ/2HojR5a7rL2LIo3qMCZsak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfZMgY/btsPAEtiPsQ/2HojR5a7rL2LIo3qMCZsak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfZMgY/btsPAEtiPsQ/2HojR5a7rL2LIo3qMCZsak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfZMgY%2FbtsPAEtiPsQ%2F2HojR5a7rL2LIo3qMCZsak%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;700&quot; height=&quot;341&quot; data-origin-width=&quot;1497&quot; data-origin-height=&quot;729&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ID 제공업체: 아까 추가한 OIDC 제공업체가 목록에 있을 것이다. 이것을 선택한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Audience: OIDC 제공업체 추가 시 입력한 `대상`을 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub organization: 배포할 레포지토리가 위치한 조직명을 입력한다. 조직을 만든 적이 없다면, 보통 GitHub `username`이 조직명이 된다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Repository: 기본적으로 모두 허용인데(`*`), 보안상 타겟이 되는 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;레포지토리명을 입력하는 것이 좋다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;GitHub branch: 이것도 기본적으로 모두 허용이지만, 배포할 브랜치를 따로 입력하자.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2단계:&amp;nbsp;권한 추가&lt;/b&gt;에서 `AmazonS3FullAccess`, `AWSCodeDeployDeployerAccess`를 검색하여 추가한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/aws-managed-policy/latest/reference/AmazonS3FullAccess.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; AmazonS3FullAccess&lt;/a&gt;: &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;모든 버킷에 대한 전체 액세스를 제공한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/aws-managed-policy/latest/reference/AWSCodeDeployDeployerAccess.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWSCodeDeployDeployerAccess&lt;/a&gt;: &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;개정을 등록하고 배포할 수 있는 액세스 권한을 제공한다.&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;b&gt;3단계: 이름 지정, 검토 및 생성&lt;/b&gt;에서 역할의 이름과 설명을 입력한다.&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;1060&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhqGLN/btsPAUvYNXR/5o4HQk7trfVQfIP7fzK1wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhqGLN/btsPAUvYNXR/5o4HQk7trfVQfIP7fzK1wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhqGLN/btsPAUvYNXR/5o4HQk7trfVQfIP7fzK1wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhqGLN%2FbtsPAUvYNXR%2F5o4HQk7trfVQfIP7fzK1wK%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;700&quot; height=&quot;244&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;370&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;1단계에서 지정했던 신뢰할 수 있는 엔터티 선택의 JSON이 아래와 같은지 확인하자.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753530462640&quot; class=&quot;JSON&quot; data-ke-language=&quot;json, jsonc, json5&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Principal&quot;: {
                &quot;Federated&quot;: &quot;arn:aws:iam::{aws_account_id}:oidc-provider/token.actions.githubusercontent.com&quot;
            },
            &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
            &quot;Condition&quot;: {
                &quot;StringEquals&quot;: {
                    &quot;token.actions.githubusercontent.com:aud&quot;: &quot;sts.amazonaws.com&quot;
                },
                &quot;StringLike&quot;: {
                    &quot;token.actions.githubusercontent.com:sub&quot;: &quot;repo:{organization}/{repository}:ref:refs/heads/{branch}&quot;
                }
            }
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그다음 추가한 정책들이 모두 포함되어 있는지 확인하고, 모두 문제가 없다면 완료하자.&amp;nbsp;&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CodeDeploy을 위한 IAM 역할 생성 및 CodeDeploy 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CodeDeploy IAM 역할 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 CodeDeploy를 위한 IAM 역할을 생성한다. 이전과 같이 역할을 생성하는 곳으로 이동하여 진행한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;1단계: 신뢰할 수 있는 엔터티 선택&lt;/b&gt;에서 &lt;b&gt;AWS 서비스&lt;/b&gt;를 선택한다. &lt;b&gt;사용 사례&lt;/b&gt;는 &lt;b&gt;CodeDeploy&lt;/b&gt;를 선택하고 다음으로 넘어간다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;2단계: 권한 추가&lt;/b&gt;를 보면 자동으로 `AWSCodeDeployRole` 권한이 추가되어 있다. 다음으로 넘어가자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;3단계: 이름 지정, 검토 및 생성&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;에서 역할의 이름과 설명을 입력하고 마친다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CodeDeploy 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 &lt;b&gt;CodeDeploy &amp;gt; 배포 &amp;gt; 애플리케이션&lt;/b&gt;으로 이동하자. 이동하면 대시보드에 있는 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;애플리케이션 생성&lt;/b&gt;&lt;/span&gt; 버튼을 클릭한다.&amp;nbsp;&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;254&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xJszo/btsPzYeSXMa/q0nFayw6Pt0bFxb3Eqa47K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xJszo/btsPzYeSXMa/q0nFayw6Pt0bFxb3Eqa47K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xJszo/btsPzYeSXMa/q0nFayw6Pt0bFxb3Eqa47K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxJszo%2FbtsPzYeSXMa%2Fq0nFayw6Pt0bFxb3Eqa47K%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;200&quot; height=&quot;277&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;352&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;본인의 애플리케이션의 이름을 입력하고, 우리는 EC2 인스턴스에 배포할 계획이므로 컴퓨팅 플랫폼을 `EC2/온프레미스`로 선택한다. 그리고 &lt;b&gt;생성&lt;/b&gt; 버튼을 클릭한다. 참고로 애플리케이션 이름은 GitHub Actions CI 단계에서 필요하므로 잘 기억해 두자.&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;821&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS6Kaf/btsPCk1jzbZ/Q7wZFS6CJGtYCPNvXT1epK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS6Kaf/btsPCk1jzbZ/Q7wZFS6CJGtYCPNvXT1epK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS6Kaf/btsPCk1jzbZ/Q7wZFS6CJGtYCPNvXT1epK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS6Kaf%2FbtsPCk1jzbZ%2FQ7wZFS6CJGtYCPNvXT1epK%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;700&quot; height=&quot;492&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;577&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;span style=&quot;color: #000000;&quot;&gt;이후 생성한 애플리케이션을 클릭하면 나오는 대시보드에서 &lt;b&gt;배포 그룹&lt;/b&gt; 탭을 클릭한다. 여기에서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;배포 그룹 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&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;1543&quot; data-origin-height=&quot;493&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZCBYp/btsPAzZWBbG/4VzMrRLoA2Mman4oyybQB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZCBYp/btsPAzZWBbG/4VzMrRLoA2Mman4oyybQB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZCBYp/btsPAzZWBbG/4VzMrRLoA2Mman4oyybQB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZCBYp%2FbtsPAzZWBbG%2F4VzMrRLoA2Mman4oyybQB1%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;700&quot; height=&quot;224&quot; data-origin-width=&quot;1543&quot; data-origin-height=&quot;493&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배포 그룹 이름&lt;/b&gt;을 원하는 대로 입력한다. 입력해 놓고 잘 기억해 두자. GitHub Actions CI 단계에서 필요하다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스 역할은 방금 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;CodeDeploy&lt;/span&gt; 용으로 생성한 IAM 역할을 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vm4ky/btsPBIO8RGa/m2WDEthKTqYh95usyAFxFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vm4ky/btsPBIO8RGa/m2WDEthKTqYh95usyAFxFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vm4ky/btsPBIO8RGa/m2WDEthKTqYh95usyAFxFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvm4ky%2FbtsPBIO8RGa%2Fm2WDEthKTqYh95usyAFxFk%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;700&quot; height=&quot;591&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;702&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배포 유형&lt;/b&gt;과 &lt;b&gt;환경 구성&lt;/b&gt;을 아래와 같이 선택한다. 이때 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;태그 그룹&lt;/b&gt;의 키와 값은 본인이 EC2 인스턴스에 추가한 태그의 키와 값과 &lt;u&gt;동일&lt;/u&gt;&lt;/span&gt;해야 한다.&amp;nbsp;&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;818&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd0ees/btsPCjg3Aba/kkXl36wPhOQplCLnDmShMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd0ees/btsPCjg3Aba/kkXl36wPhOQplCLnDmShMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd0ees/btsPCjg3Aba/kkXl36wPhOQplCLnDmShMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd0ees%2FbtsPCjg3Aba%2FkkXl36wPhOQplCLnDmShMk%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;700&quot; height=&quot;744&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;869&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 유형: 현재 위치(In-place)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;환경 구성: Amazon EC2 인스턴스&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;태그 그룹: EC2 인스턴스에 등록한 태그와 동일하게 설정&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CodeDeploy 에이전트는 이미 설치했으므로 &lt;b&gt;안 함&lt;/b&gt;을 선택한다. &lt;b&gt;배포 설정&lt;/b&gt;의 &lt;b&gt;배포 구성&lt;/b&gt;은 `AllAtOnce`로 선택한다. &lt;b&gt;로드 밸런싱 활성화&lt;/b&gt;는 체크하지 않는다. 이제 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;배포 그룹 생성&lt;/b&gt;&lt;/span&gt;을 클릭하여 완료한다.&amp;nbsp;&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;810&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvkR81/btsPBhLeZ6v/kbMTsKoKDBJZqUOIxa1jJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvkR81/btsPBhLeZ6v/kbMTsKoKDBJZqUOIxa1jJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvkR81/btsPBhLeZ6v/kbMTsKoKDBJZqUOIxa1jJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvkR81%2FbtsPBhLeZ6v%2FkbMTsKoKDBJZqUOIxa1jJK%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;700&quot; height=&quot;709&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;820&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;S3 버킷 생성 및 정책 설정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;S3 버킷 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;S3 서비스로 이동해서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;버킷 만들기&lt;/b&gt;&lt;/span&gt;를 클릭한다. 그리고 다음과 같이 진행한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;버킷 이름&lt;/b&gt;: 누군가가 쓰고 있는 이름은 사용할 수 없으므로, 전역적으로 식별되는 이름을 입력해야 한다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions에서 필요하니 이름을 기억해 둘 것&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;객체 소유권&lt;/b&gt;: ACL 비활성화됨(권장)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;이 버킷의 퍼블릭 액세스 차단 설정&lt;/b&gt;: 모든 퍼블릭 액세스 차단&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;버킷 버전 관리&lt;/b&gt;: 활성화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;기본 암호화 - 암호화 유형&lt;/b&gt;: Amazon&amp;nbsp;S3&amp;nbsp;관리형&amp;nbsp;키(SSE-S3)를&amp;nbsp;사용한&amp;nbsp;서버&amp;nbsp;측&amp;nbsp;암호화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;기본 암호화 - 버킷 키&lt;/b&gt;: 활성화&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위에서 언급되지 않은 나머지 요소들은 초기 상태 그대로 놔둔다. 이제 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;버킷 만들기&lt;/b&gt;&lt;/span&gt;를 클릭하여 완료한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;S3 버킷 정책 설정&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리는 방금 생성한 S3 버킷에 대해 크게 네 가지 Sid를 갖도록 정책을 생성할 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions OIDC를 통해 S3 버킷에 객체를 업로드할 수 있게 함. `Condition` 문을 추가하여 역할을 맡으려는 보안 주체에 대한 추가 요구 사항을 설정할 것.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions OIDC를 통해 업로드 중&amp;nbsp;객체의 액세스 제어 목록 (ACL)을 변경할 수 있으며, S3 버킷에서 객체를 조회할 수 있게 함.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CodeDeploy가 S3 버킷에서 객체를 조회할 수 있고, 버킷 리스트를 확인할 수 있게 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스가 S3 버킷에서 객체를 조회할 수 있고, 버킷 리스트를 확인할 수 있게 함&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 `GitHub Actions ODIC 공급자의 ARN`, `CodeDeploy ARN`, `EC2 인스턴스 ARN`, `S3 버킷 ARN`가 모두 필요하다. 미리 찾아놓자.&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;범용 버킷&lt;/b&gt; 탭에서 생성한 S3 버킷을 클릭하고 &lt;b&gt;권한&lt;/b&gt; 탭으로 이동한다. 아래로 이동하면 나오는 &lt;b&gt;버킷 정책&lt;/b&gt;에서 &lt;b&gt;편집&lt;/b&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgbgK4/btsPB0WHTan/j3FjM9hkiZ41SnRKJlPaQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgbgK4/btsPB0WHTan/j3FjM9hkiZ41SnRKJlPaQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgbgK4/btsPB0WHTan/j3FjM9hkiZ41SnRKJlPaQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgbgK4%2FbtsPB0WHTan%2Fj3FjM9hkiZ41SnRKJlPaQ1%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;700&quot; height=&quot;515&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;676&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;span style=&quot;color: #000000;&quot;&gt;이동 후 나오는 창에서 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;정책 생성기&lt;/b&gt;&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;914&quot; data-origin-height=&quot;173&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cv7z7q/btsPAkbdi4Q/6NUtK0kXNYYoaQzjSdZGg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cv7z7q/btsPAkbdi4Q/6NUtK0kXNYYoaQzjSdZGg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cv7z7q/btsPAkbdi4Q/6NUtK0kXNYYoaQzjSdZGg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcv7z7q%2FbtsPAkbdi4Q%2F6NUtK0kXNYYoaQzjSdZGg1%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;700&quot; height=&quot;132&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;173&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;span style=&quot;color: #000000;&quot;&gt;Sid 마다 항목별로 입력하고 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;Add Statement&lt;/b&gt;&lt;/span&gt;를 클릭하면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Sid 1.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step 1: Select policy type&lt;/b&gt;: S3 Bucket Policy&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step 2: Add statement(s)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Effect&lt;/b&gt;: Allow&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Principal&lt;/b&gt;: IAM &amp;gt; OIDC 공급자 &amp;gt; 등록했던 GitHub Actions 공급자 클릭 &amp;gt; ARN 복사 및 붙여 넣기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Actions&lt;/b&gt;: PutObject&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Amazon Resource Name (ARN)&lt;/b&gt;: &quot;{생성한 S3 버킷 ARN}&quot;,&quot; {생성한 S3 버킷 ARN}/*&quot;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Add conditions. &lt;/b&gt;아래처럼 입력 후 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;Add Condition&lt;/b&gt;&lt;/span&gt; 클릭.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Condition&lt;/b&gt;: StirngEquals&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Key&lt;/b&gt;: s3:x-amz-server-side-encryption&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Value&lt;/b&gt;: AES256&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Sid 2.&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step 1: Select policy type&lt;/b&gt;: S3 Bucket Policy&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step 2: Add statement(s)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Effect&lt;/b&gt;: Allow&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Principal&lt;/b&gt;: IAM &amp;gt; OIDC 공급자 &amp;gt; 등록했던 GitHub Actions 공급자 클릭 &amp;gt; ARN 복사 및 붙여 넣기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Actions&lt;/b&gt;: PutObjectAcl, GetObject&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Amazon Resource Name (ARN)&lt;/b&gt;: &lt;span style=&quot;text-align: left;&quot;&gt;&quot;{생성한 S3 버킷 ARN}&quot;,&quot; {생성한 S3 버킷 ARN}/*&quot;&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Sid 3.&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step 1: Select policy type&lt;/b&gt;: S3 Bucket Policy&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step 2: Add statement(s)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Effect&lt;/b&gt;: Allow&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Principal&lt;/b&gt;: 생성한 CodeDeploy ARN 입력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Actions&lt;/b&gt;: GetObject, ListBucket&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Amazon Resource Name (ARN)&lt;/b&gt;:&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&quot;{생성한 S3 버킷 ARN}&quot;,&quot; {생성한 S3 버킷 ARN}/*&quot;&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Sid 4.&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step 1: Select policy type&lt;/b&gt;: S3 Bucket Policy&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step 2: Add statement(s)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Effect&lt;/b&gt;: Allow&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Principal&lt;/b&gt;: 생성한 EC2 인스턴스 ARN 입력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Actions&lt;/b&gt;: GetObject, ListBucket&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Amazon Resource Name (ARN)&lt;/b&gt;:&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&quot;{생성한 S3 버킷 ARN}&quot;,&quot; {생성한 S3 버킷 ARN}/*&quot;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;모두 입력했다면 아래에 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Generate Policy&lt;/b&gt;&lt;/span&gt;를 클릭하고, 나오는 팝업에서 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;Copy Policy&lt;/b&gt;&lt;/span&gt;를 클릭한다. 다시 버킷 정책 편집창으로 돌아가서 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;복사한 정책을&lt;/span&gt; JSON 입력란에 붙여 넣기 한다. &lt;/span&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px;&quot;&gt;완료하면 아래와 같은 JSON 형식의 정책을 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753667163072&quot; class=&quot;JSON&quot; data-ke-language=&quot;json, jsonc, json5&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
	&quot;Version&quot;: &quot;2012-10-17&quot;,
	&quot;Statement&quot;: [
		{
			&quot;Sid&quot;: &quot;{자유롭게_입력_중복안됨}&quot;,
			&quot;Effect&quot;: &quot;Allow&quot;,
			&quot;Principal&quot;: {
				&quot;AWS&quot;: {GitHub_Actions_OIDC_ARN}
			},
			&quot;Action&quot;: &quot;s3:PutObject&quot;,
			&quot;Resource&quot;: [
				&quot;arn:aws:s3:::{S3_BUCKET_NAME}&quot;,
				&quot;arn:aws:s3:::{S3_BUCKET_NAME}/*&quot;
			],
			&quot;Condition&quot;: {
				&quot;StringEquals&quot;: {
					&quot;s3:x-amz-server-side-encryption&quot;: &quot;AES256&quot;
				}
			}
		},
		{
			&quot;Sid&quot;: &quot;{자유롭게_입력_중복안됨}&quot;,
			&quot;Effect&quot;: &quot;Allow&quot;,
			&quot;Principal&quot;: {
				&quot;AWS&quot;: {GitHub_Actions_OIDC_ARN}
			},
			&quot;Action&quot;: [
				&quot;s3:PutObjectAcl&quot;,
				&quot;s3:GetObject&quot;
			],
			&quot;Resource&quot;: [
				&quot;arn:aws:s3:::{S3_BUCKET_NAME}&quot;,
				&quot;arn:aws:s3:::{S3_BUCKET_NAME}/*&quot;
			]
		},
		{
			&quot;Sid&quot;: &quot;{자유롭게_입력_중복안됨}&quot;,
			&quot;Effect&quot;: &quot;Allow&quot;,
			&quot;Principal&quot;: {
				&quot;AWS&quot;: {CodeDeploy_ARN}
			},
			&quot;Action&quot;: [
				&quot;s3:GetObject&quot;,
				&quot;s3:ListBucket&quot;
			],
			&quot;Resource&quot;: [
				&quot;arn:aws:s3:::{S3_BUCKET_NAME}&quot;,
				&quot;arn:aws:s3:::{S3_BUCKET_NAME}/*&quot;
			]
		},
		{
			&quot;Sid&quot;: &quot;{자유롭게_입력_중복안됨}&quot;,
			&quot;Effect&quot;: &quot;Allow&quot;,
			&quot;Principal&quot;: {
				&quot;AWS&quot;: {EC2_INSTANCE_ARN}
			},
			&quot;Action&quot;: [
				&quot;s3:GetObject&quot;,
				&quot;s3:ListBucket&quot;
			],
			&quot;Resource&quot;: [
				&quot;arn:aws:s3:::{S3_BUCKET_NAME}&quot;,
				&quot;arn:aws:s3:::{S3_BUCKET_NAME}/*&quot;
			]
		}
	]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모두 확인했다면 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;변경 사항 저장&lt;/b&gt;&lt;/span&gt;을 클릭하여 완료한다.&amp;nbsp;&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;appspec.yml 작성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 AWS에서의 작업은 완료되었고, 프로젝트 루트 디렉토리에 `appspec.yml`을 생성한다. 이 파일은 CodeDeploy가 각 배포 단계에서 무엇을 수행해야 하는지 명시한다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이 글에서는 각 요소가 무엇을 의미하는지는 설명하지 않는다. 사실 보면 어떤 역할을 하는지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start; color: #000000;&quot;&gt;자세히는 몰라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;어림잡아 알 수 있다. `AppSpec` 파일의 `hooks`에 대한 자세한 설명은 &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-user.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753669910721&quot; class=&quot;YAML yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: 0.0
os: linux

files:
  - source: /
    destination: /home/ubuntu/hello
    overwrite: yes

permissions:
  - object: /
    owner: ubuntu
    group: ubuntu
    mode: 755

hooks:
  BeforeInstall:
    - location: scripts/install_dependencies.sh
      timeout: 300
      runas: root
  ApplicationStart:
    - location: scripts/start_server.sh
      timeout: 300
      runas: ubuntu
  ApplicationStop:
    - location: scripts/stop_server.sh
      timeout: 300
      runas: ubuntu&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;scripts 작성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 루트 디렉토리에 `scripts` 디렉토리를 생성하고 하위에 `install_dependencies.sh`, `start_server.sh`, `stop_server.sh`를 생성한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;install_dependencies.sh&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 파일은 배포 파일(bundle)이 설치되기 전 실행되며, 애플리케이션 실행에 필요한 라이브러리 등을 설치한다. 이 글에서는 간단한 Gradle, Java 애플리케이션을 실행하는 경우를 다룬다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 애플리케이션마다 요구되는 의존성이 다르므로 확인 후 여기에 추가 작성하면 된다. 또한 EC2 인스턴스의 AMI(OS)에 따라 명령어가 다르니 그 부분도 잘 확인하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753670208873&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

sudo apt update -y&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;start_server.sh&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 단계는 의존성이나 배포 파일이 설치된 이후에 실행된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753670515762&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

cd /home/ubuntu/hello
sudo pkill -f java
nohup java -jar build/libs/*.jar &amp;gt; springboot-app.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`.jar` 파일이 있는 위치로 이동&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행되고 있는 java 프로그램 종료: 새로운 버전을 실행하기 전에 기존 버전 애플리케이션 종료&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`nohup`을 이용한 `.jar` 파일 실행 및 로그 기록&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`nohup`으로 프로그램을 데몬으로 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로그램의 표준 출력(1)과 표준 에러(2)를 `springboot-app.log` 파일에 기록&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`2&amp;gt;&amp;amp;1` : 표준 에러를 표준 출력이 기록되는 같은 파일에 리다이렉트함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;백그라운드로 실행(&amp;amp;)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;stop_server.sh&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 단계는 애플리케이션의 새 버전을 설치하기 전에 실행되므로, 기존 버전을 종료하는 명령어를 작성한다. 지금까지의 순서상으로는 가장 먼저 실행된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753671349472&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

sudo pkill -f java&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;deploy.yml 작성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;루트 디렉토리에 `.github/workflows/deploy.yml`를 생성한다. 위치만 정확하면 파일 이름은 상관없다.&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;&lt;span style=&quot;color: #000000;&quot;&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;290&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2l08T/btsPC6WkHoD/1ZLay9q84fq2v758YSKyjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2l08T/btsPC6WkHoD/1ZLay9q84fq2v758YSKyjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2l08T/btsPC6WkHoD/1ZLay9q84fq2v758YSKyjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2l08T%2FbtsPC6WkHoD%2F1ZLay9q84fq2v758YSKyjk%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;200&quot; height=&quot;262&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;380&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;span style=&quot;color: #000000;&quot;&gt;배포할 작업물이 있는 GitHub 레포지토리로 이동하자. 그리고 &lt;b&gt;Settings &amp;gt; Security &amp;gt; Secrets and variables &amp;gt; Actions&lt;/b&gt;로 이동하자. `Repository secrets`에 AWS 계정 ID를 환경변수로 등록하자.&amp;nbsp;&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;620&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQFPio/btsPAMk0nh9/1HBMkpdsxYHNNGKp0tYZZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQFPio/btsPAMk0nh9/1HBMkpdsxYHNNGKp0tYZZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQFPio/btsPAMk0nh9/1HBMkpdsxYHNNGKp0tYZZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQFPio%2FbtsPAMk0nh9%2F1HBMkpdsxYHNNGKp0tYZZk%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;700&quot; height=&quot;515&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;456&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;span style=&quot;color: #000000;&quot;&gt;이 코드는 Gradle 프로젝트에 맞춰져 있다. 따라서 아래 코드를 복붙하고 상황에 맞춰 잘 수정하자. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 단계마다 간단하게 주석을 적어놨으니 꼭! 제대로! 확인!&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753679185577&quot; class=&quot;YAML yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Java CI with Gradle

on:
  push:
    branches: [ &quot;main&quot; ]  # main 브랜치에 push 하면 워크플로우 동작
  pull_request:
    branches: [ &quot;main&quot; ]  # main 브랜치에 PR이 발생하면 워크플로우 동작

env:
  AWS_REGION: {AWS_리소스를_생성한_REGION}
  S3_BUCKET: {S3_BUCKET_이름}
  CODEDEPLOY_APPLICATION: {CodeDeploy_Application_이름}
  CODEDEPLOY_DEPLOYMENT_GROUP: {CodeDeploy_배포그룹_이름}
  
jobs:
  # Job 1. 빌드
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
    # Step 1. 소스코드 가져오기
    - name: Checkout code
      uses: actions/checkout@v4

    # Step 2. Java 환경 설정
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    - name: Caching gradle dependencies
      uses: actions/cache@v4
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    # Step 3. Gradle 설정
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

    # Step 4. gradlew 실행 권한 얻기
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    # Step 5. 애플리케이션 빌드
    - name: Build with Gradle Wrapper
      run: ./gradlew clean bootJar

    # Step 6. 빌드한 JAR 파일 저장
    - name: Upload build artifacts
      uses: actions/upload-artifact@v4
      with:
        name: jar-artifact
        path: build/libs/*.jar

  # Job 2. 배포
  deploy:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
    # Step 1. 소스 코드 가져오기
    - uses: actions/checkout@v4

    # Step 2. 이전 단계에서 빌드한 JAR 파일 다운로드
    - name: Download build artifacts
      uses: actions/download-artifact@v4
      with:
        name: jar-artifact
        path: build/libs/

    - name: show all files downloaded  # 어디에 저장되었는지 확인용
      run: |
        ls -al build/libs/
        
    # Step 3. OIDC로 AWS credentials 설정
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::${{secrets.AWS_ACCOUNT_ID}}:role/GitHubActionsRole
        role-session-name: GitHub_to_AWS_via_FederatedOIDC
        aws-region: ${{ env.AWS_REGION }}

    # Step 4. 배포 패키징
    - name: Create deployment package
      run: |
        # Create a temporary directory for deployment package
        mkdir -p deployment-package

        # Create a ZIP file: .jar 파일, AppSpec 파일, scripts 디렉토리 전체
        zip -r deployment-package.zip build/libs/*.jar appspec.yml scripts  

    # Step 5. S3에 패키징 파일 업로드
    - name: Upload to S3
      run: | 
        # Generate a unique filename 
        COMMIT_HASH=$(echo ${{ github.sha }} | cut -c1-7)
        DEPLOYMENT_KEY=&quot;deployments/springboot-${COMMIT_HASH}.zip&quot;  # S3 버킷에 deploymenets 객체(디렉토리)가 생기고 그 하위에 springboot-{github_commit_id}.zip 파일이 생긴다.

        # Upload to S3
        aws s3 cp deployment-package.zip s3://${{ env.S3_BUCKET }}/${DEPLOYMENT_KEY}

        # Save the S3 key 
        echo &quot;DEPLOYMENT_KEY=${DEPLOYMENT_KEY}&quot; &amp;gt;&amp;gt; $GITHUB_ENV

    # Step 6. CodeDeploy 트리거
    - name: Deploy with CodeDeploy
      run: | 
        # Create a deployment with CodeDeploy
        DEPLOYMENT_ID=$(aws deploy create-deployment \
          --application-name ${{ env.CODEDEPLOY_APPLICATION }} \
          --deployment-group-name ${{ env.CODEDEPLOY_DEPLOYMENT_GROUP }} \
          --s3-location bucket=${{ env.S3_BUCKET }},key=${{ env.DEPLOYMENT_KEY }},bundleType=zip \
          --deployment-config-name CodeDeployDefault.AllAtOnce \
          --description &quot;Deployment from GitHub Actions - commit ${{ github.sha }}&quot; \
          --query 'deploymentId' \
          --output text)

        echo &quot;Deployment ID: $DEPLOYMENT_ID&quot;
        echo &quot;DEPLOYMENT_ID=${DEPLOYMENT_ID}&quot; &amp;gt;&amp;gt; $GITHUB_ENV

    # Step 7. 배포 완료 대기
    - name: Wait for deployment completion
      run: |
        echo &quot;Waiting for deployment ${{ env.DEPLOYMENT_ID }} to complete...&quot;    

        # Wait for the deployment to finish
        aws deploy wait deployment-successful --deployment-id ${{ env.DEPLOYMENT_ID }}

        # Get deployment status
        STATUS=$(aws deploy get-deployment \
          --deployment-id ${{ env.DEPLOYMENT_ID }} \
          --query 'deploymentInfo.status' \
          --output text)
        
        echo &quot;Deployment completed with status: $STATUS&quot;
        
        if [ &quot;$STATUS&quot; != &quot;Succeeded&quot; ]; then
          echo &quot;Deployment failed!&quot;
          exit 1
        fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GitHub Actions를 CI로 사용하여 AWS에 배포하는 스크립트는 구글링 하면 많이 나오니, 여기에서는 OIDC 부분만 참고해도 좋다.&amp;nbsp;&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;확인&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기까지 진행했다면 이번에는 `main` 브랜치에 `push` 해보자! &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방금 적은 스크립트에 의해 워크플로우가 트리거 되고, 레포지토리 `Actions` 탭에서 확인할 수 있다. 잘 진행되었다면 아래와 같이 파란 불을 볼 수 있다!&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;927&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNR3P7/btsPADBSfmd/YkWmuGd4KdklLgvKkmjPS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNR3P7/btsPADBSfmd/YkWmuGd4KdklLgvKkmjPS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNR3P7/btsPADBSfmd/YkWmuGd4KdklLgvKkmjPS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNR3P7%2FbtsPADBSfmd%2FYkWmuGd4KdklLgvKkmjPS0%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;700&quot; height=&quot;639&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;846&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;span style=&quot;color: #000000;&quot;&gt;위에서 생성한 S3 버킷도 확인해 보면 `deploymenets/springboot-{github_commit_id}.zip`이 있는 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhFuUS/btsPB30qSui/25oFOKjN08zbeYmKfKHp00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhFuUS/btsPB30qSui/25oFOKjN08zbeYmKfKHp00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhFuUS/btsPB30qSui/25oFOKjN08zbeYmKfKHp00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhFuUS%2FbtsPB30qSui%2F25oFOKjN08zbeYmKfKHp00%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;700&quot; height=&quot;396&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;897&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;&lt;span style=&quot;color: #000000;&quot;&gt;CodeDeploy 상태도 확인해 보자. &lt;b&gt;CodeDeploy 애플리케이션&lt;/b&gt;에서 &lt;b&gt;배포&lt;/b&gt;나 &lt;b&gt;배포그룹&lt;/b&gt; 탭으로 이동하고, 성공이 있는 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;777&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGvoor/btsPA0DoIvp/4ZJz0TbQ9hZgenArzYExlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGvoor/btsPA0DoIvp/4ZJz0TbQ9hZgenArzYExlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGvoor/btsPA0DoIvp/4ZJz0TbQ9hZgenArzYExlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGvoor%2FbtsPA0DoIvp%2F4ZJz0TbQ9hZgenArzYExlk%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;700&quot; height=&quot;609&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;777&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;span style=&quot;color: #000000;&quot;&gt;마지막으로 EC2 인스턴스에 배포된 헬스체크 API를 확인해 보자. Postman이든 브라우저든 창에 아래처럼 입력하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753683538878&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://{EC2_퍼블릭_DNS_주소}/{health_check_api_endpoint}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정한 대로 잘 나왔다면 진짜 끝!&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배포 실패의 경우 확인하는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포가 언제나 성공하면 좋겠지만, 그렇지 않아서...&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CodeDeploy &amp;gt; 애플리케이션 &amp;gt; 배포 &amp;gt; 실패한 배포 ID&lt;/b&gt;를 클릭하고, 나오는 창에서 하단으로 내리면 &lt;b&gt;배포 수명 주기 이벤트 &amp;gt; `View events`&lt;/b&gt; 버튼이 있다(없는 경우도 있는데, 그런 경우에는 `실패한 배포 ID`를 클릭해서 나오는 메시지를 확인하자). 이걸 클릭하면 (그나마) 자세한 실패 원인을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;249&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzWxG8/btsPClzNsiQ/sggrT6SxRQGZMYaO5dFY81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzWxG8/btsPClzNsiQ/sggrT6SxRQGZMYaO5dFY81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzWxG8/btsPClzNsiQ/sggrT6SxRQGZMYaO5dFY81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzWxG8%2FbtsPClzNsiQ%2FsggrT6SxRQGZMYaO5dFY81%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;700&quot; height=&quot;173&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;249&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;다음으로&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기까지 잘 성공했다면 이제 CI/CD 구축을 완료한 것이다! 물론 한 대의 인스턴스만을 사용하기 때문에 무중단 배포는 아니지만, 일단 CI/CD 구축 성공을 축하하자 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음 글에서는 현재 조합에서 AWS Elastic Load Balancing(ELB), AWS AutoScaling Group(ASG)를 추가하여 블루/그린 무중단 배포 하는 방법을 다룰 예정이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.uplus.co.kr/%EA%B9%83%ED%97%88%EB%B8%8C-%EC%95%A1%EC%85%98-github-action-%EF%B8%8E-aws-%EC%9D%B8%EC%A6%9D-openid-connect-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-8cbe0ff4434c&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.uplus.co.kr/%EA%B9%83%ED%97%88%EB%B8%8C-%EC%95%A1%EC%85%98-github-action-%EF%B8%8E-aws-%EC%9D%B8%EC%A6%9D-openid-connect-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-8cbe0ff4434c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tao-tech.tistory.com/21&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tao-tech.tistory.com/21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/getting-started-setting-up.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/getting-started-setting-up.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://joonyon.tistory.com/entry/%EC%89%BD%EA%B2%8C-%EC%84%A4%EB%AA%85%ED%95%9C-nohup-%EA%B3%BC-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%82%AC%EC%9A%A9%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://joonyon.tistory.com/entry/%EC%89%BD%EA%B2%8C-%EC%84%A4%EB%AA%85%ED%95%9C-nohup-%EA%B3%BC-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%82%AC%EC%9A%A9%EB%B2%95&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>AWS</category>
      <category>ci/cd</category>
      <category>CodeDeploy</category>
      <category>GitHub Actions OIDC</category>
      <category>S3</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/197</guid>
      <comments>https://backend-jaamong.tistory.com/197#entry197comment</comments>
      <pubDate>Mon, 28 Jul 2025 20:01:53 +0900</pubDate>
    </item>
    <item>
      <title>[SQLite Error] SQLITE_BUSY 예외와 WAL 모드</title>
      <link>https://backend-jaamong.tistory.com/195</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQLite를 로컬 DB로 사용하고 있는 프로젝트에서 잊을만하면 마주치는 에러가 있어서 정리한다. 프로젝트에서는 Hibernate 버전 6을 사용하고 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인 API 이후 사용자 정보 수정 API를 시도했을 때 아래와 같은 예외가 발생했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752205914337&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.sqlite.SQLiteException: [SQLITE_BUSY] The database file is locked (database is locked)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`application.yml`에서 `datasource` 설정은 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752206235939&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  spring.datasource:
    url: jdbc:sqlite:project.db?busy_timeout=5000
    driver-class-name: org.sqlite.JDBC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가적으로 알아야 할 정보가 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인 API는 정상 응답으로 현재 로그인한 사용자의 정보를 반환한다. (transaction: read-only)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 정보 수정 API 또한 로그인 API가 접근하는 엔티티에 접근하며, 이를 수정해야 한다. (transaction: not read-only)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 문제는 SQLite가 동시 액세스를 처리하는 방식와 Hibernate가 연결을 관리하는 방식에서 비롯된다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. SQLite Locking Mechanism&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQLite는 대부분의 엔터프라이즈 데이터베이스가 사용하는 행 수준(row-level)이 아닌, 파일 수준(file-level) lock을 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;읽기 작업에 대해서, 한 트랜잭션이 시작하면 SQLite는 파일에 대해 `shared lock`을 건다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 프로세스가 읽기 작업은 가능하지만, 쓰기 작업은 할 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쓰기 작업에 대해서, 한 트랜잭션이 시작하면 SQLite는 파일에 대해 `exclusive lock`을 건다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 프로세스는 쓰기 작업을 할 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 잠금은 예상보다 길어질 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. Hibernate Connection Management&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Hibernate는 데이터베이스 커넥션을 트랜잭션 완료 이후에 바로 해제하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커넥션은 커넥션 풀에 반환되지만, SQLite 파일 lock은 지속될 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정 API가 `exclusive lock`을 획득하려고 할 때, 로그인 API의 read-only 트랜잭션은 여전히 `shared lock`을 유지하고 있을 수 있다.&lt;/span&gt;&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. Transaction Isolation&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 API의 트랜잭션이 분리되어 있어도, 시간적으로는 겹칠 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQLite의 기본 `journal mode`인 `DELETE`는 이러한 잠금 문제가 발생하기 쉽다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1752209240821&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;읽기 시작 &amp;rarr; `shared lock` 획득 &amp;rarr; 모든 쓰기 작업 차단
쓰기 대기 &amp;rarr; `SQLITE_BUSY` 에러 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 수정 API를 통해 쓰기 작업(exclusive lock)을 시도했지만, 여전히 로그인 API의 읽기 작업으로 걸린 `shared lock`이 해제되지 않아서 발생한 문제이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;찾아본 해결 방법은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동시 접근을 위한 설정 추가: `application.yml` 수정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정 기능에 재시도 메커니즘 추가: &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;Spring `@Retryable` 애노테이션 사용&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션 수준의 동기성 제어: &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java `&lt;span style=&quot;text-align: start;&quot;&gt;synchronized&lt;/span&gt;` 사용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선 `application.yml` 수정을 시도해보고 동일 문제가 발생하면 점진적으로 수정 단계를 높여가는 것으로 정했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752208617585&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:sqlite:project.db?busy_timeout=30000&amp;amp;journal_mode=WAL&amp;amp;synchronous=NORMAL&amp;amp;cache_size=10000&amp;amp;temp_store=MEMORY
    driver-class-name: org.sqlite.JDBC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. journal_mode=WAL&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 옵션은 `Write-Ahead Logging`을 활성화하여 더 나은 동시성을 허용한다. 메인 데이터베이스 파일을 변경하는 것이 아닌 분리된 WAL 파일에 쓰기 작업을 진행한다. 읽을 때는 메인 데이터베이스와 커밋된 WAL 엔트리를 읽게 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752209517788&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;읽기 작업 &amp;rarr; 읽기: 메인 DB + WAL 파일 &amp;rarr; No blocking
쓰기 작업 &amp;rarr; WAL 파일에 쓰기 &amp;rarr; 동시 진행 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동시에 여러 읽기 작업을 할 수 있고 이때 하나의 쓰기 작업도 가능하다. (하지만 여전히 동시 여러 쓰기 작업은 지원하지 않는다)&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. synchronous=NORMAL &lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;(문제 해결 X, 쓰기 성능 향상 목적)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 옵션은 SQLite가 디스크에 데이터를 동기화하는 빈도를 조절할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`OFF` : 가장 빠르지만, 가장 안전하지 않은 옵션&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;`NORMAL` : 균형잡힌 상태. &lt;/span&gt;안전성을 유지하면서 `fsync` 호출을 줄인다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`FULL` : 가장 안전하지만, 가장 느리다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 모든 쓰기 작업마다 `fsync`를 호출하지 않으며, &lt;span style=&quot;text-align: start;&quot;&gt;중요한 순간에만 동기화 된다. 다만 서버가 다운되면 데이터 소실이 일어날 수 있다.&amp;nbsp;&lt;/span&gt;&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;3. cache_size=10000 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: start; color: #333333;&quot;&gt;(문제 해결 X, 읽기 성능 향상 목적)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;페이지(Page)는 SQLite에서 데이터를 다루는 기본 단위이다. 이 옵션은 페이지 캐시 사이즈를 설정한다. 기본적으로 2000 pages로 설정되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사이즈를 높임으로써 메모리를 더 사용하게 된다. 이는 메모리에 더 많은 데이터를 유지할 수 있고, 자주 접근되는 데이터는 캐시에서 가져올 수 있음을 의미한다. 또한 무거운 읽기 작업에 대해 더 나은 성능을 제공하게 된다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;4. temp_store=MEMORY &lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;(문제 해결 X, 성능 향상 목적)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 설정은 `FILE`로 임시 작업에 대해 디스크를 사용한다. `MEMORY`로 변경하면 임시 테이블, 인덱스, 정렬 작업에 대해 disk 대신 메모리를 사용하게 된다. 임시 파일은 file lock을 위해 경쟁하지 않게 된다(lock 경쟁 감소).&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이는 sorting, join 쿼리와 같은 곳의 성능을 높일 수 있다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;5. busy_timeout=30000&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터베이스가 잠겼을 때(lock) 포기하기 전에 얼마나 대기할지 설정&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;.db-wal, .db-shm&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 설정을 적용하고 나서 서버를 가동하고 나니 db 파일 외에 다른 파일이 추가로 생성되었다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;`.db-wal` 파일은 커밋되지 않은 쓰기 작업을 담고 있다. 새로운 데이터는 처음에 여기에 쓰이고 후에 메인 데이터베이스로 이동된다(Write-Ahead Log). 처음 쓰기 작업이 발생할 때 생성되며, 체크포인트될 때까지 유지된다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;`.db-shm` 파일은 더욱 빠른 접근을 위해 WAL 파일을 인덱싱하는 목적을 갖고 있다. &lt;span style=&quot;text-align: start;&quot;&gt;WAL 파일과 함께 생성되고 WAL이 체크포인트되면 삭제된다.&amp;nbsp;&lt;/span&gt; &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;&lt;span style=&quot;color: #000000;&quot;&gt;이 두 파일은 런타임 시 발생하는 것이므로 굳이 버전 관리 저장소에 올리거나 배포할 때 포함시키지 않는다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://junstar17.github.io/db/2020/11/16/DB-SQLite-Lock-%EB%A7%A4%EC%BB%A4%EB%8B%88%EC%A6%98.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://junstar17.github.io/db/2020/11/16/DB-SQLite-Lock-%EB%A7%A4%EC%BB%A4%EB%8B%88%EC%A6%98.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gywn.net/2013/08/let-me-intorduce-sqlite/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gywn.net/2013/08/let-me-intorduce-sqlite/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yeon-kr.tistory.com/213&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yeon-kr.tistory.com/213&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/195</guid>
      <comments>https://backend-jaamong.tistory.com/195#entry195comment</comments>
      <pubDate>Sat, 19 Jul 2025 12:33:18 +0900</pubDate>
    </item>
    <item>
      <title>Hexagonal Architecture / Ports &amp;amp; Adapters Pattern</title>
      <link>https://backend-jaamong.tistory.com/196</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Notice &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 글은 개인적으로 읽기 편하라고 개인 노션에 막 정리한 글을 공유한 것입니다. 스스로가 정리한 것을 까먹지 않기 위함...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Hexagonal Architecture인지 포트 어댑터 패턴인지... 여러 개념들이 등장해서 어딘가 적어두면서 생각하지 않으면 이해하기가 너무 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 라인에서 작성한 글을 메인으로 잡고 잘 모르는 개념을 검색하며 노션에 막! 정리했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/Hexagonal-Port-Adapter-22ee4ff26d86807497cbe5ddd763765d?source=copy_link&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.notion.so/Hexagonal-Port-Adapter-22ee4ff26d86807497cbe5ddd763765d?source=copy_link&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;후기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;살짝 프로젝트 적용해 보면서 느낀 점은 우선... 다 좋은데 코드 작성량이 정말 많이 많아진다. 필요한 무언가가 있으면 이걸 위해서 작성해야 하는 코드(파일)들이 +1이 아니라 +n이 되기 쉽다. 패키지도 많이 만들어진다. 그리고 개념을 정확히 알아두지 않으면 클래스 생성할 때마다 어디에 둬야 하는지 혼란스럽고, 이게 맞나? 싶어진다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 영속 계층과 애플리케이션 계층이 분리되어야 하다보니 이 계층 간 데이터 이동이 JPA 엔티티가 아니라 POJO를 통해 이루어지는데, 이때 레이어드 아키텍처랑 Spring Data JPA에 매우 익숙해져 있으면 처음에 트랜잭션 관련으로 불편을 겪을 수도 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장점은 보통 `@Service`를 붙이는 클래스들이 깔끔해지고 설계 방향성이 뚜렷하다 보니 DTO를 이동시킬 때 일관되게 만들 수 있다. 연관된 친구들끼리 두니 의도 파악에도 좋다. 테스트도 가벼워진다. 이 부분에서 크나큰 좋음을 느꼈다. (그런데 이제 수많은 코드 생성을 곁들인...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기에는 다양한 참고 링크가 있습니다만, 노션에 첨부해뒀습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/196</guid>
      <comments>https://backend-jaamong.tistory.com/196#entry196comment</comments>
      <pubDate>Sat, 12 Jul 2025 17:39:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] @CurrentUser</title>
      <link>https://backend-jaamong.tistory.com/194</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스프링 시큐리티는 `&lt;a href=&quot;https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/core/annotation/AuthenticationPrincipal.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@AuthenticationPrincipal&lt;/a&gt;` 애노테이션을 제공한다. 이는 `&lt;a href=&quot;https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/core/Authentication.html#getPrincipal()&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Authenticaiton.getPrincipal()&lt;/a&gt;`를 메서드 인자로 가져오는 데 사용되며, &lt;span style=&quot;text-align: start;&quot;&gt;보통은 `UserDetails` 타입이나 해당 타입의 커스텀 구현체로 가져오게 된다.&amp;nbsp;&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Authentication.getPrincipal()`에는 username, password&lt;span style=&quot;color: #333333;&quot;&gt;(AuthenticationManager 구현에 따라 없을 수 있음)&lt;/span&gt;, role과 같은 사용자 정보가 담겨있다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간단하게 해당 애노테이션을 그대로 사용해도 되지만, 더 편리하게 사용할 수 있다. 이는 공식문서에서도 안내하고 있다.&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-filename=&quot;blob&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBxbmB/btsO4mNFGCx/xg5tQxjqPtO1HGkyqqdQvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBxbmB/btsO4mNFGCx/xg5tQxjqPtO1HGkyqqdQvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBxbmB/btsO4mNFGCx/xg5tQxjqPtO1HGkyqqdQvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBxbmB%2FbtsO4mNFGCx%2Fxg5tQxjqPtO1HGkyqqdQvK%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;610&quot; height=&quot;336&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;486&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;span style=&quot;color: #000000;&quot;&gt;실제로 구현해 보자. 여기서는 커스텀 애노테이션 이름을 `@CurrentUser`로 설정했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751607473094&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.security.core.annotation.AuthenticationPrincipal;

import java.lang.annotation.*;

/**
 * 익명 사용자인 경우에는 null로, 익명 사용자가 아닌 경우에는 실제 Users 객체로
 */
@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = &quot;#this == 'anonymousUser' ? null : user&quot;)
public @interface CurrentUser {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`@AuthenticationPrincipal.expression`에 SpEL 표현식을 사용하여 원하는 값을 가져오도록 설정할 수 있다. 위 표현식은 다음을 의미한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 객체(`#this`)가 익명 사용자(`anonymousUser`, 인증되지 않음)라면 `null`을 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아니라면, `Authentication.getPrincipal()`을 통해 사용자를 나타내는 객체(`UserDetails` 또는 다른 구현체)를 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구현을 완료하면 다음과 같이 사용자 객체를 간편하게 애노테이션으로 가져올 수 있다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751607830572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class UserController {
	
    ...

    @GetMapping(&quot;/info&quot;)
    public ResponseEntity&amp;lt;ResponseBody&amp;lt;UserInfo&amp;gt;&amp;gt; getInfo(@CurrentUser Users user) {
        UserInfo userInfo = userInfoService.get(user);
        return ResponseUtil.from(HttpStatus.OK.value(), userInfo);
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로 위에서 사용한 `Users` 객체는 직접 정의한 도메인 객체이다.&amp;nbsp; `UserDetails` 구현체는 아니지만, `UserDetailsService. loadUserByUsername`에서 `UserDetails` 구현체를 생성하여 반환할 때 `Users` 객체를 인자로 넣어주고 있어서 가능하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751608189721&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) {
        Users user = userRepository.findByEmail(email)
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(NOT_FOUND_USER.getMessage()));
        return new CustomUserDetails(user); // 커스텀 UserDetails 클래스 생성자에 직접 구현한 Users 객체를 인자로 넘김
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>@AuthenticationPrincipal</category>
      <category>@currentuser</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/194</guid>
      <comments>https://backend-jaamong.tistory.com/194#entry194comment</comments>
      <pubDate>Sat, 12 Jul 2025 12:09:37 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] 세션 기반 로그인 구현하기</title>
      <link>https://backend-jaamong.tistory.com/193</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;u&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Spring Security 6.5.0 기준 작성&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/u&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;span style=&quot;color: #000000;&quot;&gt;로그인을 구현할 때 보통 JWT를 많이 사용하는데, 이번에는 세션 기반으로 구현했다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;일단... JWT를 사용하여 로그인과 로그아웃을 구현하면 신경 쓸 것이 꽤 많다. 특히 로그아웃이 복잡한 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 인증을 위한 액세스 토큰(Access Token) 및 액세스 토큰 재발급을 위한 리프레시 토큰(Refresh Token) 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청 시 사용된 토큰이 유효한지 검증(만료, 변조 여부 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그아웃을 해도 액세스 토큰의 만료기간이 남아있다면 재사용 가능 &amp;rarr; 이를 막기 위해 로그아웃 시 해당 액세스 토큰을 블랙리스트(저장소)에 저장&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;블랙리스트 저장소에 대한 액세스 속도를 높이기 위해 보통 Redis와 같은 인메모리 DB를 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한 번은 블랙리스트 사용말고 다른 방법이 없나 찾아봤다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리프레시 토큰은 DB에 저장해야 하니 로그아웃 요청이 들어오면 리프레시 토큰을 DB에서 제거하는 방법을 사용한 적도 있다. 이러면 다시 인증을 수행할 수밖에 없다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이외에도 고민할 것은 더 많을 것이다. 그래서 지금은 (배포 금지) MVP 단계이기도 하고, 주어진 개발 시간이 매우 짧기 때문에 세션 기반 로그인으로 선택했다.&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;size14&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; &lt;i&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/span&gt; &amp;nbsp;&lt;/i&gt;세션 기반 로그인 구현의 경우, 서버는 로그인이 성공하면 쿠키에 `JSESSIONID` 값을 담아 응답을 반환한다. 클라이언트는 서버로 인증이 필요한 요청을 보낼 때 이 값을 쿠키에 담아 보내야 한다.&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;h3 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;세션 인증 필터 구현하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;요청이 들어오면 무엇을 해야 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JWT를 사용하는 경우, 로그인 시 로그인 요청 정보가 유효한 회원 정보면 JWT를 생성하고 이를 클라이언트에게 반환하는 것이 기본 흐름이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필터 수준으로 생각해보자. 일단 매 요청마다 JWT 검증 필터를 거치게 하고, 로그인과 같이 인증이 필요 없는 요청인 경우에는 JWT 검증을 하지 않는다. 이때 &lt;span style=&quot;color: #f3c000;&quot;&gt;(1)&lt;/span&gt;요청 경로가 로그인 URI이고 &lt;span style=&quot;color: #f3c000;&quot;&gt;(2)&lt;/span&gt;요청 바디에 담긴 정보가 DB에 저장된 정보와 일치하면 &lt;span style=&quot;color: #f3c000;&quot;&gt;(3)&lt;/span&gt;JWT를 생성하고 &lt;span style=&quot;color: #f3c000;&quot;&gt;(4)&lt;/span&gt;이 토큰을 &lt;span style=&quot;text-align: start;&quot;&gt;`SecurityContext`에 저장하여 인증이 유지되도록 한다.&amp;nbsp;&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; color: #000000;&quot;&gt;이 역할을 수행하는 필터는 보통 `OncePerRequestFilter`를 상속하여 구현되며, `UsernamePasswordAuthenticationFilter` 이전에 실행된다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;세션 기반으로 구현할 때도 로그인을 처리하고 새로운 세션을 생성하여 인증을 유지하기 위한 필터가 필요하다. 마찬가지로&amp;nbsp;매 요청마다 수행하기 위해 `OncePerRequestFilter`를 상속하여 커스텀 필터를 구현했다. 이 필터에서는 다음의 것들이 수행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인증이 필요하지 않은 요청인지 확인&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음 필터 호출&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인 요청인지 확인&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자 정보가 유효한지 검증&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유효하다면 새로운 세션 생성 및 저장&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 두 조건에 모두 해당하지 않는다면&amp;nbsp;HTTP 세션에서 현재 요청의 `SecurityContext`로 사용자 인증 상태를 복원&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SessionBasedAuthenticationFilter&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위에서 언급한 일을 수행하는 필터를 구현해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 코드는 `OncePerRequestFilter`를 상속하고, 이 필터의 추상 메서드인 `doFilterInternal` 로직을 구현한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751531754234&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class SessionBasedAuthenticationFilter extends OncePerRequestFilter {

    private final Set&amp;lt;String&amp;gt; NOT_AUTH_URI = Set.of(
            &quot;/health&quot;,
            &quot;/user/join&quot;,
            ...
    );

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        if (NOT_AUTH_URI.contains(request.getRequestURI())) { // 인증이 필요없는 요청
            filterChain.doFilter(request, response);
            return;
        }

        String loginUrl = &quot;/user/login&quot;;
        if (loginUrl.equals(request.getRequestURI())) { // 로그인 요청
            handleLogin(request, response);
            filterChain.doFilter(request, response);
            return;
        }

        // 인증이 요구되는 요청
        loadAuthenticationFromSession(request); 
        filterChain.doFilter(request, response);
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;첫 번째 `if`문에서는 인증이 필요 없는 요청인지 확인한다. 맞다면,&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;세션으로부터 인증 객체를 가져와서 무언가를 수행할 필요가 없는 요청이기 때문에 다음 필터를 호출한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;호출 이후 돌아오면 필터를 종료시킨다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;두 번째 `if`문에서는 로그인 요청인지 확인한다. 맞다면,&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인 관련 로직을 수행하는 `handleLogin` 메서드를 호출한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수행 후 다음 필터를 호출하고, 호출 이후 돌아오면 필터를 종료시킨다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 조건에 부합하지 않는다면, 조건을 따져 세션으로부터 사용자 인증 상태를 복원한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 로그인 요청이 맞다면 로그인 자격 증명을 수행하는 `handleLogin` 메서드를 구현한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751532516970&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class SessionBasedAuthenticationFilter extends OncePerRequestFilter {

    private final Set&amp;lt;String&amp;gt; NOT_AUTH_ENDPOINT = Set.of(...);

    private final AuthenticationManager authenticationManager;
    private final SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ...
    }
    
    private void handleLogin(HttpServletRequest request, HttpServletResponse response) {
        try {
            UserLogin loginRequest = objectMapper.readValue(request.getInputStream(), UserLogin.class);
            // 인증 처리
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword());
            Authentication authentication = authenticationManager.authenticate(authenticationToken);

            SecurityContext context = SecurityContextHolder.createEmptyContext();
            context.setAuthentication(authentication);
            SecurityContextHolder.setContext(context);

            // 세션 저장
            securityContextRepository.saveContext(context, request, response);

            // 타임아웃 설정 - 1시간
            HttpSession session = request.getSession();
            session.setMaxInactiveInterval(3600);
        } catch (UsernameNotFoundException e) {
            log.error(...);
            request.setAttribute(&quot;exception&quot;, e.getMessage());
        } catch (BadCredentialsException e) {
            log.error(...);
            request.setAttribute(&quot;exception&quot;, e.getMessage());
        } catch (IOException e) {
            log.error(...);
            request.setAttribute(&quot;exception&quot;, e.getMessage());
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;`ObjectMapper`를 이용하여 요청 바디를 추출한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청 바디로부터 사용자 이메일 또는 닉네임 및 비밀번호를 추출하여&lt;span style=&quot;text-align: left;&quot;&gt; `UsernamePasswordAuthenticationToken` 타입의 인증 토큰을 생성한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성한 토큰을 `AuthenticationManager.authenticate()`로 보내 유효한지 검증한다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제로 이를 수행하는 건 `AuthenticationProvider`를 구현한 커스텀 `UsernamePasswordAuthenticationProvider` 클래스의 `authenticate` 메서드이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;`authenticate()`&lt;/span&gt;에서는 `UserDetailsService.loadUserByUsername` 구현체를 사용하여 DB에 저장된 정보와 요청 정보가 일치하는지 검증한다. 유효하면 새로운 `Authentication` 객체를 생성하여 반환하고, 유효하지 않은 경우 `UsernameNotFoundException` 또는 `BadCredentialsException` 예외를 던진다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 발생한 예외들은 `AuthenticationEntryPoint`를 통해 처리된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;새로운 `SecurityContext`를 생성하고, 여기에 위에서 생성한 인증 객체를 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`SecurityContextHolder`에 위 `SecurityContext`를 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`HttpSessionSecurityContextRepository`에도 컨텍스트를 저장하여 요청 간 컨텍스트을 유지한다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`HttpSessionSecurityContextRepository`는 인증된 사용자의 `SecurityContext`를 HTTP 세션에 저장하고 관리하는 역할을 수행한다. 즉, 사용자가 로그인하면 해당 사용자의 인증 정보(Ex. 사용자 ID, 권한 등)를 세션에 저장하여, 이후 요청에서도 해당 정보를 재사용할 수 있도록 한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로, 요청 객체에서 세션을 추출하여 만료 시간을 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 세션으로부터 기존에 저장된 인증을 가져오는 `loadAuthenticationFromSession`을 구현한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그전에 알아둘 것이 있다!&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTP는 상태를 유지하지 않는다.(stateless) &amp;rarr; 각 요청은 새로이 시작하며 독립적이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 1분 전에 로그인을 해서 `authentication`이 세션이 저장됐더라도, &lt;b&gt;새로 &lt;/b&gt;들어온 요청은 `authentication`이 없다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`authentication`은 세션에 존재하지만 현재 요청의 `SecurityContext`에는 없다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 메서드는 세션으로부터 현재 요청의 `SecurityContext`에 `authentication`을 가져오는 역할을 수행한다. &amp;rarr; 이제 현재 요청도 인증된 것으로 나타난다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만일 세션이 만료되었거나 로그인한 적이 없다면&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;세션에서 `authentication`을 찾을 수 없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 요청은 인증되지 않은 상태로 유지됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security이 인증 상태를 확인하지만, `authentication`이 없음 &amp;rarr; `AuthenticationEntryPoint`가 401 에러를 반환&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 메서드는 유효한 인증 세션이 있는 클라이언트의 요청을 다루지만, 현재 요청 스레드에는 아직 인증이 로드되지 않은 상태이다.&amp;nbsp; &lt;b&gt;즉, &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;클라이언트가 인증된 상태인지 검증하는 것이 아니라 &lt;/span&gt;`authentication`을 복원하는 로직이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751542035171&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class SessionBasedAuthenticationFilter extends OncePerRequestFilter {

    private final Set&amp;lt;String&amp;gt; NOT_AUTH_ENDPOINT = Set.of(...);

    private final AuthenticationManager authenticationManager;
    private final SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ...
    }
    
    private void handleLogin(HttpServletRequest request, HttpServletResponse response) {
        ...
    }
    
    private void loadAuthenticationFromSession(HttpServletRequest request) {
       
        // 세션에 이미 authentication 객체가 존재하는지 확인 (중복 저장 방지)
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            return;
        }

        SecurityContext context = securityContextRepository.loadDeferredContext(request).get();

        if (context != null &amp;amp;&amp;amp;
                context.getAuthentication() != null &amp;amp;&amp;amp;
                context.getAuthentication().isAuthenticated()) {
            SecurityContextHolder.setContext(context);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`authentication`이 이미 존재하는지 확인한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 필터가 이미 `authentication`을 설정했는지 확인하여 중복 처리를 방지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SecurityContext에서 기존 `authentication`을 덮어쓰지 않기 위함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTP 세션에서 이전에 저장된 `SecurityContext`를 가져온다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 deprecate 된 `loadContext` 메서드는 사용할 수 없으므로 `loadDeferredContext` 메서드를 사용해야 한다.&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cKboHJ/btsO2IKBXeX/AAAAAAAAAAAAAAAAAAAAAHe8hvpEMIYZIsBV4DIPHWA52LZXnslcFLuDJHgLi3_-/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1753973999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ysKGXqjiY4uH0p41T12YmlB5T5w%3D&quot; width=&quot;610&quot; height=&quot;511&quot; data-image-src=&quot;https://blog.kakaocdn.net/dna/cKboHJ/btsO2IKBXeX/AAAAAAAAAAAAAAAAAAAAAHe8hvpEMIYZIsBV4DIPHWA52LZXnslcFLuDJHgLi3_-/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1753973999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ysKGXqjiY4uH0p41T12YmlB5T5w%3D&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;737&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 컨텍스트는 사용자의 인증 정보를 담고 있다. (로그인 처리 시 저장한 것을 떠올리기!)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`HttpSessionSecurityContextRepository`를 사용하여 세션 저장소에 접근한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;불러온 컨텍스트가 유효한지, 인증된 사용자 정보를 담고 있는지 확인한다. 조건에 부합하면&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 요청에 대해서 `SecuritnContext`를 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 함으로써 컨트롤러나 다른 곳에서 사용자 인증을 사용할 수 있게 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SecurityConfig&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정 클래스에서는 위에서 구현한 필터가 `UsernamePasswordAuthenticationFilter` 전에 실행되도록 필터 체인에 등록하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751544835332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final String[] PERMIT_ALL_URL = {...};
    private final SessionBasedAuthenticationFilter sessionBasedAuthenticationFilter;
    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final AccessDeniedHandler accessDeniedHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .formLogin(AbstractHttpConfigurer::disable)
                .headers(headers -&amp;gt; headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
                .csrf(AbstractHttpConfigurer::disable)
                .cors(cors -&amp;gt; cors.configurationSource(CorsConfig.corsConfigurationSource()))
                .authorizeHttpRequests(
                        auth -&amp;gt; auth
                                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                                .requestMatchers(PERMIT_ALL_URL).permitAll()
                                .anyRequest().authenticated()
                )
                .addFilterBefore(  // here
                        sessionBasedAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(e -&amp;gt; e
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler)
                );
        return http.build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로 필터 체인 빈 외에 시큐리티와 관련하여 필요한 빈들은 다른 설정 클래스에 두었다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;여러 컴포넌트와 참조되는 상태라&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;같이 두면 순환 참조가 발생할 확률이 높다. (CORS는 아예 전용 설정 클래스를 만들어 두는 게 편해서 따로 빼두었다.)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751545041873&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class SecurityDependencyConfig {

    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder);
        return new ProviderManager(authenticationProvider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 설정 클래스에서 `AuthenticationManager`는 `DaoAuthenticationProvider`로 구현할까 했는데, 기본 생성자는 deprecate 되었다. 이제는 `UserDetailsService`를 파라미터로 받는 생성자를 사용해야 하며 `PasswordEncoder`&lt;span style=&quot;text-align: start;&quot;&gt;는 setter를 사용하는 것으로 바뀌었다.&lt;/span&gt; (이 내용을 좀 뒤늦게 찾아서 처음에는 일단 `UsernamePasswordAuthenticationProvider`를 대신 사용했다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 세션 기반 로그인 구현이 완료되었다!&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/web/context/SecurityContextRepository.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/web/context/SecurityContextRepository.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://okky.kr/questions/1530117&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://okky.kr/questions/1530117&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>DaoAuthenticationProvider</category>
      <category>SecurityContextRepository</category>
      <category>spring security 6.5.0</category>
      <category>세션 로그인</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/193</guid>
      <comments>https://backend-jaamong.tistory.com/193#entry193comment</comments>
      <pubDate>Sat, 5 Jul 2025 12:49:34 +0900</pubDate>
    </item>
    <item>
      <title>[SQLite] 타입 친화성(Type Affinity)</title>
      <link>https://backend-jaamong.tistory.com/192</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;평소에 SQLite는 테스트용으로만 사용하고 실제로는 잘 사용하지 않는데, 설치가 불필요한 로컬 RDB가 요구되는 프로젝트를 진행하게 되어 SQLite를 사용하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; padding: 2px 5px; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;타입 종류&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQLite는 대표적인 RDBMS인 MySQL이나 PostgreSQL 등과 달리 데이터 타입이 매우 적다. 다음은 SQLite에서 제공하는 storage class이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;TEXT&lt;/b&gt; : 데이터베이스 인코딩&lt;span style=&quot;text-align: left;&quot;&gt;(UTF-8, UTF-16BE or UTF-16LE)&lt;/span&gt;을 사용하여 저장된 텍스트 문자열&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;INTEGER&lt;/b&gt; : 값 크기에 따라 &lt;span style=&quot;text-align: left;&quot;&gt;0, 1, 2, 3, 4, 6, 8 바이트로 저장되는 정수(signed integer)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;REAL&lt;/b&gt; : 8 바이트 IEEE 부동 소수점 숫자로 저장되는 부동 소수점 값&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;BLOB&lt;/b&gt; : 입력 그대로 저장되는 데이터&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;NULL&lt;/b&gt; : NULL 값&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문자열을 저장할 때 문자열을 지원하는 다양한 타입을 고민할 필요 없이 `TEXT`에 저장하면 된다. `BOOLEAN` 타입은 `INTEGER` 타입에 0(FALSE) 또는 1(TRUE) 값으로 저장하면 된다. &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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt; JPA를 사용한다면 `Boolean` 타입 필드에 `@Column(columnDefinition=&quot;INTEGER&quot;)`을 적용해서 타입을 명시하자.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;날짜 및 시간&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위에서 보이는 것처럼 날짜(date)나 시간(time) 관련 타입은 따로 제공하지 않기 때문에 `TEXT`, `REAL` 또는 `INTEGER`로 저장해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;TEXT&lt;/b&gt; : &quot;YYYY-MM-DD HH:MM:SS.SSS&quot;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;REAL&lt;/b&gt; : 율리우스&amp;nbsp;일수로,&amp;nbsp;기원전&amp;nbsp;4714년&amp;nbsp;11월&amp;nbsp;24일&amp;nbsp;그리니치&amp;nbsp;정오&amp;nbsp;이후의&amp;nbsp;일수(예상&amp;nbsp;그레고리력&amp;nbsp;기준)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;INTEGER&lt;/b&gt; : 유닉스 시간으로, 1970년 1월 1일 00:00:00 UTC 이후의 초 수&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 저장된 데이터는&lt;/span&gt; &lt;a title=&quot;SQLite에서 제공하는 날짜 및 시간 함수들&quot; href=&quot;https://www.sqlite.org/lang_datefunc.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;내장된 날짜/시간 함수&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 사용해서 형식을 자유롭게 변환할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개인적으로 `LocalDateTime` 형식과 유사한 `TEXT` 타입을 선호한다. 위 포맷으로 저장하고 변환하는 유틸을 만들어두면 편리하다. 다만 포맷터가 있다 하더라도 `createdAt`, `modifiedAt` 등과 같이 날짜 시간 타입을 다룰 때 손이 더 가는 건 어쩔 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음은 이번에 프로젝트를 진행하면서 작성했던 코드로, 공통으로 사용하는 `createdAt`, `modifiedAt` 필드를 담고 있는 클래스이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750490882948&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@MappedSuperclass 
public abstract class BaseEntity {

    @Column(name = &quot;created_at&quot;, updatable = false)
    protected String createdAt; // 이 클래스를 상속받는 다른 클래스에서 접근하기 위해서 protected로 선언

    @Column(name = &quot;modified_at&quot;)
    private String modifiedAt;

    @PrePersist
    protected void onCreate() {
        createdAt = CommonUtils.localDateTimeToString(LocalDateTime.now());
        modifiedAt = CommonUtils.localDateTimeToString(LocalDateTime.now());
    }

    @PreUpdate
    protected void onUpdate() {
        modifiedAt = CommonUtils.localDateTimeToString(LocalDateTime.now());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처음에는 `LocalDateTime` 필드로 선언하고 `@Column` 애노테이션을 사용해서 `TEXT` 타입을 명시해줬는데 오히려 일이 복잡해져서 처음부터 `String` 타입으로 선언하고 따로 컬럼 삽입이나 변경 시 동작하는 메서드를 만들었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 `@PrePersist`, `@PreUpdate`를 사용하기 때문에 JPA auditing 기능은 필요하지 않다.&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/span&gt;&lt;/i&gt; SQLite에서 `DEFAULT CURRENT_TIMESTAMP`는 지원하지만, `ON UPDATE CURRENT_TIMESTAMP`는 지원하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; padding: 2px 5px; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;타입 친화성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SQLite는 다른 데이터베이스와의 호환성을 위해 타입 친화성을 지원한다. 이는 다음을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컬럼을 SQLite에는 없는 `FLOAT`나 `TIMESTAMP` 타입 등으로 선언할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 선언된 컬럼을 SQLite는 내부적으로 위에서 소개한 5가지 storage class 중 하나로 매핑한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 네이티브 하지 않은 타입으로 선언된 컬럼들도 SQLite에서 기술적으로 유효하다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 `Double` 타입 &lt;span style=&quot;text-align: start;&quot;&gt;필드에 `@Column(columnDefinition=&quot;REAL&quot;)`을 사용해서 타입을 명시하지 않아도 실행했을 때 오류가 발생하지 않는다. 실행 시 Hibernate가 만드는 쿼리를 보면 해당&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;컬럼이&lt;span&gt; &lt;/span&gt;&lt;/span&gt;`REAL` 타입으로 선언되지 않고, `Double` 타입으로 들어가는 것을 확인할 수 있다.&lt;/span&gt;&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;i&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;Hibernate community dialect 의존성을 추가해도 SQLite에 있는 타입으로 변환되지 않는다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이러한 타입 친화성 덕분에 오류가 나지는 않지만 그래도 컬럼을 명시하는 방향을 지향한다.&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;span style=&quot;color: #000000;&quot;&gt;  &lt;b&gt;참고&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sqlite.org/datatype3.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.sqlite.org/datatype3.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <category>hibernate</category>
      <category>SQLite</category>
      <category>type affinity</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/192</guid>
      <comments>https://backend-jaamong.tistory.com/192#entry192comment</comments>
      <pubDate>Sat, 21 Jun 2025 17:00:18 +0900</pubDate>
    </item>
    <item>
      <title>Render로 FastAPI 배포한 후기</title>
      <link>https://backend-jaamong.tistory.com/191</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;근래 FastAPI + PostgreSQL 조합으로 백엔드 개발을 진행하고 있다. 프론트엔드에서 API를 가져다 쓰려면 배포를 해야 하므로 처음에는 AWS의 EC2나 Lambda, Elastic Beanstalk을 고려했다. (고려하면서 좀 찾아봤는데 셋 다 목적이 달라서 어쩌다 보니 공부하게 된건 덤)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;백엔드 개발 규모가 그리 크지 않아서 AWS를 사용하는 건 부담스럽게 느껴졌다. 그렇게 다른 플랫폼을 찾다 보니&lt;/span&gt; &lt;a href=&quot;https://render.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Render&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 선택했다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실 전에 Java(SpringBoot)도 가능하다는 말을 어디선가 봐서 도전했다가 안 좋은 감정만 남았던 기억이 있는데, 사용 후기가 꽤 있는 Python은 뭔가 다를지도 몰라! 하는 기대감으로 도전했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Render는 여타 다른 클라우드 플랫폼이 그렇듯 여러 플랜과 인스턴스 타입을 지원한다. 그중에서 내가 사용 중인 건 Hobby 플랜의 무료(Free) 인스턴스이다. 해당 스펙이 규모가 작은 개발 단계에서 사용하기 적합했다. 선택한 다른 이유로는 빌트인 PostgreSQL을 지원하는 것과 GitHub로부터 자동 배포를 해준다는 점이었다. 월마다 무료 사용량으로 750시간이 주어지는 것도 충분했다. (자세한 사항은 &lt;a href=&quot;https://render.com/docs/free&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Render&lt;/a&gt;에서 확인)&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;883&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AH1zp/btsMFrCIm8V/6LeAkPOaj4wI0JAYXD7mF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AH1zp/btsMFrCIm8V/6LeAkPOaj4wI0JAYXD7mF1/img.png&quot; data-alt=&quot;Monthly usage limits - Free instance hours&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AH1zp/btsMFrCIm8V/6LeAkPOaj4wI0JAYXD7mF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAH1zp%2FbtsMFrCIm8V%2F6LeAkPOaj4wI0JAYXD7mF1%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;680&quot; height=&quot;323&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Monthly usage limits - Free instance hours&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;무료 인스턴스의 단점이라면 요청을 처리하는데 50초 이상 지연되거나, 15분 이상 요청이 들어오지 않으면 자동으로 서버가 내려간다는 정도다. 실제로 서비스할 때는 Starter 인스턴스(7$ / month)를 써보려고 한다.&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;1431&quot; data-origin-height=&quot;723&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tTJMx/btsMEBTTILA/mS7PV0VQzFbYrZCSIzbupk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tTJMx/btsMEBTTILA/mS7PV0VQzFbYrZCSIzbupk/img.png&quot; data-alt=&quot;Instance Types&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tTJMx/btsMEBTTILA/mS7PV0VQzFbYrZCSIzbupk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtTJMx%2FbtsMEBTTILA%2FmS7PV0VQzFbYrZCSIzbupk%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;680&quot; height=&quot;344&quot; data-origin-width=&quot;1431&quot; data-origin-height=&quot;723&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Instance Types&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;더불어 같이 제공해 주는 빌트인 PostgreSQL도 좋았다. 배포한 FastAPI 애플리케이션과의 연동이 매우 간단하고, pgAdmin을 통해서 외부에서도 접근할 수 있다. 설정만 잘하면 된다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;종합 후기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파이썬으로 백엔드 개발을 하고 있고 배포를 해야 한다면 Render도 고려할만한 것 같다.&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;/p&gt;</description>
      <category>Architecture &amp;amp; Tool</category>
      <category>fastapi</category>
      <category>postgresql</category>
      <category>render</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/191</guid>
      <comments>https://backend-jaamong.tistory.com/191#entry191comment</comments>
      <pubDate>Sat, 8 Mar 2025 16:03:21 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI 개발 환경 설정과 애플리케이션 시작하기</title>
      <link>https://backend-jaamong.tistory.com/190</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;보통 백엔드 개발 시 주로 스프링 부트를 사용하는데, 파이썬을 사용하는 AI 엔지니어와 협업할 때 호환성이 좋지 않았다. 그래서 AI 엔지니어와 협업할 때만 사용할, 스프링 부트를 대체할 파이썬 기반 백엔드 프레임워크 사용을 계속 고민해 왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;인증이나 주된 기능은 스프링 부트로 처리하되 AI 프로그램에서 넘어오는 데이터를 처리하고 API로 전달하는 용도로 쓰고자 했다. 따라서 비교적 무거운 Django를 제외하고 Flask와 FastAPI 중에서 고민했다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;이전에 사용해 본 Flask는 Java로 백엔드 개발하는 것과 비슷한 부분이 있어서 끌렸는데, FastAPI는 Flask와 동일하게 API나 마이크로서비스 개발 등에 적합하며 API 문서 자동화를 지원한다고 한다! API 문서 자동화 지원은 사용할 이유로 충분했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;환경 설정&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;⚠️파이썬이 먼저 설치되어 있어야 한다. 없다면 &lt;a href=&quot;https://www.python.org/downloads/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;로 가서 최신 버전을 설치하고 시스템 환경 변수에 추가하자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파이썬 설치가 완료되었다면 이제 FastAPI와 Uvicorn을 설치해야 한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;  Uvicorn은 FastAPI를 실행하는 가벼운 ASGI 서버이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FastAPI와 Uvicorn을 설치하기 위해 아래 명령어를 터미널에 입력하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738389826097&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install fastapi uvicorn&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설치가 완료되면 FastAPI 애플리케이션을 빌드할 수 있다. &amp;rarr; 환경 설정 끝!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;간단한 애플리케이션 만들기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선 FastAPI를 `import` 해야 한다. 이는 FastAPI 인스턴스 생성에 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738390173068&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from fastapi import FastAPI  # fastapi 모듈에서 FastAPI 클래스 import

app = FastAPI()  # FastAPI 클래스의 인스턴스 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`app`은 서버로 들어오는 모든 요청을 처리하는 메인 애플리케이션이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 Path operation(API)을 정의한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;/&quot; 경로로 요청이 들어온다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리하는 함수명은 `read_root`이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`&quot;hello&quot;`와 `&quot;world&quot;`를 담은 딕셔너리 데이터를 반환하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1738390366402&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from fastapi import FastAPI 

app = FastAPI()  


@app.get(&quot;/&quot;)
def read_root():
    return {&quot;hello&quot;, &quot;world&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 Uvicorn을 사용하여 서버를 실행하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738390501283&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;uvicorn main:app --reload&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`uvicorn main:app` :&amp;nbsp; 메인 앱이 Uvicorn에게 `main.py`안에 존재하는 `app` 인스턴스를 찾으라고 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`--reload` :&amp;nbsp; 코드에 변경사항에 생기면 서버가 자동으로 재시작된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJzHd6/btsL4EQyVlk/WEvHONWJcakKbabEvAZPt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJzHd6/btsL4EQyVlk/WEvHONWJcakKbabEvAZPt1/img.png&quot; data-alt=&quot;터미널에서 명령어 입력&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJzHd6/btsL4EQyVlk/WEvHONWJcakKbabEvAZPt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJzHd6%2FbtsL4EQyVlk%2FWEvHONWJcakKbabEvAZPt1%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;600&quot; height=&quot;131&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;터미널에서 명령어 입력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;191&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b72MQh/btsL3qMyHNL/wzOn478ypRyUKfwdKuJ82K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b72MQh/btsL3qMyHNL/wzOn478ypRyUKfwdKuJ82K/img.png&quot; data-alt=&quot;서버 실행 후 localhost:8080/ 접근 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b72MQh/btsL3qMyHNL/wzOn478ypRyUKfwdKuJ82K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb72MQh%2FbtsL3qMyHNL%2FwzOn478ypRyUKfwdKuJ82K%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;200&quot; height=&quot;132&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;191&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버 실행 후 localhost:8080/ 접근 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;문서 자동화&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위에서 언급했던 것과 같이 FastAPI는 문서 자동화 기능을 지원한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`/docs`으로 접근하면 Swagger 형태로, `/redoc`으로 접근하면 Redoc 형태로 API 문서를 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CDgOz/btsL3PL6M7K/dHKGzq69UdHcostc3y1KH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CDgOz/btsL3PL6M7K/dHKGzq69UdHcostc3y1KH1/img.png&quot; data-alt=&quot;/docs로 접근 시 Swagger 형태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CDgOz/btsL3PL6M7K/dHKGzq69UdHcostc3y1KH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCDgOz%2FbtsL3PL6M7K%2FdHKGzq69UdHcostc3y1KH1%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;600&quot; height=&quot;282&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;/docs로 접근 시 Swagger 형태&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxD4QX/btsL4NfAlsf/Gx9mTXiz22aycw07kfEKo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxD4QX/btsL4NfAlsf/Gx9mTXiz22aycw07kfEKo0/img.png&quot; data-alt=&quot;/redoc으로 접근 시 Redoc 형태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxD4QX/btsL4NfAlsf/Gx9mTXiz22aycw07kfEKo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxD4QX%2FbtsL4NfAlsf%2FGx9mTXiz22aycw07kfEKo0%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;600&quot; height=&quot;512&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;/redoc으로 접근 시 Redoc 형태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Python/FastAPI</category>
      <category>API 문서 자동화</category>
      <category>fastapi</category>
      <category>uvicorn</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/190</guid>
      <comments>https://backend-jaamong.tistory.com/190#entry190comment</comments>
      <pubDate>Sat, 8 Feb 2025 09:27:50 +0900</pubDate>
    </item>
    <item>
      <title>[AWS RDS] MySQL 파라미터 그룹 생성과 설정 - timezone, encoding</title>
      <link>https://backend-jaamong.tistory.com/189</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RDS MySQL를 사용하는 프로젝트에서 시간대가 맞지 않음을 알게 되었다. 타임존 설정을 까먹은 것...!&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 DB 인스턴스의 설정을 바꾸는 일이므로 DB 파라미터 그룹을 새로 생성하여 수정하기로 했다.&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;DB 파라미터 그룹&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB 파라미터 그룹은 하나 이상의 DB 인스턴스에 적용되는 구성 값의 모음이다. DB 파라미터 그룹을 지정하지 않고 DB 인스턴스를 만드는 경우 DB 인스턴스에서는 기본 DB 파라미터 그룹을 사용한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;기본 DB 파라미터 그룹 설정은 수정할 수 없으므로 새 파라미터 그룹을 생성해야 한다. 그리고 원하는 파라미터 설정을 변경하고, DB 인스턴스나 DB 클러스터를 수정하여 새로 생성한 파라미터 그룹을 연결하면 된다.&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;&lt;span style=&quot;color: #000000;&quot;&gt; AWS 콘솔로 들어가서 파라미터 그룹을 생성하자.&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;설정하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;파라미터 그룹 생성하기&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RDS 서비스의 오른쪽 메뉴탭에서 파라미터 그룹을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;367&quot; data-origin-height=&quot;795&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbQrPJ/btsLXT2OcID/I38NDzZYkdsEWcD4XugRr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbQrPJ/btsLXT2OcID/I38NDzZYkdsEWcD4XugRr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbQrPJ/btsLXT2OcID/I38NDzZYkdsEWcD4XugRr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbQrPJ%2FbtsLXT2OcID%2FI38NDzZYkdsEWcD4XugRr0%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;240&quot; height=&quot;520&quot; data-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;367&quot; data-origin-height=&quot;795&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;span style=&quot;color: #000000;&quot;&gt;클릭하면 아래와 같은 화면으로 이동한다. 여기에서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;파라미터 그룹 생성&lt;/b&gt;&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-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ds4nHV/btsLYtJj6rJ/Dg68rCTjrh3L7c1exxw9u0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ds4nHV/btsLYtJj6rJ/Dg68rCTjrh3L7c1exxw9u0/img.png&quot; data-alt=&quot;RDS &amp;amp;gt; 파라미터 그룹 &amp;amp;gt; 사용자 지정 파라미터 그룹&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ds4nHV/btsLYtJj6rJ/Dg68rCTjrh3L7c1exxw9u0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fds4nHV%2FbtsLYtJj6rJ%2FDg68rCTjrh3L7c1exxw9u0%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;600&quot; height=&quot;383&quot; data-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RDS &amp;gt; 파라미터 그룹 &amp;gt; 사용자 지정 파라미터 그룹&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클릭하면 아래와 같이 파라미터 그룹 생성 화면이 나온다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtCqt8/btsLYJkHK3I/S5ujvlnEPkEsMuqCkSc4B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtCqt8/btsLYJkHK3I/S5ujvlnEPkEsMuqCkSc4B0/img.png&quot; data-alt=&quot;RDS &amp;amp;gt; 파라미터 그룹 &amp;amp;gt; 파라미터 그룹 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtCqt8/btsLYJkHK3I/S5ujvlnEPkEsMuqCkSc4B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtCqt8%2FbtsLYJkHK3I%2FS5ujvlnEPkEsMuqCkSc4B0%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;600&quot; height=&quot;537&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RDS &amp;gt; 파라미터 그룹 &amp;gt; 파라미터 그룹 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파라미터 그룹 이름, 설명을 입력하자. 파라미터 그룹 패밀리는 엔진 유형을 선택하면 입력이 활성화된다. 네 개 옵션을 모두 선택하고 나면 아래 화면처럼 유형 옵션이 나타난다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;757&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AKiLF/btsLZk5Pj7L/OwBkOLryicL2RLB0nkvgS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AKiLF/btsLZk5Pj7L/OwBkOLryicL2RLB0nkvgS0/img.png&quot; data-alt=&quot;RDS &amp;amp;gt; 파라미터 그룹 &amp;amp;gt; 파라미터 그룹 생성 - 파라미터 그룹 세부 정보&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AKiLF/btsLZk5Pj7L/OwBkOLryicL2RLB0nkvgS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAKiLF%2FbtsLZk5Pj7L%2FOwBkOLryicL2RLB0nkvgS0%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;600&quot; height=&quot;541&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;757&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RDS &amp;gt; 파라미터 그룹 &amp;gt; 파라미터 그룹 생성 - 파라미터 그룹 세부 정보&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB 파라미터 그룹 유형을 선택하는 것인데, 파라미터 그룹을 적용할 DB 유형에 맞춰서 선택하면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;DB Cluster parameter group:&lt;/b&gt; 클러스터에 포함된 모든 인스턴스에 적용됨&amp;nbsp;&amp;rarr; 클러스터수준&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;DB parameter group:&amp;nbsp;&lt;/b&gt;개별 인스턴스에 적용 &amp;rarr; 인스턴스 수준&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다 설정하고 나면 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;생성&lt;/b&gt;&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-filename=&quot;edited_blob&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/de0KFw/btsL0n1HU5I/yeuJtfdLzhy9mUstPYKmLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/de0KFw/btsL0n1HU5I/yeuJtfdLzhy9mUstPYKmLk/img.png&quot; data-alt=&quot;RDS &amp;amp;gt; 파라미터 그룹&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/de0KFw/btsL0n1HU5I/yeuJtfdLzhy9mUstPYKmLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fde0KFw%2FbtsL0n1HU5I%2FyeuJtfdLzhy9mUstPYKmLk%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;600&quot; height=&quot;431&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RDS &amp;gt; 파라미터 그룹&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Timezone 설정하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성한 파라미터 그룹의 이름을 클릭하면 파라미터 설정을 변경할 수 있는 화면으로 이동한다. &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;편집&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;1882&quot; data-origin-height=&quot;785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2cC3z/btsLZXPQQS9/8Xh2ehYSrFb1M8O22VdVJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2cC3z/btsLZXPQQS9/8Xh2ehYSrFb1M8O22VdVJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2cC3z/btsLZXPQQS9/8Xh2ehYSrFb1M8O22VdVJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2cC3z%2FbtsLZXPQQS9%2F8Xh2ehYSrFb1M8O22VdVJk%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;600&quot; height=&quot;250&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1882&quot; data-origin-height=&quot;785&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;span style=&quot;color: #000000;&quot;&gt;편집을 클릭하면 수정 상태로 바뀐다. 검색창에서 `time_zone`을 입력하고, `값`을 `Asia/Seoul`로 입력한다. 그리고 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;변경 사항 저장&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;1883&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg82aC/btsLZ06MXsI/pQWAfV65xawQ3605BFYTb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg82aC/btsLZ06MXsI/pQWAfV65xawQ3605BFYTb1/img.png&quot; data-alt=&quot;RDS &amp;amp;gt; 파라미터 그룹 &amp;amp;gt; 파라미터 그룹 수정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg82aC/btsLZ06MXsI/pQWAfV65xawQ3605BFYTb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg82aC%2FbtsLZ06MXsI%2FpQWAfV65xawQ3605BFYTb1%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;600&quot; height=&quot;206&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1883&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RDS &amp;gt; 파라미터 그룹 &amp;gt; 파라미터 그룹 수정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Encoding 설정하기 (한글 사용 기준)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 &lt;b&gt;character set&lt;/b&gt;을 설정한다. 다시 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;편집&lt;/b&gt;&lt;/span&gt;을 클릭하고 검색창에서 `character_set`를 입력한다. 검색하여 나온 모든 항목의 값을 `utf8mb4`로 설정한다(이모지 입력이 필요 없다면 utf8로도 괜찮다). 설정 후 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;변경 사항 저장&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;773&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co7I7W/btsLZ7ZajH7/AkCQraodGAKuAuniBj6Nzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co7I7W/btsLZ7ZajH7/AkCQraodGAKuAuniBj6Nzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co7I7W/btsLZ7ZajH7/AkCQraodGAKuAuniBj6Nzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco7I7W%2FbtsLZ7ZajH7%2FAkCQraodGAKuAuniBj6Nzk%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;1616&quot; height=&quot;773&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;773&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;color: #000000;&quot;&gt;이번에는 &lt;b&gt;collation&lt;/b&gt;을 설정한다. `character set`이 글자 자체에 대한 모양과 인코딩에 대해 정의한다면, `collation`은 정해져 있는 인코딩을 기반으로 글자끼리 어떻게 비교할지 정의한 규칙이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동일한 문자에 대해 어떻게 비교할지, 정렬을 어떻게 할지, 문자열 함수의 결과는 어떻게 달라지는지, 검색 시 어떤 결과를 줄지 등 규칙이 다르게 정의되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;편집을 클릭하고, 검색창에서 `collation`을 입력한다. 결과로 나온 항목인 `collation_connection`, `collation_server`의 값을 `utf8mb4_general_ci`로 변경한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DPgEi/btsLZ8KvcUY/WRZ3RFoiJXXchCJIWn2IKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DPgEi/btsLZ8KvcUY/WRZ3RFoiJXXchCJIWn2IKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DPgEi/btsLZ8KvcUY/WRZ3RFoiJXXchCJIWn2IKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDPgEi%2FbtsLZ8KvcUY%2FWRZ3RFoiJXXchCJIWn2IKk%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;600&quot; height=&quot;241&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;utf8mb4 charset 기준으로 선택할 수 있는 collation은 아래 화면에서 볼 수 있다시피 꽤나 다양한데, `general_ci`로 선택한 이유가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/llSHF/btsL0qqBfFF/SYQ9yJmASrzTwmGNN5Rha1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/llSHF/btsL0qqBfFF/SYQ9yJmASrzTwmGNN5Rha1/img.png&quot; data-alt=&quot;utf8mb4 charset collation&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/llSHF/btsL0qqBfFF/SYQ9yJmASrzTwmGNN5Rha1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FllSHF%2FbtsL0qqBfFF%2FSYQ9yJmASrzTwmGNN5Rha1%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;500&quot; height=&quot;476&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;491&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;utf8mb4 charset collation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;collation은 검색 및 정렬뿐만 아니라 유니크 인덱스와 같은 다른 부분에서도 사용되는데 이때 한글 데이터가 있는 경우 주의해야 한다. 한글을 포함한 동아시아 언어 등을 서비스한다면 정확한 검색 처리를 위해 `utf8mb4_general_ci`로 설정해야 한다. 예시로 MySQL의 default collation인 `0900_ai_ci`는 한글의 자음과 모음, 일본어의 가타카나와 히라가나를 같은 문자열로 인식하여 처리한다. 따라서 한글 데이터 등을 처리한다면 꼭 `general_ci`로 설정하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 변경 사항을 저장하고, RDS 데이터베이스 화면으로 이동한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;DB 인스턴스와 파라미터 그룹 연결하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정하고자 하는 DB 인스턴스를 선택하고 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;수정&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qsQIY/btsLYFCPnty/sX3TG1THg26W87MdN1c43K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qsQIY/btsLYFCPnty/sX3TG1THg26W87MdN1c43K/img.png&quot; data-alt=&quot;RDS 데이터베이스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qsQIY/btsLYFCPnty/sX3TG1THg26W87MdN1c43K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqsQIY%2FbtsLYFCPnty%2FsX3TG1THg26W87MdN1c43K%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;600&quot; height=&quot;228&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;863&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RDS 데이터베이스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가 구성 &amp;gt; 데이터베이스 옵션에 DB 파라미터 그룹을 설정하는 부분이 있다. 방금 생성한 파라미터 그룹을 선택한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/exQSKV/btsLYuBwArD/BpCED4xxvSrOLe7Fp1YY81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/exQSKV/btsLYuBwArD/BpCED4xxvSrOLe7Fp1YY81/img.png&quot; data-alt=&quot;추가 구성 &amp;amp;gt; 데이터베이스 옵션 &amp;amp;gt; DB 파라미터 그룹&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/exQSKV/btsLYuBwArD/BpCED4xxvSrOLe7Fp1YY81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FexQSKV%2FbtsLYuBwArD%2FBpCED4xxvSrOLe7Fp1YY81%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;600&quot; height=&quot;236&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;추가 구성 &amp;gt; 데이터베이스 옵션 &amp;gt; DB 파라미터 그룹&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;계속을 클릭하고, 아래 화면의 수정 예약에서 원하는 옵션 클릭, &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;DB 인스턴스 수정&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EteKb/btsL0pebSBz/Jd5KjPq6ywd8ciIrSqj3b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EteKb/btsL0pebSBz/Jd5KjPq6ywd8ciIrSqj3b0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EteKb/btsL0pebSBz/Jd5KjPq6ywd8ciIrSqj3b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEteKb%2FbtsL0pebSBz%2FJd5KjPq6ywd8ciIrSqj3b0%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;600&quot; height=&quot;366&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt; 참고&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.naver.com/sory1008/223071678680&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.naver.com/sory1008/223071678680&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rastalion.dev/mysql-8-0-1-%EB%B2%84%EC%A0%84%EB%B6%80%ED%84%B0-%EC%B1%84%ED%83%9D%EB%90%9C-utf8mb4_0900_ai_ci%EC%9D%98-%ED%95%9C%EA%B8%80-%EC%82%AC%EC%9A%A9%EC%97%90-%EB%8C%80%ED%95%9C-%EB%AC%B8%EC%A0%9C%EC%A0%90/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://rastalion.dev/mysql-8-0-1-%EB%B2%84%EC%A0%84%EB%B6%80%ED%84%B0-%EC%B1%84%ED%83%9D%EB%90%9C-utf8mb4_0900_ai_ci%EC%9D%98-%ED%95%9C%EA%B8%80-%EC%82%AC%EC%9A%A9%EC%97%90-%EB%8C%80%ED%95%9C-%EB%AC%B8%EC%A0%9C%EC%A0%90/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.bespinglobal.com/post/db-cluster-parameter-group-%EA%B3%BC-db-instance-parameter-group-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B0%8F-%ED%99%9C%EC%9A%A9/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.bespinglobal.com/post/db-cluster-parameter-group-%EA%B3%BC-db-instance-parameter-group-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B0%8F-%ED%99%9C%EC%9A%A9/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>AWS RDS</category>
      <category>collation</category>
      <category>encoding</category>
      <category>timezone</category>
      <category>utf8mb4_general_ci</category>
      <category>파라미터 그룹</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/189</guid>
      <comments>https://backend-jaamong.tistory.com/189#entry189comment</comments>
      <pubDate>Fri, 31 Jan 2025 09:14:58 +0900</pubDate>
    </item>
    <item>
      <title>[Network] STOMP와 SockJS</title>
      <link>https://backend-jaamong.tistory.com/188</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring 환경에서 STOMP 프로토콜을 사용하여 일대일 실시간 채팅을 구현해야 하는 일이 있었는데, 잘 알지도 못하고 설정에 SockJS 활성화를 추가했다. 클라이언트인 Flutter는 `StompClient` 라이브러리와 `ws` 프로토콜로 서버에 연결 요청을 시도했으나 계속 실패했다. 개발 시간이 촉박해서 제대로 STOMP와 SockJS에 대해 공부하지 않았던 탓에 계속 오류를 뿜어댔다...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래는 Flutter에서 Spring 서버로 연결 시도 시 실패했던 Spring, Flutter 코드이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Boot 3.4.1&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1737182797038&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        ...
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(&quot;/stomp&quot;) // create connection - ws://domain:port/stomp
                .setAllowedOriginPatterns(&quot;*&quot;)
                .withSockJS();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`registerStompEndpoints`에서 `registry`에 체이닝(chaining)으로 `withSockJS()`를 추가한 부분이 포인트이다. 해당 함수를 추가하면 `SockJS` 사용이 활성화된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Flutter 3.5.4&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1737183045508&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ChatService {
  static const baseUrl = 'YOUR_BASE_URL'; // e.g., 'http://your-domain:port'
  late StompClient stompClient;
  final Function(String) onMessageReceived;
  final int userId;

  ChatService({required this.userId, required this.onMessageReceived}) {
    _initializeStompClient();
  }

  void _initializeStompClient() {
    stompClient = StompClient(
      config: StompConfig(  // 여기를 주목!
        url: '${baseUrl.replaceFirst('http', 'ws')}/stomp',
        onConnect: onConnect,
        onDisconnect: (f) =&amp;gt; print('Disconnected'),
        onWebSocketError: (dynamic error) =&amp;gt; print(error.toString()),
        stompConnectHeaders: {'userId': userId.toString()},
        webSocketConnectHeaders: {'userId': userId.toString()},
      ),
    );
  }
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`_initializeStompClient()`의 `config` 설정을 `StompConfig()`로 한 것과 `URL`의 `scheme`을 `http`에서 `ws`로 교체하여 서버에 연결을 시도하는 것이 주목해야 하는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 STOMP와 SockJS가 무엇인지 알아보고, 어떻게하면 연결이 성공하는지 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;STOMP와 SockJS&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;  &lt;b&gt;프레임, 패킷&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&lt;i&gt;https://ict-story.tistory.com/39&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;프레임(Frame)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프레임은 데이터와 제어정보를 합친 것으로, 다양한 &lt;b&gt;프로토콜들에 의해 교환되고 운반되는&lt;/b&gt; &lt;b&gt;데이터 단위&lt;/b&gt;를 뜻한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OSI 7계층 중 데이터링크 계층(Data Link layer, Layer 2)에서 정의된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적인 프레임 구성 형태&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;48&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FkIoW/btsLToTZomF/6IFE3lJzjV43bsQk5klKnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FkIoW/btsLToTZomF/6IFE3lJzjV43bsQk5klKnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FkIoW/btsLToTZomF/6IFE3lJzjV43bsQk5klKnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFkIoW%2FbtsLToTZomF%2F6IFE3lJzjV43bsQk5klKnk%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;342&quot; height=&quot;48&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;48&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;패킷(Packet)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;패킷은 데이터를 일정 크기로 자른 것으로, OSI 7 계층 중 네트워크 계층(Network layer, Layer 3)에서 정의되는 데이터 단위이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 외에도 OSI의 각 계층에서 주고받는 정보의 단위를 모두 패킷이라고 총칭하기도 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #ffc9af;&quot;&gt;&lt;b&gt;1. 기본 개념&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style3&quot;&gt; `&lt;b&gt;Frame&lt;/b&gt;`&amp;nbsp;&amp;harr; `&lt;b&gt;Framing&lt;/b&gt;`&lt;br /&gt;&lt;br /&gt;&lt;b&gt;SockJS framing&lt;/b&gt;은 클라이언트와 서버 간 전송되는 메세지를 다루고 캡슐화하기 위한 전체적인 메커니즘 또는 구조를 의미한다. 통신이 SockJS 프로토콜을 준수하는지 확인하여 다양한 전송 방법(웹소켓, HTTP Streaming, long polling, etc.) 간의 호환성을 지원한다.&lt;br /&gt;다음 문맥에서는 SockJS 프레임(개별 통신 단위)의 형식을 지정, 전송, 해석하는 방법을 제어하는 프로세스 또는 방법론을 의미한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;SockJS frame&lt;/b&gt;은 SockJS 프로토콜 내의 단일 통신 단위이다. 통신을 통해 전송된 실제 메세지나 컨트롤 패킷을 나타내며 다음의 것들이 포함되어 있을 수 있다; Initialization data, Heartbeat signals, Actual application data &lt;br /&gt;프레임은 일반적으로 SockJS의 특정 형식 규칙을 준수한다. 예를 들어, 페이로드를 JSON 형태로 캡슐화하거나 메시지 유형을 구별하기 위해 헤더를 추가하여 규칙을 준수한다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;SockJS framing&lt;/b&gt;은 메세지 패키징, 전송 및 해석을 위한 프로토콜 레벨의 프로세스를 설명하는 더 넓은 개념이고, &lt;b&gt;SockJS frame&lt;/b&gt;은 framing 규칙에 따라 형식이 지정되고 전송되는 메시지의 특정 인스턴스다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SockJS&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 웹소켓을 사용할 수 없을 때 대체 매커니즘(fallback mechanism)을 제공하는 웹소켓 에뮬레이션(emulation) &lt;b&gt;라이브러리&lt;/b&gt;이다. &lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 따라서 Internet Explorer와 같은 레거시 브라우저나 제한적인 네트워크 환경에서 신뢰할 수 있는 전송을 보장할 수 있다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt; Provided Fallback Mechanism Ex.&lt;/b&gt;&lt;/i&gt; XHR streaming, long polling, &amp;hellip;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 웹소켓으로 연결을 처음 시도하고, 실패하면 HTTP 기반의 전송으로 대체한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 클라이언트와 서버 간의 호환성을 위해 메세지에 &lt;b&gt;SockJS 관련 프레이밍(framing)&lt;/b&gt;을 추가한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;STOMP&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;웹소켓이나 SockJS와 같은 전송을 통한 구조화된 통신을 위한 메시지 &lt;b&gt;프로토콜&lt;/b&gt;이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 애플리케이션 계층&lt;/b&gt;에서 메세지 교환 규칙을 정의한다.&lt;/span&gt;&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;span style=&quot;color: #000000; background-color: #ffc9af;&quot;&gt;&lt;b&gt;2. SockJS와 STOMP가 함께 동작하는 방식&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; SockJS:&lt;/b&gt; 웹소켓으로 첫 연결을 시도하고, 필요한 경우 HTTP 기반의 전송으로 대체하는 전송 메커니즘을 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; STOMP:&lt;/b&gt; 구조화된 메세지 호환성을 제공하기 위해 SockJS 또는 웹소켓 위에서 동작한다.&lt;/span&gt;&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;span style=&quot;color: #000000; background-color: #ffc9af;&quot;&gt;&lt;b&gt;3. Configuration 기반 동작&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Case 1. Spring 서버에서 SockJS 활성화&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;예상되는 동작&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 클라이언트가 웹소켓이나 HTTP을 통해서 통신을 하는 것과 상관없이, 서버는 모든 통신에 대해서 SockJS 관련 프레이밍을 예상한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; SockJS는 웹소켓으로 통신을 시도하고 실패하면 HTTP 통신으로 대체한다. &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Flutter 클라이언트 시나리오&lt;/b&gt; &lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; SockJS를 활성화하고 `ws` 프로토콜을 사용하는 경우&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 결과:&lt;/b&gt; 프로토콜 미스매치로 인한 연결 실패&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;원인: &lt;/b&gt;SockJS 프레이밍을 예상했으나, 원시 웹소켓 핸드셰이크가 전송됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SockJS를 활성화하고 `http` 프로토콜을 사용하는 경우&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 성공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; SockJS는 전송 폴백을 관리하고 서버가 예상한 값에 맞춰 조정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SockJS를 비활성화하고 `ws` 또는 `http` 프로토콜을 사용하는 경우&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;결과:&lt;/span&gt; &lt;/b&gt;SockJS 프레이밍의 부재로 인한 실패&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;원&lt;span data-token-index=&quot;0&quot;&gt;인: &lt;/span&gt;&lt;/b&gt;서버에서 연결을 거절함 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qsTdg/btsLTaPhrME/mO0Se3RSXlhfWbKkACcHfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qsTdg/btsLTaPhrME/mO0Se3RSXlhfWbKkACcHfk/img.png&quot; data-alt=&quot;Spring 서버에서 SockJS 활성화 시 Flutter 클라이언트 시나리오&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qsTdg/btsLTaPhrME/mO0Se3RSXlhfWbKkACcHfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqsTdg%2FbtsLTaPhrME%2FmO0Se3RSXlhfWbKkACcHfk%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;600&quot; height=&quot;175&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring 서버에서 SockJS 활성화 시 Flutter 클라이언트 시나리오&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Case 2. Spring 서버에서 SockJS 비활성화&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;예상되는 동작&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 서버가 원시 웹소켓으로 동작한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; HTTP 전송과 같은 대체(fallback) 메커니즘을 지원하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Flutter 클라이언트 시나리오&lt;/b&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SockJS를 비활성화하고 `ws` 프로토콜을 사용하는 경우&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 성공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; 직접적인 WebSocket 연결과 서버가 기대한 값이 일치함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; SockJS를 활성화하고 `ws` 또는 `http` 프로토콜을 사용하는 경우&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 결과:&lt;/b&gt; 성공 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 원인: &lt;/b&gt;SockJS 프레이밍과 서버의 원시 웹소켓 구현이 호환되지 않음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SockJS를 비활성화하고 `http` 프로토콜을 사용하는 경우&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;결과:&lt;/span&gt; &lt;/b&gt;실패&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;원인:&lt;/span&gt; &lt;/b&gt;일반 HTTP는 원시 웹소켓 서버에서 지원되지 않음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cExFDn/btsLSnu5X57/rf5OJqyPkKIEwYlC2tN7l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cExFDn/btsLSnu5X57/rf5OJqyPkKIEwYlC2tN7l0/img.png&quot; data-alt=&quot;Spring 서버에서 SockJS 비활성화 Flutter 클라이언트 시나리오&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cExFDn/btsLSnu5X57/rf5OJqyPkKIEwYlC2tN7l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcExFDn%2FbtsLSnu5X57%2Frf5OJqyPkKIEwYlC2tN7l0%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;600&quot; height=&quot;153&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring 서버에서 SockJS 비활성화 Flutter 클라이언트 시나리오&lt;/figcaption&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;span style=&quot;color: #000000; background-color: #ffc9af;&quot;&gt;&lt;b&gt;4. 특정 Configuration의 성공 또는 실패 이유&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span&gt;✅&lt;/span&gt; &lt;b&gt;Flutter에서 SockJS 활성화 시 `ws` 연결이 실패하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; SockJS를 활성화한 클라이언트는 SockJS 프레이밍이 포함된 핸드셰이크를 전송한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 원시 `ws://` 요청은 SockJS 협상(negotiate)을 우회하여, SockJS를 활성화한 Spring 서버와의 프로토콜 미스매치를 발생시킨다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; ✅ &lt;b&gt;Flutter에서 SockJS 활성화 시, `http` 연결이 성공하는 이유 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; SockJS는 대체 매커니즘의 일부인 `http://`를 사용한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 클라이언트는 SockJS가 전송을 협상하고 서버와 조율할 수 있도록 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style3&quot;&gt;  위 문맥에서 &lt;b&gt;협상(negotiate)&lt;/b&gt;의 의미&lt;br /&gt;&lt;br /&gt;클라이언트와 서버 간의 통신에 가장 적합한 전송 방법을 결정하고 합의하는 과정&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Step 1. 최선의 옵션으로 시작하기 (웹소켓)&lt;/b&gt;&lt;br /&gt;SockJS는 첫 연결 시도를 웹소켓으로 한다. 이는 가장 효과적이고 선호되는 전송 방법이기 때문이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Step 2. 필요하면 대체하기&lt;br /&gt;&lt;/b&gt;웹소켓이 안된다면 SockJS는 `HTTP streaming`이나 `long polling`과 같은 전송 방법들을 시도한다.&lt;br /&gt;이때 웹소켓 연결 실패 원인에는 네트워크 제한, 오래된 브라우저, 프록시 등이 있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Step 3. 전송 방법 확정하기&lt;br /&gt;&lt;/b&gt;SockJS는 사용할 수 있는 최선의 방법을 결정하고 해당 방법을 통신에 사용하도록 클라이언트와 서버를 모두 조정한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 내용을 바탕으로 다시 설정을 해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1737203525135&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        ...
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(&quot;/stomp&quot;) // create connection - ws://domain:port/stomp
                .setAllowedOriginPatterns(&quot;*&quot;)
                .withSockJS();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처음과 동일하게 SockJS를 활성화시킨 코드이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Flutter&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1737203588930&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ChatService {
  static const baseUrl = 'YOUR_BASE_URL'; // e.g., 'http://your-domain:port'
  late StompClient stompClient;
  final Function(String) onMessageReceived;
  final int userId;

  ChatService({required this.userId, required this.onMessageReceived}) {
    _initializeStompClient();
  }

  void _initializeStompClient() {
    stompClient = StompClient(
      config: StompConfig.sockJS(  // sockJS 추가됨
        url: '$baseUrl/stomp',  //http 또는 https 사용
        onConnect: onConnect,
        onDisconnect: (f) =&amp;gt; print('Disconnected'),
        onWebSocketError: (dynamic error) =&amp;gt; print(error.toString()),
        stompConnectHeaders: {'userId': userId.toString()},
        webSocketConnectHeaders: {'userId': userId.toString()},
      ),
    );
  }
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처음 코드와 달리 `StompConfig`에 `sockJS`가 추가되었고, URL도 `ws`로 대체하는 것이 아닌 `http`를 그대로 사용하도록 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처음 &lt;span style=&quot;text-align: start;&quot;&gt;Flutter&lt;/span&gt; 코드처럼 `sockJS`를 사용하지 않을 거라면 Spring 설정에서 `withSockJS()`를 제거하고, Flutter URL의 scheme도 `ws`를 사용하면 된다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;연결 성공!  &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;</description>
      <category>개발 지식</category>
      <category>flutter</category>
      <category>frame</category>
      <category>framing</category>
      <category>sockjs</category>
      <category>spring</category>
      <category>stomp</category>
      <category>websocket</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/188</guid>
      <comments>https://backend-jaamong.tistory.com/188#entry188comment</comments>
      <pubDate>Fri, 24 Jan 2025 09:42:58 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 기본 문법 2</title>
      <link>https://backend-jaamong.tistory.com/187</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;if&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코틀린의 `if`문은 표현식(expression)으로, 아래와 같은 방식으로 변수에 값으로 할당될 수 있다. &lt;span style=&quot;color: #333333;&quot;&gt;(&lt;span style=&quot;text-align: start;&quot;&gt;삼항연산자를 풀어서 쓰는 느낌이다)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737006441557&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    val a: Int = 100
    val b: Int = 200
    val c: Int
    
    c = if (a &amp;gt;= b) {
        a
    } else {
        b
    }
    
    println(c)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`null` 체크는 다음과 같이 할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737008355360&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
	val a: Int? = null
    
	if (a == null) {
            println(&quot;null check true&quot;)  // print 
	} else {
            println(&quot;a = $a&quot;)  // a에 null이 아닌 값이 담긴 경우, a에 할당된 값이 출력됨. ex) a = 100
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;in&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`in` 연산자를 사용하여 변수에 할당된 값이 주어진 범위에 속하는지 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737008590669&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
	val a: Int? = 100
    
	if (a in arrayOf(100, 200, 300)) {
	    println(&quot;contained&quot;)  // print
	} else {
	    println(&quot;not contained&quot;)  // a에 할당된 값이 배열에 없는 값이면, not contained 가 출력된다.
	}

}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;when&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음 `if-else`문과 `when`문은 동일한 의미이다. &lt;span style=&quot;color: #333333;&quot;&gt;(swtich-case문 같기도...)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737008816987&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
	val a: Int? = 100
    
	when (a) { 
	    100 -&amp;gt; println(&quot;1&quot;)  // print
	    200 -&amp;gt; println(&quot;2&quot;)
	    300 -&amp;gt; println(&quot;3&quot;)
	    else -&amp;gt; println(&quot;4&quot;)
	}

	if (a == 100) {
	    println(&quot;1&quot;)  // print
	} else if (a == 200) {
	    println(&quot;2&quot;)
	} else if (a == 300) {
	    println(&quot;3&quot;)
	} else {
	    println(&quot;4&quot;)
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 `when`문에서 범위를 지정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737011790449&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
	val a: Int = 399
    
	when (a) {
   	    in 100..199 -&amp;gt; println(&quot;1&quot;)  // 100 &amp;lt;= a &amp;lt;= 199
  	    in 200..299 -&amp;gt; println(&quot;2&quot;)  // 200 &amp;lt;= a &amp;lt;= 299
 	    in 300..399 -&amp;gt; println(&quot;3&quot;)  // 300 &amp;lt;= a &amp;lt;= 399
  	    else -&amp;gt; println(&quot;4&quot;) 
	}

// 399가 출력됨

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로 nullable 한 변수를 대상으로 실행하면 다음과 같은 에러가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737011653546&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Incompatible types 'kotlin.Int?' and 'kotlin.ranges.IntRange'.&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;Class / Data class / Enum class&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1737027033349&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    val item1: Item1 = Item1(&quot;BOOK&quot;, 10_000)  // Item1 클래스의 인스턴스 생성
    println(&quot;Item1 name is ${item1.name}, price is ${item1.price}&quot;)
    println(item1)

    val item2: Item2 = Item2(&quot;BOOK&quot;, 10_000) // Item2 데이터 클래스의 인스턴스 생성
    println(&quot;Item2 name is ${item1.name}, price is ${item1.price}&quot;)
    println(item2)
}

// 기본적으로 생성자가 생성됨
class Item1(val name: String, val price: Int) 

// 기본적으로 getter, setter, Equal, hashCode, toString, Copy, Component, and 함수들이 생성됨
data class Item2(val name: String, val price: Int)

enum class Color {
    RED,
    GREEN,
    BLUE,
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1737027599292&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 위 코드 실행 결과
Item1 name is BOOK, price is 10000
Item1@133314b
Item2 name is BOOK, price is 10000
Item2(name=BOOK, price=10000)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Data class&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코틀린의 `&lt;span style=&quot;text-align: left;&quot;&gt;Data&lt;/span&gt; class`는 자바의 `Record`와 유사하게 사용할 수 있는 듯하다. &amp;rarr; &lt;b&gt;Ex.&lt;/b&gt; `DTO` 역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와같이 &lt;span style=&quot;text-align: left;&quot;&gt;`&lt;span style=&quot;text-align: left;&quot;&gt;Data&lt;/span&gt;&amp;nbsp;class`를 &lt;/span&gt;정의하고 변수를 var로 선언하면 `setter`가 자동으로 생성된다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`val`로 선언한 경우, 특성상 변수를 변경할 수 없기 때문에 `setter`가 생성되지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행결과를 보면 `Data class`는 클래스 정의 시 `toString`이 자동 생성되므로, 출력했을 때 초기화한 인스턴스 값이 나오는 것을 확인할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>class</category>
      <category>Data Class</category>
      <category>enum class</category>
      <category>if</category>
      <category>In</category>
      <category>when</category>
      <category>코틀린</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/187</guid>
      <comments>https://backend-jaamong.tistory.com/187#entry187comment</comments>
      <pubDate>Sun, 19 Jan 2025 13:43:27 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] 기본 문법 1</title>
      <link>https://backend-jaamong.tistory.com/186</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요즘 스프링 개발 환경에서도 코틀린을 사용하는 경향이 꽤 보인다. 그래서 코틀린에 관심이 생겨서 찾아본 기본 문법!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;변수 타입&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코틀린에는 `var`와 `val`라는 두 가지 형태의 변수가 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;`var`&lt;/b&gt;: 초기화 이후에도 값 변경이 가능한 &lt;b&gt;가변 변수&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;`val`&lt;/b&gt;: 초기화 이후에는 값을 변경할 수 없는 &lt;b&gt;불변 변수&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1736915957017&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    var a = 1  
    a = 2  // 가변 변수이므로 값 변경 가능
    val b = 1
    b = 2  // 불변 변수이므로 에러 발생

    var c: Int  // 타입은 명시했으나, 초기화 하지 않은 상태
    println(c)  // 초기화 하지 않은 상태이므로 에러 발생

    c = 1  // 초기화
    println(c)  // 1 출력
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;타입 추론(Type Inference)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;타입 추론이란 변수나 함수를 선언할 때 타입을 명시적으로 선언하지 않아도 자동으로 자료형을 추론해 주는 기능이다. 코틀린은 기본적으로 이러한 타입 추론을 지원하므로, 변수를 초기화할 때 명시적으로 타입을 입력하지 않아도 된다. 하지만 협업 시에는 가급적 타입을 명시하는 것이 좋다고 한다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736916293297&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    var a = 1  // int로 타입 추론
    var a: Int = 1 // 타입 명시
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;null과 ?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코틀린 변수는 기본적으로 non-null 타입으로, 위와 같이 선언하여 `null`을 할당하면 에러가 발생한다. `null`을 허용하려면 아래와 같이 선언해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736916764031&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    var str1: String? = null  // str1 변수에는 String이나 null이 들어올 수 있다
    val str2: String? = null
    var str3: String = null  // non-null 타입이므로 에러 발생

    var i: Int? = null // i 변수에는 Int나 null이 들어올 수 있다
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;Null Safe Operator&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; `?.`&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 문자열의 길이를 출력해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736917867321&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    var str: String? = &quot;test&quot;
    println(str.length)  // 에러 발생: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'kotlin.String?'.
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1737027970004&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 위 코드 실행 결과 (에러 발생)
Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'kotlin.String?'.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에러를 해석하면 `String?` 타입에서 `null`을 허용하려면 `?.`나 `!!.` 호출이 필요하다는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736918030522&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    var str: String? = &quot;test&quot;
    println(str?.length)  // 4
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`str` 변수와 `.` 사이에 `?`를 붙이면 성공적으로 실행이 된다. 정확히는 `?.`에 의해 성공한 것인데, 이는 `str`이 `null`이 아닌 경우에 `?.` 뒤에 있는 명령어를 실행할 수 있게 한다. 이때 쓰인 `?.`는 `null safe operator`라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736918372540&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    var str: String? = null
    println(str?.length)  // null 출력
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`?.` 앞에 위치한 `str` 변수의 값이 `null`이므로 뒤에 있는 `length` 명령어는 실행하지 않으며, `null`이 출력된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;Not Null Assertion Operator&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;`!!.`&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;방금 위에서 봤던 에러 문구 중에는 `!!.`도 있었는데, 이는 좌항이 절대 `null`이 아님을 보장한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736919320848&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    var str: String? = &quot;test&quot; 
    println(str!!.length)  // 4

    var str2: String? = null
    println(str2!!.length)  // str2가 non-null임을 보장 -&amp;gt; null에서 length를 구하는 것이므로 NullPointerException 에러 발생
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;Elvis Operator&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; `?:`&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엘비스 연산자는 좌항이 `null`인 경우 우항에 있는 명령어를 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736944871963&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    
    var str1: String? = null
    str1 ?: println(&quot;str is null&quot;)  // &quot;str is null&quot; 출력

    var str2: String? = &quot;not null&quot;
    str2 ?: println(&quot;str is null&quot;)  // 아무것도 출력되지 않는다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>elvis operator</category>
      <category>not null assertion operator</category>
      <category>null</category>
      <category>null safe operator</category>
      <category>val</category>
      <category>Var</category>
      <category>코틀린</category>
      <category>타입 추론</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/186</guid>
      <comments>https://backend-jaamong.tistory.com/186#entry186comment</comments>
      <pubDate>Sat, 18 Jan 2025 09:08:48 +0900</pubDate>
    </item>
    <item>
      <title>[AWS &amp;amp; Docker] Docker 컨테이너 로그를 AWS CloudWatch로 보내기</title>
      <link>https://backend-jaamong.tistory.com/185</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존에는 EC2 우분투 환경에서 운영되는 프로그램들의 로그를 AWS CloudWatch에 전송할 수 있도록, 우분투에서 CloudWatch Agent 패키지를 설치하고 관련 설정을 진행했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 EC2(마찬가지로 우분투 환경)에서 Docker 컨테이너로 띄운 프로그램의 로그를 AWS CloudWatch에 전송할 수 있도록 설정을 해보자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;관련 공식 자료는&amp;nbsp;&lt;a href=&quot;https://docs.docker.com/engine/logging/drivers/awslogs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 참고하자.&lt;/span&gt; &lt;span style=&quot;color: #666666;&quot;&gt;(딱히 좋은 글인 것 같지는 않다...)&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Credentials&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 상에서 운영되는 Docker 컨테이너의 로그를 AWS CloudWatch에 전송하기 위해서는 `awslogs` 로깅 드라이버를 사용해야 한다. 이를 위해 자격 증명(credentials)이 필요하며, 해당 자격 증명을 Docker 데몬에 전달해야 한다. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;자격 증명이 필요한 이유는 EC2에 CloudWatch에 접근할 수 있는 역할(Role)을 부여해야 하기 때문이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;제공할 수 있는 자격 증명은 다음과 같다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`AWS_ACCESS_KEY_ID`&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`AWS_SECRET_ACCESS_KEY`&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`AWS_SESSION_TOKEN` &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 AWS 공유 자격 증명 파일(root 사용자의&amp;nbsp; `~/.aws/credentials`)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또는 AWS EC2 인스턴스 상에서 Docker 데몬을 실행하고 있다면, &lt;a title=&quot;EC2 IAM instance profile&quot; href=&quot;https://docs.aws.amazon.com/managedservices/latest/userguide/defaults-instance-profile.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;EC2 인스턴스 프로필&lt;/a&gt;로 충분한 것 같다(이 부분은 정확히 어떻게 번역을 해야 할지 모르겠다).&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Instance Profile&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스 프로필은 IAM Role(역할)을 저장하는 컨테이너로, EC2 인스턴스를 시작하면 인스턴스 프로필을 읽어서 설정된 IAM Role을 작동시킨다. 예를 들어, A 리소스에 접근가능한 IAM Role을 갖고 있는 인스턴스 프로필이 있다고 하자. 해당 인스턴스 프로필을 EC2에 연결하면 A 리소스에 접근할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 이유로 Docker 데몬이 실행되고 있는 EC2에 인스턴스 프로필이 연결되어 있다면, 충분한 자격 증명이 되는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;IAM Role(역할)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM Role은 계정에서 생성할 수 있는 &lt;b&gt;특정 권한을 가진 임시 자격 증명&lt;/b&gt;이다. 권한은 AWS에서 제공하고 있는 클라우드 서비스와 그 서비스로 할 수 있는 작업, 행동에 관한 범위를 의미한다. IAM Role의 권한은 IAM Policy(정책)에 JSON 형식으로 정의된 문서로, 정책에 나열된 서비스와 작업에 의해 부여된다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM Role 생성&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 콘솔에 접속 후 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;IAM&lt;/b&gt;&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-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfrAgF/btsLDXpExDb/5VqLWBZxKpJvkGfkwa5BF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfrAgF/btsLDXpExDb/5VqLWBZxKpJvkGfkwa5BF1/img.png&quot; data-alt=&quot;그림 1. IAM 서비스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfrAgF/btsLDXpExDb/5VqLWBZxKpJvkGfkwa5BF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfrAgF%2FbtsLDXpExDb%2F5VqLWBZxKpJvkGfkwa5BF1%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;660&quot; height=&quot;210&quot; data-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 1. IAM 서비스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IAM 대시보드의 왼쪽 메뉴에서 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;액세스 관리 &amp;gt; 역할&lt;/b&gt;&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-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEpILW/btsLDG9wEFa/csBNOkd9WKQrOWJPEL0FNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEpILW/btsLDG9wEFa/csBNOkd9WKQrOWJPEL0FNK/img.png&quot; data-alt=&quot;그림 2. 액세스 관리 &amp;amp;gt; 역할&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEpILW/btsLDG9wEFa/csBNOkd9WKQrOWJPEL0FNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEpILW%2FbtsLDG9wEFa%2FcsBNOkd9WKQrOWJPEL0FNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;260&quot; height=&quot;423&quot; data-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 2. 액세스 관리 &amp;gt; 역할&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;역할 생성&lt;/b&gt;&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-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1915&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpG6u0/btsLDuhc8IJ/mCmaDf3YbkHvvbsCw4CXYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpG6u0/btsLDuhc8IJ/mCmaDf3YbkHvvbsCw4CXYK/img.png&quot; data-alt=&quot;그림 3. 역할 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpG6u0/btsLDuhc8IJ/mCmaDf3YbkHvvbsCw4CXYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpG6u0%2FbtsLDuhc8IJ%2FmCmaDf3YbkHvvbsCw4CXYK%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;660&quot; height=&quot;85&quot; data-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1915&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 3. 역할 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1단계 &lt;b&gt;신뢰할 수 있는 엔터티 선택&lt;/b&gt;은 생성할 IAM역할을 연결할 수 있는 서비스 또는 사용자 계정 등을 의미한다. 아래 &lt;b&gt;신뢰할 수 있는 엔터티 유형&lt;/b&gt; 중에서&lt;/span&gt; &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;AWS 서비스&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boSDEv/btsLDX4brQj/k5bbc7pK6wzMPw6e8r7o7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boSDEv/btsLDX4brQj/k5bbc7pK6wzMPw6e8r7o7k/img.png&quot; data-alt=&quot;그림 4. 1단계 - 신뢰할 수 있는 엔터티 선택1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boSDEv/btsLDX4brQj/k5bbc7pK6wzMPw6e8r7o7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboSDEv%2FbtsLDX4brQj%2Fk5bbc7pK6wzMPw6e8r7o7k%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;660&quot; height=&quot;278&quot; data-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 4. 1단계 - 신뢰할 수 있는 엔터티 선택1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;사용 사례 - 서비스 또는 사용 사례&lt;/b&gt;에서&lt;/span&gt; &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;EC2&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 선택하고, 선택 후 나오는 사용 사례에서&lt;/span&gt; &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;EC2&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 선택한다. 이후 하단의&lt;/span&gt; &lt;b&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;다음&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;757&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpnJeN/btsLE2X3KwB/K81OTpV0DMHfYBMlrGYQ71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpnJeN/btsLE2X3KwB/K81OTpV0DMHfYBMlrGYQ71/img.png&quot; data-alt=&quot;그림 5. 1단계 - 신뢰할 수 있는 엔터티 선택2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpnJeN/btsLE2X3KwB/K81OTpV0DMHfYBMlrGYQ71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpnJeN%2FbtsLE2X3KwB%2FK81OTpV0DMHfYBMlrGYQ71%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;660&quot; height=&quot;264&quot; data-filename=&quot;edited_edited_edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;757&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 5. 1단계 - 신뢰할 수 있는 엔터티 선택2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음을 클릭하면 2단계 화면으로 넘어간다. 2단계에서는 IAM Role에 IAM Policy를 추가하여 권한을 부여한다. Docker 공식 문서에서는 `logs:CreateLogStream`과 `logs:PutLogEvents` 정책을 적용해야 한다고 한다. 아래 화면에서 필터를 걸어 검색하니 나오지 않아서, 우선 여기서는 정책을 추가하지 않고&lt;/span&gt; &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;으로 넘어갔다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;807&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/df0kp0/btsLC4XmalM/YSmOuU1jsyx4NmZsZDZANk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/df0kp0/btsLC4XmalM/YSmOuU1jsyx4NmZsZDZANk/img.png&quot; data-alt=&quot;그림 6. 2단계 - 권한 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/df0kp0/btsLC4XmalM/YSmOuU1jsyx4NmZsZDZANk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdf0kp0%2FbtsLC4XmalM%2FYSmOuU1jsyx4NmZsZDZANk%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;660&quot; height=&quot;281&quot; data-filename=&quot;edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;807&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 6. 2단계 - 권한 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음으로 넘어가면 3단계가 나온다. 여기에서는 IAM 역할에 이름을 지정하고 설명과 태그를 추가할 수 있다. 이름은 해당 역할이 어떤 목적으로 생성되었는지 설명할 수 있도록 짓는다. 이후 아래로 스크롤을 내려 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;역할 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBDg15/btsLDVk5MlJ/tgegxjhThzKDmacGAgpYn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBDg15/btsLDVk5MlJ/tgegxjhThzKDmacGAgpYn0/img.png&quot; data-alt=&quot;그림 7. 3단계 - 이름 지정, 검토 및 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBDg15/btsLDVk5MlJ/tgegxjhThzKDmacGAgpYn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBDg15%2FbtsLDVk5MlJ%2FtgegxjhThzKDmacGAgpYn0%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;660&quot; height=&quot;280&quot; data-filename=&quot;edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 7. 3단계 - 이름 지정, 검토 및 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 생성한 역할을 클릭하고, 아래 &lt;b&gt;권한 정책&lt;/b&gt;에서 &lt;b&gt;권한 추가 &amp;gt; 인라인 정책 생성&lt;/b&gt;을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1887&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T3Isn/btsLC3RDouP/8tM5FO46FzjpmhVLsaaKx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T3Isn/btsLC3RDouP/8tM5FO46FzjpmhVLsaaKx1/img.png&quot; data-alt=&quot;그림 8. 권한 추가 &amp;amp;gt; 인라인 정책 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T3Isn/btsLC3RDouP/8tM5FO46FzjpmhVLsaaKx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT3Isn%2FbtsLC3RDouP%2F8tM5FO46FzjpmhVLsaaKx1%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;660&quot; height=&quot;282&quot; data-filename=&quot;edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1887&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 8. 권한 추가 &amp;gt; 인라인 정책 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래와 같이 권한 지정 화면이 나오면, JSON 형식으로 정책을 편집할 수 있도록 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;JSON&lt;/b&gt;&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-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7QjV0/btsLDY9Xrj4/USA0KC06DbmIYX0mhxHCu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7QjV0/btsLDY9Xrj4/USA0KC06DbmIYX0mhxHCu0/img.png&quot; data-alt=&quot;그림 9. 권한 지정 - 정책 편집&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7QjV0/btsLDY9Xrj4/USA0KC06DbmIYX0mhxHCu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7QjV0%2FbtsLDY9Xrj4%2FUSA0KC06DbmIYX0mhxHCu0%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;660&quot; height=&quot;281&quot; data-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 9. 권한 지정 - 정책 편집&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 아래와 같이 &lt;b&gt;Action&lt;/b&gt;에 Docker 공식 문서에서 지정한 정책을 추가한다. 이후 스크롤을 내려 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&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-filename=&quot;edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sNkIM/btsLDmwC0EW/pXVun597qG7DUKVhTCDQqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sNkIM/btsLDmwC0EW/pXVun597qG7DUKVhTCDQqk/img.png&quot; data-alt=&quot;그림 10. Action&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sNkIM/btsLDmwC0EW/pXVun597qG7DUKVhTCDQqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsNkIM%2FbtsLDmwC0EW%2FpXVun597qG7DUKVhTCDQqk%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;660&quot; height=&quot;279&quot; data-filename=&quot;edited_edited_edited_edited_blob&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 10. Action&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음을 클릭하면 방금 정의한 정책을 검토하는 화면이 나온다. 틀린 부분이 없다면 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;변경 사항 저장&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;1917&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blZZxJ/btsLErD0e9Q/Lwqce7Bl5Eb0s7F9l4f1ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blZZxJ/btsLErD0e9Q/Lwqce7Bl5Eb0s7F9l4f1ak/img.png&quot; data-alt=&quot;그림 11. 검토 및 저장&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blZZxJ/btsLErD0e9Q/Lwqce7Bl5Eb0s7F9l4f1ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblZZxJ%2FbtsLErD0e9Q%2FLwqce7Bl5Eb0s7F9l4f1ak%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;660&quot; height=&quot;171&quot; data-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;1917&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 11. 검토 및 저장&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 생성한 역할을 Docker 데몬이 실행되고 있는 EC2에 연결해야 한다. EC2 서비스로 이동한다. 역할을 연결할 EC2를 선택하고 &lt;b&gt;작업 &amp;gt; 보안 &amp;gt; IAM 역할 수정&lt;/b&gt;을 클릭한다.&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-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;1897&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tyK9Q/btsLDOT1nXk/rsijRZkW9dP65FPrwr9dcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tyK9Q/btsLDOT1nXk/rsijRZkW9dP65FPrwr9dcK/img.png&quot; data-alt=&quot;그림 12. 작업 &amp;amp;gt; 보안 &amp;amp;gt; IAM 역할 수정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tyK9Q/btsLDOT1nXk/rsijRZkW9dP65FPrwr9dcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtyK9Q%2FbtsLDOT1nXk%2FrsijRZkW9dP65FPrwr9dcK%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;1897&quot; height=&quot;416&quot; data-filename=&quot;edited_edited_edited_blob&quot; data-origin-width=&quot;1897&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 12. 작업 &amp;gt; 보안 &amp;gt; IAM 역할 수정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 화면에서 방금 생성한 IAM 역할을 선택하고 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;IAM 역할 업데이트&lt;/b&gt;&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-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baQv90/btsLE3ilqW9/IJf0yfMpNrVeQrLwyzTyn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baQv90/btsLE3ilqW9/IJf0yfMpNrVeQrLwyzTyn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baQv90/btsLE3ilqW9/IJf0yfMpNrVeQrLwyzTyn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaQv90%2FbtsLE3ilqW9%2FIJf0yfMpNrVeQrLwyzTyn0%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;660&quot; height=&quot;377&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;521&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;color: #000000;&quot;&gt;지금까지는 로그를 AWS CloudWatch로 보내는 역할과 정책을 설정했다. 이제 받은 로그를 보관할 공간을 생성하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CloudWatch&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudWatch로 이동하여, 왼쪽 메뉴에서 &lt;b&gt;로그 &amp;gt; 로그 그룹&lt;/b&gt;을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yi1ik/btsLDtP6RoN/zyuuirKHjyUkSsa8gabMuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yi1ik/btsLDtP6RoN/zyuuirKHjyUkSsa8gabMuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yi1ik/btsLDtP6RoN/zyuuirKHjyUkSsa8gabMuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyi1ik%2FbtsLDtP6RoN%2FzyuuirKHjyUkSsa8gabMuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;260&quot; height=&quot;504&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;615&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;span style=&quot;color: #000000;&quot;&gt;로그 그룹 화면에서 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;로그 그룹 생성&lt;/b&gt;&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-filename=&quot;edited_blob&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qCexy/btsLDuuJFqx/vTh0CiDjdJpdiCUUp3W5C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qCexy/btsLDuuJFqx/vTh0CiDjdJpdiCUUp3W5C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qCexy/btsLDuuJFqx/vTh0CiDjdJpdiCUUp3W5C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqCexy%2FbtsLDuuJFqx%2FvTh0CiDjdJpdiCUUp3W5C1%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;660&quot; height=&quot;146&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;208&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;span style=&quot;color: #000000;&quot;&gt;아래 화면에서 &lt;b&gt;로그 그룹 이름&lt;/b&gt;을 설정하고, &lt;b&gt;보존 설정&lt;/b&gt;은 원하는 대로 설정하면 된다. 다만 저장된 로그 용량이 크면 요금이 청구될 수 있다. 이 부분은 정확하지 않으니, 찾아보고 적절한 기간을 설정하자. 이름과 보존 설정 이후 스크롤을 내려 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;생성&lt;/b&gt;&lt;/span&gt; 버튼을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/besBV0/btsLCp8HvIk/UCZLKTtvF80AGkd3kyluz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/besBV0/btsLCp8HvIk/UCZLKTtvF80AGkd3kyluz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/besBV0/btsLCp8HvIk/UCZLKTtvF80AGkd3kyluz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbesBV0%2FbtsLCp8HvIk%2FUCZLKTtvF80AGkd3kyluz0%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;660&quot; height=&quot;507&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;717&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;span style=&quot;color: #000000;&quot;&gt;방금 생성한 로그 그룹을 클릭하고 아래를 보면 &lt;b&gt;로그 스트림 &lt;/b&gt;탭이 있다. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;로그 스트림 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;1897&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/or3HL/btsLFiTS8MI/Gvavz1OB3MmrRSujtobXYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/or3HL/btsLFiTS8MI/Gvavz1OB3MmrRSujtobXYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/or3HL/btsLFiTS8MI/Gvavz1OB3MmrRSujtobXYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2For3HL%2FbtsLFiTS8MI%2FGvavz1OB3MmrRSujtobXYK%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;660&quot; height=&quot;261&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;1897&quot; data-origin-height=&quot;750&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;span style=&quot;color: #000000;&quot;&gt;그룹 내에서 각 도커 컨테이너 또는 프로그램 별 로그를 식별할 수 있는 이름을 지정하고 &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;Create&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bdd6F/btsLE062nr7/YiT5BJOB1oPdrsIp0MZLrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bdd6F/btsLE062nr7/YiT5BJOB1oPdrsIp0MZLrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bdd6F/btsLE062nr7/YiT5BJOB1oPdrsIp0MZLrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBdd6F%2FbtsLE062nr7%2FYiT5BJOB1oPdrsIp0MZLrK%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;660&quot; height=&quot;221&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;262&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;span style=&quot;color: #000000;&quot;&gt;이제 AWS 설정은 완료되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Docker&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2 인스턴스 상에서 실행되는 Docker 컨테이너의 docker-compose.yml에 아래와 같이 내용을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1735975806772&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'

services:
  app:
    ...
    logging:
      driver: awslogs
      options:
        awslogs-group: &quot;CloudWatch에서 설정한 로그 그룹 이름&quot;
        awslogs-region: &quot;EC2 인스턴스가 존재하는 리전&quot;
        awslogs-stream: &quot;로그 그룹 내에서 생성한 로그 스트림 이름&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같이 설정하고 컨테이너를 다시 빌드하면 된다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 5~10분 정도 기다리고, AWS CloudWatch에서 `awslogs-group`에 입력한 로그 그룹 &amp;gt; `awslogs-stream`에 입력한 로그 스트림으로 이동하면 컨테이너 로그를 확인할 수 있다.&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;&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 참고&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/engine/logging/drivers/awslogs/#credentials&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.docker.com/engine/logging/drivers/awslogs/#credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devnm.tistory.com/8&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devnm.tistory.com/8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jibinary.tistory.com/389&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jibinary.tistory.com/389&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>AWS</category>
      <category>awslogs</category>
      <category>CloudWatch</category>
      <category>Docker</category>
      <category>docker-compose</category>
      <category>IAM Policy</category>
      <category>iam role</category>
      <category>instance profile</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/185</guid>
      <comments>https://backend-jaamong.tistory.com/185#entry185comment</comments>
      <pubDate>Sat, 4 Jan 2025 16:39:33 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] OAuth2 카카오 로그인 구현 with JWT</title>
      <link>https://backend-jaamong.tistory.com/184</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;⚠️이 포스트는 지식 공유보다는 본인이 나중에 다시 보기 위해 작성하는 기록용입니다...&lt;/i&gt;&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;build.gradle&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1735447902007&quot; class=&quot;groovy&quot; data-ke-language=&quot;groovy&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1735448031428&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  security.oauth2:
    client.registration:
      kakao:
        client-name: Kakao
        client-id: ${CLIENT_ID}
        client-secret: ${CLIENT_SECRET}
        client-authentication-method: client_secret_post
        redirect-uri: ${REDIRECT_URI}
        authorization-grant-type: authorization_code
        scope:
          - account_email
    client.provider:
      kakao:
        authorization-uri: https://kauth.kakao.com/oauth/authorize
        token-uri: https://kauth.kakao.com/oauth/token
        user-info-uri: https://kapi.kakao.com/v2/user/me
        user-name-attribute: id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security에서 제공하는 OAuth2 기능을 사용한다면 yml 또는 properties 파일에 OAuth2 클라이언트, 서버 정보를 작성해야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;`spring.security.oauth2.client.registration.{registrationId}`&lt;/li&gt;
&lt;li&gt;`spring.security.oauth2.client.provider.{registrationId}`&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSaFH1/btsLyXqVlSB/daHtviJitlHgxBysIy5YK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSaFH1/btsLyXqVlSB/daHtviJitlHgxBysIy5YK0/img.png&quot; data-alt=&quot;Getting Started ❘ Spring Boot and OAuth2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSaFH1/btsLyXqVlSB/daHtviJitlHgxBysIy5YK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSaFH1%2FbtsLyXqVlSB%2FdaHtviJitlHgxBysIy5YK0%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;670&quot; height=&quot;517&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;707&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Getting Started ❘ Spring Boot and OAuth2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SecurityConfig&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1735448482893&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableMethodSecurity
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final String[] PERMIT_ALL_URL = {
            &quot;/favicon.ico/**&quot;,
            &quot;/h2-console/**&quot;,
            &quot;/api/health/**&quot;,
            &quot;/api/users/login&quot;,
            &quot;/api/users/access-token&quot;,
            &quot;/oauth2/authorization/kakao&quot;,
    };

    private final CorsConfig corsConfig;

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final AccessDeniedHandler accessDeniedHandler;

    private final OAuth2UserService&amp;lt;OAuth2UserRequest, OAuth2User&amp;gt; customOAuth2UserService;
    private final AuthenticationSuccessHandler customOAuth2SuccessHandler;
    private final AuthenticationFailureHandler customAuthExceptionHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .cors(cors -&amp;gt; cors.configurationSource(corsConfig.corsConfigurationSource()))
                .headers(headers -&amp;gt; headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
                .sessionManagement(session -&amp;gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(req -&amp;gt; req
                        .requestMatchers(PERMIT_ALL_URL).permitAll()
                        .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(e -&amp;gt; e
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler)
                )
                .oauth2Login(config -&amp;gt; config
                        .userInfoEndpoint(endpointConfig -&amp;gt; endpointConfig
                                .userService(customOAuth2UserService))
                        .successHandler(customOAuth2SuccessHandler)
                        .failureHandler(customAuthExceptionHandler)
                );

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;별도로 설정하지 않는 한, 클라이언트의&amp;nbsp;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;카카오 로그인 요청은&lt;/span&gt;&amp;nbsp; `/oauth2/authorization/kakao` 경로로 들어온다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;The `OAuth2AuthorizationRequestRedirectFilter` uses an `OAuth2AuthorizationRequestResolver` to resolve an `OAuth2AuthorizationRequest` and initiate the Authorization Code grant flow by redirecting the end-user&amp;rsquo;s user-agent to the Authorization Server&amp;rsquo;s Authorization Endpoint.&lt;br /&gt;&lt;br /&gt;The primary role of the `OAuth2AuthorizationRequestResolver` is to resolve an `OAuth2AuthorizationRequest` from the provided web request. The default implementation `DefaultOAuth2AuthorizationRequestResolver` matches on the (default) path `&lt;b&gt;/oauth2/authorization/{registrationId}`&lt;/b&gt;, extracting the registrationId, and using it to build the `OAuth2AuthorizationRequest` for the associated ClientRegistration.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;http://The%20OAuth2AuthorizationRequestRedirectFilter uses an OAuth2AuthorizationRequestResolver to resolve an OAuth2AuthorizationRequest and initiate the Authorization Code grant flow by redirecting the end-user&amp;rsquo;s user-agent to the Authorization Server&amp;rsquo;s Authorization Endpoint.  The primary role of the OAuth2AuthorizationRequestResolver is to resolve an OAuth2AuthorizationRequest from the provided web request. The default implementation DefaultOAuth2AuthorizationRequestResolver matches on the (default) path /oauth2/authorization/{registrationId}, extracting the registrationId, and using it to build the OAuth2AuthorizationRequest for the associated ClientRegistration.&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;i&gt;Initiating the Authorization Request, Authorization Grant Support, Spring Docs&lt;/i&gt;&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CustomOAuth2FailureHandler&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1735451101043&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        log.info(&quot;[CustomOAuth2SuccessHandler.onAuthenticationFailure] oauth2 authentication fail&quot;);
        configureResponse(response);
        objectMapper.writeValue(response.getWriter(), makeResponseBody(exception));
    }

    private ResponseDto&amp;lt;String&amp;gt; makeResponseBody(AuthenticationException exception) {
        String message = &quot;카카오 로그인에 실패했습니다. (exception = &quot; + exception + &quot;) &quot;;
        return new ResponseDto&amp;lt;&amp;gt;(HttpStatus.UNAUTHORIZED, message);
    }

    private void configureResponse(HttpServletResponse response) {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인 요청이 들어왔을 때 인증에 실패하면 `AuthenticationFailureHandler` 필터가 호출된다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 필터는 `SecurityFilterChain`에 등록된 `OAuth2Login` 용도의 `userService`보다 앞에 위치한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기에서 발생한 에러는 `AuthenticationEntryPoint`에서 처리되며, 이는 `@RestControllerAdvice`가 적용된 클래스에서 처리할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CustomOAuth2UserService &amp;amp; OAuth2CustomUser&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1735449698217&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        try {
            log.info(&quot;[CustomOAuth2UserService.loadUser] receive oauth2 login request&quot;);

            // obtain user info from the provider using access token
            Map&amp;lt;String, Object&amp;gt; originAttributes = super.loadUser(userRequest).getAttributes();

            // OAuth2 서비스 id (kakao, ...)
            String registrationId = userRequest.getClientRegistration().getRegistrationId();

            // Provider 에서 제공하는 attribute 추출
            OAuthAttributes attributes = OAuthAttributes.of(registrationId, originAttributes);
            String email = attributes.email();

            // 회원 가입 또는 로그인
            registerIfNewUser(email);

            List&amp;lt;SimpleGrantedAuthority&amp;gt; authorities = UserRole.CUSTOMER.getAuthorities();
            return new OAuth2CustomUser(authorities, originAttributes, &quot;id&quot;, email);
        } catch (RuntimeException e) {
            log.error(&quot;[CustomOAuth2UserService.loadUser] exception = &quot;, e);
            throw new OAuth2AuthenticationException(e.getMessage());
        }
    }

    private void registerIfNewUser(String email) {
        Optional&amp;lt;UserEntity&amp;gt; optionalUser = userRepository.findByUsername(email);

        // 이미 등록된 사용자면 패스
        if (optionalUser.isPresent()) return;

        // 등록된 사용자가 아니면 저장
        UserEntity user = UserEntity.builder()
                .username(email)
                .userRole(UserRole.CUSTOMER)
                .build();

        userRepository.save(user);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1735450141384&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record OAuth2CustomUser(
        List&amp;lt;SimpleGrantedAuthority&amp;gt; authorities,
        Map&amp;lt;String, Object&amp;gt; attributes,
        String registrationId,
        String email) implements OAuth2User, Serializable {

    @Override
    public Map&amp;lt;String, Object&amp;gt; getAttributes() {
        return this.attributes;
    }

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getName() {
        return this.registrationId;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`DefaultOAuth2UserService`는 `OAuth2UserService`의 구현체로 표준 OAuth2.0 공급자를 지원한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인증을 진행하기 위한 인증 서버(카카오)와 사용자를 저장하기 위한 데이터베이스를 호출하기 위해 `OAuth2UserService`를 구현한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구현체에서는 사용자가 정의한 `User` 객체를 상속하고 `OAuth2User`를 구현한 것을 반환해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;How to Add a Local User Database&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Many applications need to hold data about their users locally, even if authentication is delegated to an external provider. We don&amp;rsquo;t show the code here, but it is easy to do in two steps.&lt;br /&gt;&lt;br /&gt;1. Choose a backend for your database, and set up some repositories (using Spring Data, say) for a custom `User` object that suits your needs and can be populated, fully or partially, from external authentication.&lt;br /&gt;&lt;br /&gt;2. &lt;b&gt;Implement and expose `OAuth2UserService` to call the Authorization Server as well as your database.&lt;/b&gt; Your implementation can delegate to the default implementation, which will do the heavy lifting of calling the Authorization Server. &lt;b&gt;Your implementation should return something that extends your custom `User` object and implements `OAuth2User`.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Hint: add a field in the `User` object to link to a unique identifier in the external provider (not the user&amp;rsquo;s name, but something that&amp;rsquo;s unique to the account in the external provider).&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://spring.io/guides/tutorials/spring-boot-oauth2#header&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;i&gt;Getting Started | Spring Boot and OAuth2&lt;/i&gt;&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CustomOAuth2SuccessHandler&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1735451640676&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler {

    @Value(&quot;${oauth2.login.redirect-url}&quot;)
    private String redirectURL;

    private final UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        log.info(&quot;[CustomOAuth2SuccessHandler.onAuthenticationSuccess] oauth2 authentication success&quot;);

        OAuth2CustomUser oAuth2User = (OAuth2CustomUser) authentication.getPrincipal();
        String email = oAuth2User.email();

        // issue a refresh token
        UserEntity user = userService.getByUsername(email);
        ResponseCookie responseCookie = userService.authenticate(user);

        // configure HttpServletResponse
        log.info(&quot;[CustomOAuth2SuccessHandler.onAuthenticationSuccess] Redirect URL = {}&quot;, redirectURL);
        configureResponse(response, responseCookie);
        response.sendRedirect(redirectURL);
    }

    private void configureResponse(HttpServletResponse response, ResponseCookie cookie) {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setStatus(HttpStatus.OK.value());
        response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`CustomOAuth2UserService`가 성공하면 `SuccessHandler`로 넘어오게 된다.&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인에 성공했으므로 리프레시 토큰을 발급한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;발급한 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;리프레시 토큰&lt;/span&gt;은 쿠키에 담아서 전달한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿠키 설정 시 &lt;b&gt;CORS&lt;/b&gt;를 고려하여 주의해야 할 점이 있다.&lt;/span&gt;
&lt;pre id=&quot;code_1735453205862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ResponseCookie.from(&quot;refresh_token&quot;, refreshToken)
    .httpOnly(true)
    .secure(true) 
    .maxAge(600) // 10분
    .sameSite(Cookie.SameSite.NONE.attributeValue())
    .path(&quot;/&quot;)
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;백엔드 서버에 &lt;b&gt;HTTPS&lt;/b&gt;를 적용했다면 `secure`와 `sameSite`를 위 코드처럼 설정하고, &lt;b&gt;HTTP&lt;/b&gt;면 `secure`를 `false`로, `sameSite`를 `LAX`로 설정해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`CustomOAuth2UserService`에서 반환했던 `OAuth2User`를 `authentication.getPrincipal()`로 꺼낼 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 클래스까지 통과하면 카카오 로그인 과정이 성공적으로 처리된 것이다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;이후 과정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트는 쿠키에 담긴 리프레시 토큰을 가지고 서버에게 액세스 토큰 발급을 요청한다.&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;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>oauth2</category>
      <category>Spring Security</category>
      <category>카카오 로그인</category>
      <category>쿠키</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/184</guid>
      <comments>https://backend-jaamong.tistory.com/184#entry184comment</comments>
      <pubDate>Sun, 29 Dec 2024 15:43:53 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Value과 static 변수</title>
      <link>https://backend-jaamong.tistory.com/183</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적으로 Spring 애플리케이션에서 application.yml에 정의된 값을 static 변수에서 사용할 수 없다. 따라서 Spring에서 제공하는 `@Value` 애노테이션으로 설정 파일에 정의된 값을 static 변수에 주입하면 에러가 발생한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt; Java의 static 변수는 아래 시점에 메모리에 저장된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스프링 컨테이너(Application Context)가 로드되기 전&lt;b&gt; ==&lt;/b&gt; 객체 생성 이전&lt;b&gt; ==&lt;/b&gt; 의존성 주입을 받기 전&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 `@Value`는 의존 관계 주입 시점(인스턴스 수준)에서 동작한다. 이로 인해 의존성 주입을 받기 전에 메모리에 저장되는 static 변수에는 값을 주입할 수 없다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이는 더 나아가서 클래스 레벨에도 동일하게 적용할 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;`@Component` 애노테이션이&lt;span&gt; 적용되지 않은 &lt;/span&gt;&lt;/span&gt;클래스는 컴포넌트 스캔 대상에 포함되지 않으므로 스프링 빈으로 등록되지 않는다. 따라서 스프링 빈으로 등록되어 있지 않은 클래스에서 static 변수가 아니더라도 `@Value`로 값을 주입받으면 동일한 에러가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적인 주입 방법이 아닌 다른 방법을 사용하면 static 변수에 설정 값을 주입할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size14&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;span style=&quot;color: #000000;&quot;&gt;@PostConstruct&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`@PostConstruct`가 적용된 메서드는 모든 빈 초기화 직후에 호출이 된다. 이를 이용하여 인스턴스 변수에 주입된 값을 static 변수에 할당할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1734349770640&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class DemoConfig {

    @Value(&quot;${demo.config.property}&quot;)
    private String demoProperty; // 인스턴스 변수인 여기에 먼저 주입
    
    public static String STATIC_DEMO_PROPERTY; 
    
    @PostConstruct // 빈 초기화 이후 호출되어, 인스턴스 변수에 주입된 값을 static 변수에 할당
    public void setup() {
    	STATIC_DEMO_PROPERTY = demoProperty;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Setter&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;setter 메서드를 이용하여 static 변수에 값을 할당할 수 있다. 이전 방법과 달리 Spring이 값을 주입할 때 static 변수에 값을 바로 할당할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734350377719&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class DemoConfig {

    private String demoProperty;
    
    public static STATIC_DEMO_PROPERTY;
    
    @Value(&quot;@{demo.config.property}&quot;)
    public setDemoProperty(String demoProperty) {
    	DemoConfig.STATIC_DEMO_PROPERTY = demoProperty;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>@postConstruct</category>
      <category>@value</category>
      <category>application.yml</category>
      <category>SETTER</category>
      <category>spring</category>
      <category>static</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/183</guid>
      <comments>https://backend-jaamong.tistory.com/183#entry183comment</comments>
      <pubDate>Mon, 16 Dec 2024 21:06:22 +0900</pubDate>
    </item>
    <item>
      <title>잊기 전에 남겨두는 Amazon API Gateway 사용 이유</title>
      <link>https://backend-jaamong.tistory.com/181</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS Lambda 사용법을 조사하다가 Amazon API Gateway가 같이 언급되는 글이 많길래 왜 그런가 찾아봤다.&amp;nbsp;결론은 Lambda가 특별히 API Gateway가 필요해서 같이 붙어 다니는 건 아니었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;Lambda는 특성상&lt;/span&gt; 무거운 프로그램의 서버보다는, 한 가지 목적에 집중하는 가벼운 프로그램의 서버로 적합하다. 각 기능(인증, 스케쥴링, 모니터링 등)을 여러 개의 Lambda로 분리해서 API Gateway로 연결하는 편이 좋다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음은 API Gateway를 사용했을 때 얻을 수 있는 이점이다.&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;size18&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Reusability&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Lambda 유무와 관계없이&lt;/span&gt; &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;API Gateway를 사용하면&lt;/span&gt; 새 프로젝트 시작할 때마다 인증 기능을 구현할 필요도 없다. 인증 기능이 배포된 서버를 API Gateway에 연결해서 사용하면 되기 때문이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어, 클라이언트가 게이트웨이에서 한 번 인증을 하면, 인증된 요청들은 적절한 백엔드로 라우팅 된다. 따라서 각 백엔드는 인증을 처리할 필요가 없다.&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;size18&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt; Decoupling Clients from the Backend &amp;rarr; Abstracting Backend Infrastructure &lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 API Gateway은 클라이언트 요청에 대해 단일 도메인을 제공하므로, 클라이언트는 각 기능이 배포된 서버의 endnpoint를 알 필요가 없다. 오로지 API Gateway의 도메인만 알면 된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>api gateway</category>
      <category>AWS</category>
      <category>Lambda</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/181</guid>
      <comments>https://backend-jaamong.tistory.com/181#entry181comment</comments>
      <pubDate>Sat, 30 Nov 2024 09:07:17 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Data JPA] 하이버네이트 Batch Size</title>
      <link>https://backend-jaamong.tistory.com/182</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1:N 관계가 얽혀있는 엔티티를 대상으로 Spring Data JPA를 이용해서 Pagination 조회를 하니 말로만 듣던 N+1 상황이 발생했다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;처음에는 일반적인 해결 방법인 Fetch Join을 사용하려고 했다. 더불어 중복을 방지하기 위해&amp;nbsp; &lt;span style=&quot;text-align: start;&quot;&gt;`DISTINCT` 추가하려고 했다. &lt;/span&gt;그러나&amp;nbsp;&lt;b&gt;1:N 관계의 컬렉션을 Fetch Join 하면서 동시에 Pagination을 사용하는 것은 OutOfMemoryError가 발생할 수 있고&lt;/b&gt;, 이로 인해 `DISTINCT`를 적용할 수 없다고 한다. 그래서 찾은 다른 방법 중 가장 빠르고 쉬운 방법은 &lt;b&gt;Batch Size&lt;/b&gt;를 설정하는 것이었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;하이버네이트 Batch Size&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 하이버네이트에서 제공하는 `@BatchSize` 애노테이션이나 `batch_size`을 적용하여 Batch Size을 설정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엔티티를 가져오는 것은 데이터베이스에서 데이터를 가져와 애플리케이션에서 객체(Object)로 변환하는 것을 의미한다. 하이버네이트는 데이터를 그룹(batches)으로 가져와 이런 동작을 최적화할 수 있다. 이는 데이터베이스 호출 수를 줄이고, 요청 처리 효율성을 증가시킨다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;클래스 레벨에서 적용하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엔티티 클래스에 `@BatchSize(size=n)`을 적용하면 하이버네이트는 지연 로딩 동안 해당 엔티티의 인스턴스를 한 번에(한 요청) 최대 n개까지 가져오도록 지시한다. 이는 많은 연관된 엔티티들을 가져올 때 매우 효과적으로 데이터베이스 쿼리의 수를 줄일 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732513506075&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@BatchSize(size=100)
public class MyEntity {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;컬렉션 레벨에서 적용하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엔티티의 컬렉션 필드에 `@BatchSize`를 설정하면 해당 컬렉션의 요소를 배치 사이즈만큼 일괄적으로 가져와 지연 로딩 처리가 최적화된다. 이 또한 데이터베이스 쿼리 수를 줄이고 관련 컬렉션에 접근할 때 성능을 향상한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732513707368&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@OneToMany
@BatchSize(size=50)
private Set&amp;lt;MyCollection&amp;gt; myCollections = new HashSet&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;스프링 부트에서 `batch_size` 설정하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`batch_size` 파라미터 설정을 통해 insert, update, delete 작업에도 batch size를 적용할 수 있다. 해당 파라미터는 `application.properties` 또는 `application.yml`에서 쉽게 설정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732514275349&quot; class=&quot;properties&quot; data-ke-language=&quot;properties&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.jpa.properties.hibernate.jdbc.batch_size=50&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;하이버네이트에서 &lt;/span&gt;이러한 쓰기 작업을 최적화하는 것은 오직 전역 `batch_size` 설정을 통해서만 할 수 있다. `@BatchSize` 애노테이션은 오로지 읽기 작업에만 영향을 미친다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 읽기 작업 외에 쓰기(save/saveAll) 작업에도 적용되길 원했기 때문에 `application.yml`에 `batch_size` 설정을 하여 해결했다. &lt;span style=&quot;color: #333333;&quot;&gt;(보통 전역적으로 설정할 때 size는 100 이상으로 하는 것 같다)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #D8F781 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;번외. Batch Insert와 &lt;b&gt;MySQL&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;i&gt;&lt;b&gt;Notice&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&amp;nbsp; 여기서 부터는 1:N 관계 Pagination의 N+1 문제와는 관련이 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로 Batch Insert를 원하는데, MySQL을 사용하고 있고 PK 생성 전략을 `IDENTITY`로 하고 있다면 위 설정 외에도 추가적으로 해야 할 것이 있다. 그렇지 않으면 Spring Data JPA의 `saveAll()`을 쓰든 `batch_size`를 사용하든 데이터 건 수 별로 단건 insert 호출이 발생한다. 이유는 PK 생성 전략을 `IDENTITY`로 설정한 경우 &lt;span style=&quot;text-align: start;&quot;&gt;하이버네이트에서 Batch Insert 기능을 지원하지 않기 때문이다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MySQL에서 IDENTITY 전략은 `AUTO_INCREMENT`로 PK 값을 자동 증가시켜 생성하는 것이다. 이는 insert 작업을 실제로 실행하기 전까지는 ID에 할당된 값을 알 수 없음을 의미하며,&lt;/span&gt; &lt;a href=&quot;https://soongjamm.tistory.com/150&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Transaction Write-Behind&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 할 수 없어 Batch Insert 작업을 실행할 수 없는 결과를 초래한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 해결하는 방법 중 하나는 PK 생성 전략을 `IDENTITY` 대신 `SEQUENCE`나 `TABLE` 전략으로 변경하는 것이다. 하지만 이런 방법은 기존 데이터베이스 설계에 영향을 줄 수 있을 것 같아서 다른 방법을 찾았다. 바로 JDBC Template를 사용하는 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Batch Insert와 JDBC Template 사용하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JDBC Template을 사용하여 Batch Insert를 하려면 MySQL JDBC에 `rewriteBatchedStatements` 옵션을 추가하여 `true`로 설정해야 한다. JDBC Template 의존성은 Spring Data JPA를 사용중이라면 바로 import 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732516635452&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://{MYSQL_DB_URL}:{PORT}/{SCHEMA}?rewriteBatchedStatements=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 `JdbcTemplate`을 사용하여 Batch Insert를 해보자!&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732517203447&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class MyEntityJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    public void saveAll(List&amp;lt;MyEntity&amp;gt; myEntities, Long myEntityLastId) {
        String sql = &quot;INSERT INTO my_entity (my_entity_id, ..., created_at, modified_at) VALUES (?, ..., now(), now())&quot;;

        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                MyEntity myEntity = myEntities.get(i);

                // 파라미터 설정
                ps.setLong(1, myEntityLastId + i); // pk 설정
                ps.setString(2, myEntity.getXxx());
               	...
                ps.setString(n, myEntity.getXxx());
            }

            @Override
            public int getBatchSize() {  
                return myEntities.size();  // 삽입할 컬렉션 크기 만큼 배치 사이즈 설정
            }
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 insert/save 로직을 수행하는 곳에서 해당 메소드를 호출하여 사용하면 된다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;잘 적용이 되었는지 콘솔창에서 확인하고 싶다면 아래 설정을 추가하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732517592652&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://{MYSQL_DB_URL}:{PORT}/{SCHEMA}?rewriteBatchedStatements=true&amp;amp;profileSQL=true&amp;amp;logger=Slf4JLogger&amp;amp;maxQuerySizeToLog=500&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`profileSQL=true` :&amp;nbsp; Driver에서 전송하는 쿼리를 출력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`logger=Slf4JLogger` :&amp;nbsp; Driver에서 쿼리 출력 시 사용할 Logger 설정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`maxQuerySizeToLog=...` :&amp;nbsp; 출력할 쿼리 길이 설정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Batch Insert 이후 또다른 문제...&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Batch Insert를 사용한 `saveAll()` 이후 JPA를 이용한 단건 insert/save 작업에서 문제가 발생했다(둘은 별도의 트랜잭션에서 실행된다). 에러를 보니 PK 위반 문제였는데, 원인은 `SEQUENCE/IDENTITY` 생성에 관한 하이버네이트의 이해와 &lt;span style=&quot;text-align: start;&quot;&gt;데이터베이스 상태 &lt;/span&gt;사이의 불일치 때문이었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`jdbcTemplate.batchUpdate()`는 레코드를 바로 삽입하고 `AUTO_INCREMENT` 값을 증가시킨다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하이버네이트의 `save()`는 JDBC가 이미 사용한 최신 &lt;span style=&quot;text-align: left;&quot;&gt;`AUTO_INCREMENT` 값을 인지하지 못한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MySQL은 내부적으로 &lt;span style=&quot;text-align: left;&quot;&gt;`AUTO_INCREMENT` 카운터를 유지하려고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 JDBC를 사용한 Batch Insert 이후에 MySQL의 &lt;span style=&quot;text-align: left;&quot;&gt;`AUTO_INCREMENT` 값을 데이터베이스에 마지막으로 저장된 값보다 더 큰 값으로 업데이트하도록 로직을 수정했다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732519216572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class MyEntityJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    public void saveAll(List&amp;lt;MyEntity&amp;gt; myEntities, Long myEntityLastId) {
        ...
    }
    
    // 데이터베이스 id의 가장 마지막 값을 조회
    public Long getMaxId() {
        String sql = &quot;SELECT COALESCE(MAX(my_entity_id), 0) FROM my_entity&quot;;
        return jdbcTemplate.queryForObject(sql, Long.class);
    }

    // 다음 ID를 `value`로 시작하도록 테이블 변경
    public void resetAutoIncrement(Long value) {
        String sql = &quot;ALTER TABLE my_entity AUTO_INCREMENT = ?&quot;;
        jdbcTemplate.update(sql, value);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 사용 코드이다. Batch Insert 이후 ID 값을 업데이트 한다(단일 insert/save 하는 코드 쪽은 건드리지 않았다).&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732519438703&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void initData() {
    ...
    myEntityJdbcRepository.saveAll(myEntities, 1L);

    Long maxId = myEntityJdbcRepository.getMaxId();
    myEntityJdbcRepository.resetAutoIncrement(maxId + 1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 이후 정상적으로 실행됐다! &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;&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;size14&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 참고&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/community/questions/14663/fetch-join-%EC%8B%9C-paging-%EB%AC%B8%EC%A0%9C?srsltid=AfmBOopOr7Fi_2wRonYk50VyC7LBPkNir7iBzKThiEvUzuSWfS_dl0Ig&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/community/questions/14663/fetch-join-%EC%8B%9C-paging-%EB%AC%B8%EC%A0%9C?srsltid=AfmBOopOr7Fi_2wRonYk50VyC7LBPkNir7iBzKThiEvUzuSWfS_dl0Ig&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/jpa-java-persistence-api-guide/hibernate-optimization-with-batchsize-and-batch-size-configuration-579bf759fc05&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/jpa-java-persistence-api-guide/hibernate-optimization-with-batchsize-and-batch-size-configuration-579bf759fc05&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>Batch Insert</category>
      <category>batch size</category>
      <category>hibernate</category>
      <category>mysql</category>
      <category>N+1</category>
      <category>pagination</category>
      <category>Spring Data JPA</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/182</guid>
      <comments>https://backend-jaamong.tistory.com/182#entry182comment</comments>
      <pubDate>Mon, 25 Nov 2024 16:42:04 +0900</pubDate>
    </item>
    <item>
      <title>[Java Error] Caused by: java.lang.IllegalArgumentException at PropertyPlaceholderHelper.java:180</title>
      <link>https://backend-jaamong.tistory.com/180</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Gradle로 테스트를 빌드하다가 제목과 같은 에러를 만났다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실 처음에는 그냥 빌드하다가 위와 같은 정확한 에러명이 나오지 않아서 나중에 빌드할 때 아래 명령어를 사용했다. 그냥 빌드했을 때 실패했다고만 나오면 해당 옵션으로 시도하는 것을 추천한다 &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732336665190&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew test -i  # 윈도우 gitbash에서 실행&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;&lt;i&gt;Note&lt;/i&gt;&lt;/span&gt;&amp;nbsp;&lt;/b&gt; `-i / --info` 옵션은 로그 레벨을 INFO로 설정하는 것이다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;에러 내용을 봤을 때 yml/properties 파일에 작성한 프로퍼티 설정을 가져오면서 뭔가 문제가 있는 것 같았다. 그리고 역시나 `@Value`로 yml에 적힌 값을 가져올 때 오타가 있었다. 이 부분을 수정하고 다시 빌드하니 해당 에러는 더 이상 보이지 않았다! (관련 없는 다른 에러가 반겨주었다...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별거 아닌 내용이었지만 정리하는 이유는 테스트를 빌드하다가 위 에러를 처음 만났기도 했고, 처음 써본 gradle 옵션을 남겨두고 싶어서다. 저 옵션을 몰라서 계속 그냥 빌드하다가 오랜 시간 삽질을 했던... 삽질 기록기...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>build</category>
      <category>exception</category>
      <category>gradle</category>
      <category>java</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/180</guid>
      <comments>https://backend-jaamong.tistory.com/180#entry180comment</comments>
      <pubDate>Sat, 23 Nov 2024 13:57:29 +0900</pubDate>
    </item>
    <item>
      <title>Java/Spring 테스트 - 2</title>
      <link>https://backend-jaamong.tistory.com/179</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지난 포스팅에 이어서 작성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/177&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024.10.02 - [Java] - Java/Spring 테스트 - 1&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;의존성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;의존성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컴퓨터 공학에서 말하는 의존성(Dependency)은 결합(Coupling)과 같은 의미로, 다른 객체의 함수를 사용하는 상태를 말함&amp;nbsp; &amp;nbsp;&amp;rArr;&amp;nbsp; A는 B를 사용하기만 해도 의존한다고 할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성을 약하게 만드는 기술 중 하나가 의존성 주입&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필요한 값을 `new`해서 직접 인스턴스화하는 것이 아닌 외부에서 넣어주는 것&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`new`를 이용하여 인스턴스화 하는 것은 하드 코딩&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성 주입은 &lt;b&gt;의존성을 약화&lt;/b&gt;시키는 것, 의존성을 완전히 없애는 방식이 아님&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성 제거 == 객체 간의 협력 부정 / 시스템 간의 협력 부정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대부분의 디자인 패턴이나 설계는 어떻게 하면 의존성을 약화시킬 수 있는지 고민한 결과물&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;의존성 역전&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성 역전(DI) &amp;ne; 의존성 주입(DIP)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Dependency Inversion &amp;ne; Dependency Injection Principle &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;DIP&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성 역전이란?&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다형성의 원리 중 하나&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모듈 간의 상하 관계를 포함하는 개념&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;화살표의 방향을 바꾸는 기술&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;의존성 역전은 고수준 코드에서 세부 사항에 의존해서는 안됨&amp;nbsp; &amp;lt;&lt;/i&gt;&lt;/span&gt;RF. 로버트. C. 마틴 저, 클린 아키텍처&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`import`에 사용되는 경로에는 인터페이스나 추상 클래스만을 사용해야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인터페이스나 추상 클래스 같은 추상적인 선언을 &lt;b&gt;정책&lt;/b&gt;으로 볼 수 있고, 변동성이 큰 구체적인 요소를 &lt;b&gt;세부 사항&lt;/b&gt;으로 볼 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;의존성과 테스트 (의존성과 테스트의 상관관계)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트를 잘하려면 다음의 것을 잘 다룰 수 있어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성 주입&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성 역전&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;예시 | 마지막 로그인 시간&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;가정&lt;/span&gt;&amp;nbsp; 사용자가 로그인할 때마다, 마지막 로그인 시간을 기록해야 함&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730697540742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {

	private long lastLoginTimestamp;
	
	public void login() {
		// ...
		this.lastLoginTimestamp = Clock.systemUTC().millis();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성이 숨겨져 있는 경우에 해당&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내부 로직에서 `login`은 `Clock`에 의존적&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 외부에서 `login` 함수를 호출할 때는 `login`함수가 `Clock`을 사용하는지 모르기 때문&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적으로 의존성이 숨겨져 있으면 좋지 않은 신호&lt;/span&gt;
&lt;pre id=&quot;code_1730702474409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserTest {

    @Test
    public void login_테스트() {
        // given
        User user = new User();
				
        // when
        user.login();
				
        // then
        assertThat(user.getLastLoginTimestamp()).isEqualTo(???);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로 로그인한 시간을 비교해야 하는데 `???`에는 어떤 값이 들어가야 하는지 알 수 없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`login`을 호출한 시간과 결과를 비교하는 시점의 시간은 다를 수밖에 없음 &amp;rarr; 테스트 방법 X&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시간을 강제로 stub 해 주는 라이브러리가 있지만, 너무 부자연스러움 &amp;rarr; 라이브러리가 없으면 테스트가 불가능함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 코드를 테스트하는 것은 불가능하고 일관되게 유지하는 것도 어려움&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;&amp;rArr; 이런 식으로 테스트 코드를 작성하면서 &amp;lsquo;이건 테스트가 불가능한데?&amp;rsquo;, &amp;lsquo;테스트가 mock 프레임워크 없이는 불가능한데?&amp;rsquo; 라면 생각한다면, 이는 테스트가 보내는 신호!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;예시의 문제점 해결&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 의존성 주입으로 해결&lt;/b&gt; - 시계를 외부에서 주입받는 경우&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730697907154&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {

    private long lastLoginTimestamp;
	
    public void login(Clock clock) {
        // ...
        this.lastLoginTimestamp = clock.millis();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730697968079&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserTest {

    @Test
    public void login_테스트() {
        // given
        User user = new User();
        Clock clock = Clock.fixed(Instant.parse(&quot;시간&quot;), ZoneId.of(&quot;UTC&quot;));
				
        // when
        user.login(clock);
				
        // then
        assertThat(user.getLastLoginTimestamp()).isEqualTo(milliseconds로 환산한 시간);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필요한 값을 만들고, 그 값을 `login`에 전달함. 그다음 결과 값을 지정한 시간과 비교함.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 알게 된 점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;숨겨진 의존성은 테스트하기 힘들게 만든다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성은 드러내는 것이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 문제점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부에서 주입받아 해결했는데, 결국 `login`을 사용하는 곳에서도 `Clock`이라는 숨겨진 의존성을 사용해야 함&lt;/span&gt;
&lt;pre id=&quot;code_1730698192409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserService {
		
    public void login(User user) {
        // ...
        user.login(Clock.systemUTC()); // 숨겨진 의존성인 Clock 사용 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 `UserService` 테스트가 어려워짐&lt;/span&gt;
&lt;pre id=&quot;code_1730698223874&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserServiceTest {
	
    @Test
    public void login_테스트() {
        //given
        User user = new User();
        UserService userService = new UserService();
				
        //when
        userService.login(user);
				
        //then
        assertThat(user.getLastLoginTimestamp()).isEqualTo(???);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 해결하기 위해 또 다른 의존성 주입을 사용하는 것은 해결방법이 아님 &amp;rarr; `UserService`를 사용하는 또 다른 코드에 `Clock`이 숨겨지는 결과. 결국 어딘가에서는 고정된 값을 입력하여 넣어줘야만 함 &amp;rarr; 또 테스트하기 힘들어짐 &amp;rArr; 폭탄 돌리기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;2. 의존성 역전으로 해결&lt;/b&gt; - 현재 시간을 알려주는 인터페이스 정의&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730698376925&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ClockHolder {
    long getMillis();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730698421958&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
class User {
    private long lastLoginTimestamp;
		
    public void login(ClockHolder clockHolder) {
        // ...
        this.lastLoginTimestamp = clockHolder.getMillis();
    }
}

@Service
@RequiredArgsConstructor
class UserService {

    private final ClockHolder clockHolder;
		
    public void login(User user) {
        // ...
        user.login(clockHolder);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`User`는 이제 `Clock`이 아닌 `ClockHolder`에 의존&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`ClockHolder`는 외부에서 주입받도록 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`UserService`는 `ClockHolder`를 멤버 변수로 받도록 되어 있음&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유저가 로그인할 때 `clockHolder` 값을 건네줌&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt; 장점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선 `ClockHolder`의 두 가지 구현체를 생성해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730700136501&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class SystemClockHolder implements ClockHolder {
		
    @Override
    public long getMillis() {
        return Clock.systemUTC().millis();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 배포 환경에서 사용할 `SystemClockHolder`&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 시간을 요청하면 시스템 시간을 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730700191581&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
class TestClockHolder implements ClockHolder {

    private Clock clock;
		
    @Override
    public long getMillis() {
        return clock.millis();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 구현체는 객체를 생성할 때 `Clock`을 받도록 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 실제 테스트&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1730700325488&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserServiceTest {

    @Test
    public void login_테스트() {
        // given
        Clock clock = Clock.fixed(Instant.parse(&quot;시간&quot;), ZoneId.of(&quot;UTC&quot;));
        User user = new User();
        UserService userService = new UserService(new TestClockHolder(clock));
				
        // when
        userService.login(user);
				
        // then
        assertThat(user.getLastLoginTimestamp()).isEqualTo(milliseconds로 환산한 시간);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`UserService`를 생성할 때 `TestClockHolder`를 주입&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`User`가 받게 될 `ClockHolder`는 지정한 `Clock`만 내려주는 `ClockHolder`가 될 것&lt;/span&gt;&lt;/li&gt;
&amp;rArr; 쉽게 깨지지 않는 테스트 코드. 항상 같은 결과만 내려주는, 일관된 테스트가 됨&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 환경에서는 어떨까?&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`SystemClockHolder`는 스프링 빈으로 등록되어 있기 때문에 스프링이 `UserService`를 만들 때 알아서 잘 주입해 줄 것.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과적으로 &lt;b&gt;의존성 주입&lt;/b&gt;과, &lt;b&gt;의존성 역전&lt;/b&gt; 기술 모두 사용함&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성 역전을 사용했을 뿐인데, 배포 환경과 테스트 환경을 분리할 수 있게 됨 &amp;rArr; 추상화 의존 결과&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존성 역전 `Port-Adapter 패턴`으로도 부름&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Testability&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Testability&lt;/b&gt;는 테스트 가능성으로, &lt;u&gt;얼마나 쉽게 input을 변경할 수 있고, 얼마나 쉽게 output을 검증할 수 있는가&lt;/u&gt;를 의미한다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;얼마나 쉽게 Input을 변경...&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Case1.1 - 의존성이 감춰져 있는 경우&lt;/b&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1730700939416&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {

    private long lastLoginTimestamp;
		
    public void login() {
        // ...
        this.lastLoginTimestamp = Clock.systemUTC().millis();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위처럼 `Clock`같은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;감춰진 의존성이 존재한다면 input을 외부에서 변경할 수 없음&lt;/span&gt; &amp;rArr; testability가 낮은 코드&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Case1.2 - Input이 숨겨져 있는 경우&lt;/b&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1730701029792&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Builder(access = AccessLevel.PRIVATE)
@RequiredArgsConstructor
public class Account {
		
    private final String username;
    private final String authToken;
		
    public static Account create(String username) {
        return Account.builder()
            .username(username)
            .authToken(UUID.randomUUID().toString())
            .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Account` 내부에는 인스턴스를 생성하는 팩토리 메서드가 있는데 괜찮을까?&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;테스트 코드 1&lt;/b&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1730701296464&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AccountTest {

    @Test
    void create() {
		
        //given
        String username = &quot;foobar&quot;;
				
        // when
        Account account = Account.create(username);
				
        // then
        assertThat(account.getUsername()).isEqualTo(&quot;foobar&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`username` 말고도 `authToken`이 잘 생성되었는지 확인해야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`authToken`은 UUID로 생성 &amp;mdash; &quot;UUID가 사용된다는 이야기는 처음 듣는데요?&quot; &amp;rarr; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;숨겨진 input&lt;/span&gt;: 호출자는 모르는 정보&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;호출자가 해당 메서드를 타고 들어와서 내부 알고리즘을 확인함 &amp;rarr; 객체지향의 핵심 가치 중 하나인 캡슐화가 깨짐&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;알고리즘을 확인했어도 만들어진 `authToken` 값이 제대로 된 값인지 확인할 방법이 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;테스트 코드 2 &lt;/b&gt;-&amp;nbsp;mock 프레임워크 이용해 보기: 강제 라이브러리들을 이용해서 지정된 UUID만 내려주도록 바꿔보자.&lt;/span&gt;
&lt;pre id=&quot;code_1730701592082&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class AccountTest {

    @Test
    void create() {
		
        //given
        String username = &quot;foobar&quot;;
        String expectedAuthToken = &quot;...&quot;;
        PowerMockito.mockStatic(UUID.class);
        when(UUID.randomUUID()).thenReturn(UUID.fromString(expectedAuthToken));
				
        // when
        Account account = Account.create(username);
				
        // then
        assertThat(account.getUsername()).isEqualTo(&quot;foobar&quot;);
        assertThat(account.getAuthToken()).isEqualTo(expectedAuthToken);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;UUID의 `randomUUID()`는 static 메서드&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;static 메서드를 mock 하는 건 `mockito` 만으로는 안됨 &amp;rarr; java의 `PowerMock` 라이브러리를 활용해 봄&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 찾아보니 UUID는 final class &amp;rarr; 어떻게든 방법을 찾으면 해결할 수도 있음 but 이는 테스트가 보내는 신호를 무시하는 것; 잘못된 설계&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;&amp;rArr; 의존성 역전으로 해결해 보자!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Case 2.1 - 파일 경로 하드 코딩&lt;/b&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1730701768137&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    private static final File FILE = new File(&quot;data.txt&quot;);
		
    public void processData() {
        //read from FILE and process data
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;File path가 고정되어 있다면, 고정된 파일에 지나치게 의존하게 돼서 input 변경이 힘듦 &amp;rArr; 낮은 Testability &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Case 2.2 - 외부 시스템 하드 코딩&lt;/b&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1730701791557&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    public void processData(String data) {
        String processData = data.toUpperCase(); // 데이터 처리
        sendDataOverNetWork(processedData);
    }
		
    private void sendDataOverNetwork(String data) {
        // HTTP를 이용한 네트워크 요청 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스프링을 이용하다 보면 `WebClient`나 `RestTemplate` 같은 통신 클래스를 많이 사용하게 됨 &amp;rarr; 이를 &lt;b&gt;직접&lt;/b&gt; 사용하고 있을 것 &amp;rarr; 테스트 어려움 &amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;얼마나 쉽게 output을 검증&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Case. 외부에서 결과를 볼 수 없는 경우&lt;/b&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1730701939847&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    public void processData(int[] numbers) {
        int sum = 0;
        for (int number : numbers) {
            sum += number;
        }
        System.out.println(&quot;Sum: &quot; + sum);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부에서 콘솔에 출력된 값이 어떤 값인지 확인할 수 없음 &amp;rarr; 결과 확인 불가능 &amp;rArr; 낮은 Testability&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>DI</category>
      <category>dip</category>
      <category>java</category>
      <category>spring</category>
      <category>test</category>
      <category>testability</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/179</guid>
      <comments>https://backend-jaamong.tistory.com/179#entry179comment</comments>
      <pubDate>Mon, 4 Nov 2024 20:42:26 +0900</pubDate>
    </item>
    <item>
      <title>[JPA Error] No EntityManager with actual transaction available for current thread - cannot reliably process 'flush' call</title>
      <link>https://backend-jaamong.tistory.com/178</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;에러 원인&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;정말... 바보같은... 실수였다... &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오류가 났던 테스트 코드는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729659152887&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	...
    
    @PersistenceContext
    EntityManager em;

    @Test
    void test() { 
        UserEntity userEntity = UserEntity.builder()
                .nickname(&quot;test&quot;)
                .email(&quot;test@example.com&quot;)
                .build();
        userEntity = userRepository.save(userEntity);
        userService.sumTotalAmount(userEntity, 10_000L);

        Long requestAmount = 5_000L;
        Long updatedAmount = userService.minusTotalAmount(userEntity, requestAmount);

        em.flush();
        em.clear();

        //잘 반영이 되었는지 확인하기 위해 영속성 컨텍스트를 비우고 새로 조회
        UserEntity findEntity = userRepository.findById(userEntity.getId()).get();

        assertThat(updatedAmount).isEqualTo(5_000L);
        assertThat(findEntity.getTotalAmount()).isEqualTo(updatedAmount);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 하고 싶었던 부분은 `minusTotalAmount`가 요청한 만큼 `totalAmount`를 잘 감소시키는 지였다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;우선 `em.flush()`로 쿼리를 날려주고, 영속성 컨텍스트를 비운 상태에서 객체를 새로 조회하기 위해 `em.clear()`를 호출했었다. 그다음 객체를 새로 조회하여 필드에 제대로 값이 반영이 되었는지 검증했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 제목과 같은 에러가 발생했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;번역하자면...&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;현재 스레드에 사용 가능한 실제 트랜잭션이 있는 EntityManager가 없습니다. 'flush' 호출을 안정적으로 처리할 수 없습니다.&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;`EntityManager`가 `flush()`를 호출했는데 이를 처리할 Transaction이 없어서 실행할 수 없다는 뜻이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JPA는 트랜잭션 단위로 기능을 수행하고, 수행하면서 1차 캐시에 저장된 엔티티들은 DB에 flush되어 영속화된다. 그런데 위 테스트 코드는 트랜잭션이 없기 때문에 위와 같은 에러가 발생했던 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;에러 해결&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아주 간단히 해결할 수 있다... 테스트할 메서드나 클래스에 `@Transactional`를 붙여주면 된다!&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729659787551&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	...
    
    @PersistenceContext
    EntityManager em;

    @Test
    @Transactional
    void test() { 
        UserEntity userEntity = UserEntity.builder()
                .nickname(&quot;test&quot;)
                .email(&quot;test@example.com&quot;)
                .build();
        userEntity = userRepository.save(userEntity);
        userService.sumTotalAmount(userEntity, 10_000L);

        Long requestAmount = 5_000L;
        Long updatedAmount = userService.minusTotalAmount(userEntity, requestAmount);

        em.flush();
        em.clear();

        //잘 반영이 되었는지 확인하기 위해 영속성 컨텍스트를 비우고 새로 조회
        UserEntity findEntity = userRepository.findById(userEntity.getId()).get();

        assertThat(updatedAmount).isEqualTo(5_000L);
        assertThat(findEntity.getTotalAmount()).isEqualTo(updatedAmount);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 `flush()`를 호출하고 콘솔창을 확인하면 쿼리가 제대로 나가는 것을 확인할 수 있다^^!&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;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>@transactional</category>
      <category>flush</category>
      <category>jpa</category>
      <category>transaction</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/178</guid>
      <comments>https://backend-jaamong.tistory.com/178#entry178comment</comments>
      <pubDate>Wed, 23 Oct 2024 20:04:42 +0900</pubDate>
    </item>
    <item>
      <title>Java/Spring 테스트 - 1</title>
      <link>https://backend-jaamong.tistory.com/177</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 글은 아래 인프런 강의를 듣고 기록을 남기고자 작성하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1727841282074&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트 강의 | 김우근 - 인프런&quot; data-og-description=&quot;김우근 | Spring에 테스트를 넣는 방법을 알려드립니다! 더 나아가 자연스러운 테스트를 할 수 있게 스프링 설계를 변경하는 방법을 배웁니다., 프로젝트 설계를 발전시키는&amp;nbsp;테스트의 본질을 짚&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%A4%EB%8B%B5%EB%85%B8%ED%8A%B8/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/자바-스프링-테스트-개발자-오답노트&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bfAZYY/hyXaxGspxC/etOAlTjFkfMC9AvYkwSCw1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/lahbD/hyXav9Jvcj/1CYkzBZ7oufhk3didCK5Bk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/IiUdb/hyXavBRrA9/TNBfFnJLWdkTH7sNkXvcu0/img.png?width=736&amp;amp;height=479&amp;amp;face=0_0_736_479&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%A4%EB%8B%B5%EB%85%B8%ED%8A%B8/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%A4%EB%8B%B5%EB%85%B8%ED%8A%B8/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bfAZYY/hyXaxGspxC/etOAlTjFkfMC9AvYkwSCw1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/lahbD/hyXav9Jvcj/1CYkzBZ7oufhk3didCK5Bk/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/IiUdb/hyXavBRrA9/TNBfFnJLWdkTH7sNkXvcu0/img.png?width=736&amp;amp;height=479&amp;amp;face=0_0_736_479');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트 강의 | 김우근 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;김우근 | Spring에 테스트를 넣는 방법을 알려드립니다! 더 나아가 자연스러운 테스트를 할 수 있게 스프링 설계를 변경하는 방법을 배웁니다., 프로젝트 설계를 발전시키는&amp;nbsp;테스트의 본질을 짚&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;span style=&quot;color: #000000;&quot;&gt;목차&lt;/span&gt;&lt;/b&gt; &lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#TDD&quot;&gt;TDD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;Test&quot;&gt;테스트란&lt;/a&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;테스트&lt;/li&gt;
&lt;li&gt;TDD&lt;/li&gt;
&lt;li&gt;장단점&lt;/li&gt;
&lt;li&gt;개발자의 고민&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;necessity&quot;&gt;테스트 코드의 필요성&lt;/a&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;좋은 아키텍처란&lt;/li&gt;
&lt;li&gt;필요성&lt;/li&gt;
&lt;li&gt;테스트 3분류&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;concept&quot;&gt;개념&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;TDD&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;TDD&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;회귀 버그(Regression Bug) &amp;rarr; 테스트 자동화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 추가&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소프트웨어의 내적 품질 향상 O, 사용자가 체감할 수 있는 외적 품질 향상 X&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가시적 성과 지표 &amp;rarr; 커버리지(Coverage)&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비결정적인 테스트&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트를 실행할 때마다 테스트 결과가 다름&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어제는 테스트를 통과했는데 오늘은 통과하지 못하는 경우&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레거시 코드에 테스트를 넣는 것 &amp;rarr; &lt;b&gt;TDD가 아님&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레거시 코드에 테스트를 넣기만 하는 것은 크게 의미가 없을 수도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레거시 코드에 테스트를 넣는 과정은 코드 개선을 하면서 진행해야 의미가 있다 (외부 시스템 의존도&amp;nbsp;&amp;darr; &amp;amp; 설계 진화)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커버리지 집착 X&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생각보다 테스트가 필요 없는 코드가 있을 수도 있다. 커버리지 목적이 아닌, 테스트를 추가했을 때 얻을 수 있는 효용성에 집중하자.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✅테스트가 줄 수 있는 가치&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;회귀 버그 방지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;&lt;b&gt;유연한 설계 (매우 도전적)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트가 계속 신호를 보냄 &lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트가 스프링이나 JPA에 의존적인 상태일 확률 높음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;외부 시스템에 의존하는 상황을 피해야 함 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&amp;rarr; 설계 진화&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;테스트와 설계는 긴밀한 관계&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;✅&lt;/span&gt;&lt;i&gt;왜 테스트를 해야 하고, 어떻게 해야 하는지 고민할 것&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;✅&lt;/span&gt;&lt;b&gt;방향성&lt;/b&gt; - TDD를 논하기 전에 테스트가 가능한 구조로 변경되어야 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;Test&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;테스트란&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;rArr; 어떤 시스템이 제대로 동작하는지 검증하는 과정&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인수 테스트&lt;/b&gt;&lt;b&gt;&lt;/b&gt;사람이 직접 사용해 보면서 준비된 체크 리스트를 진행하는 테스트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;자동 테스트&lt;/b&gt;&lt;b&gt;&lt;/b&gt;테스트 코드라고 불리는 미리 작성된 코드를 보면서 결괏값과 예상값을 비교하는 테스트&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주로 `when/given/then` 패턴으로 작성됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;TDD&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;i&gt;&lt;b&gt;아래 과정을 무한히 반복&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`RED` : 실패하는 테스트를 먼저 작성한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컴파일되는 코드를 만들고 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;테스트가 &lt;b&gt;실패&lt;/b&gt;하는 것까지 확인&lt;/span&gt;해야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실패하는 테스트를 먼저 작성해야 하므로, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;인터페이스를 먼저 만드는 것이 강제&lt;/span&gt;됨 &amp;rarr; OOP 핵심 원리 중 하나인 &lt;b&gt;행동에 집중&lt;/b&gt;하는 것과 동일 &amp;rArr; &lt;i&gt;What/Who 사이클을 고민하게 도와줌&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`GREEN` : 위 실패하는 테스트를 성공시킨다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제로 구현하는 단계&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 테스트를 실행하면 성공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`BLUE` : 리팩토링 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;장단점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장기적인 관점에서의 장점이 확실함&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인터페이스를 먼저 만드는 것이 강제됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발 비용 감소&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장기적으로 봤을 때 장점이 뚜렷하므로 요구사항이 명확하지 않거나, 서비스의 흥망성쇠가 눈에 보이지 않는 경우에는 적용하기 부담스러움(Ex. 스타트업)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;적용 난이도 높음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;개발자의 고민&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;무의미한 테스트&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OOP에서 말하는 행동은 메서드나 함수를 의미하는 것이 아니다. 따라서 모든 메서드를 테스트하는 것보다 중요한 로직을 잘 구분하여 해당 코드를 테스트하는 것이 낫다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트가 불가능한 코드&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ex. 사용자가 로그인하고 마지막 로그인 시간을 기록하는 코드 등...&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트가 불가함 &amp;rarr; 설계가 잘못되었을 가능성 높음 &amp;rArr; 설계 발전 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;느리고 쉽게 깨지는 코드&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드 자체는 별 것 아니지만, 실행 시간이 오래 걸리는 경우&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;100개가 넘어가는 테스트 코드 중에 이와 같은 경우가 있다면 시간적으로 부담됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;necessity&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;테스트 코드의 필요성&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;좋은 아키텍처란&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SOLID와 테스트는 굉장히 긴밀한 상관관계를 갖는다 &amp;rarr; 서로에게 상호보완적&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`TEST` for 회귀버그 방지 &amp;harr; `SOLID` for 좋은 아키텍처&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SOLID 원칙이 지켜지면 경계가 만들어짐 &lt;span style=&quot;text-align: left;&quot;&gt;&amp;rarr; 이로 인해 회귀 버그를 막을 수 있음&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;단일 책임 원칙(SRP)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;테스트는 명료하고 간단하게 작성해야 하기 때문에, 단일 책임 원칙을 지키게 됨&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;테스트가 너무 많아져서 어떤 목적의 클래스인지 파악하기 어려운 지점이 생김. 이는 해당 클래스의 책임 과다 문제일 수 있음. &lt;span style=&quot;text-align: left;&quot;&gt;&amp;rarr; 클래스를 분할해야 하는 시점 &amp;rArr; 자연스러운 책임 분배 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;개방 폐쇄 원칙(OCP)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;테스트 컴포넌트와 프로덕션 컴포넌트를 나눠 작성하게 되고, 필요에 따라 이 컴포넌트를 자유자재로 탈부착이 가능하도록 개발하게 됨&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;리스코프 치환 원칙(LSP)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;이상적으로 테스트는 모든 케이스에 대해커버하고 있으므로, 서브 클래스에 대한 치환 여부를 테스트가 알아서 판단&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인터페이스 분리 원칙(ISP)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트는 그 자체로 인터페이스를 직접 사용해 볼 수 있는 환경. 불필요한 의존성을 실제로 확인할 수 있는 샌드박스.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;의존관계 역전 원칙(DIP)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가짜 객체를 이용하여 테스트를 작성하려면 의존성이 역전되어 있어야 하는 경우가 생김&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;필요성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트를 어떻게 바라보냐에 따라 테스트의 가치는 다르게 측정됨&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;낮은 가치: &quot;품질 보증을 위한 도구/회귀 버그 방지&quot;만 바라보고 테스트 코드 작성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;높은 가치: &quot;회귀 버그 방지&quot;와 &quot;설계를 위한 도구&quot;를 위한 테스트 코드 작성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;RF. Effective unit testing&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;테스트 3분류&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단위 테스트 &amp;rarr; &lt;b&gt;Small(소형) 테스트 (80%)&amp;nbsp;&lt;/b&gt; &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;중요&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`단일 서버 / 단일 프로세스/ 단일 스레드`에서 실행되는 테스트를 의미&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Disk IO`와 `Blocking call`이 있으면 안 됨&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ex. `Thread.sleep`이 테스트에 있으면 소형 테스트가 아님&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 조건 때문에, 소형 테스트는 결과가 항상 &lt;b&gt;결정적(Deterministic)이고 테스트 속도가 빠름 &lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 테스트를 여러 개 만들어서 코드 커버리지를 높여야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트에 아직 테스트가 없다면, 소형 테스트를 늘릴 수 있는 환경을 만들고, 소형 테스트를 늘려야 함.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;통합 테스트 &amp;rarr; Medium(중형) 테스트 (15%) &lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소형 테스트보다 완화된 기준&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`단일 서버 / 멀티 프로세스 / 멀티 스레드` 사용 가능&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;H2와 같은 테스트용 DB 사용 가능&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다시 말하면, 소형 테스트에서는 DB(H2) 사용 불가&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소형 테스트보다 느림&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;멀티 스레드 환경에서 어떻게 동작할지 모르기 때문에 결과가 항상 같다는 보장을 할 수 없음&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 결과가 H2 같은 외부 모듈의 동작에 따라서 달라짐&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;API 테스트 &amp;rarr; Large(대형) 테스트 (5%)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`멀티 서버` 가능 &lt;span style=&quot;text-align: left;&quot;&gt;&amp;rarr; E2E(End to End) 테스트&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;RF. Software Engineering at Google&lt;/i&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;concept&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SUT&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;System Under Test&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트하려는 대상&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ex. 북마크 결과가 제대로 됐는지 기록하는 테스트 코드가 있을 때, `SUT`는 user&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;BDD&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Behaviour driven development (given - when - then 또는 3A) &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트를 작성하다 보면 어느 순간 &quot;어디에 어떻게 테스트를 넣어야 하지?&quot;라는 질문을 마주하게 된다. 그때 BDD가 &quot;생동에 집중해야 한다&quot;고 알려준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유저가 시스템을 사용하는 user story를 강조하고, 시나리오를 강조한다. 이를 지키기 위한 뼈대로 아래와 같은 방식을 사용하라고 권유함&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 상황이 주어졌을 때(given)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 행동을 하면(when)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과가 이렇다(then)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;상호 작용 테스트(Interaction Test)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메서드가 실제로 호출이 됐는지 검증하는 테스트&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적으로, 메서드가 실제로 호출됐는지 검증하는 방법은 좋지 않음&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내부 구현을 어떻게 했는지 &lt;b&gt;감시&lt;/b&gt;하는 것이므로&amp;nbsp; &amp;rArr; 캡슐화 위배&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;객체에게 위임한 책임을 해당 객체가 제대로 수행했는지 확인만 하면 되는데 구현에 집착하게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;상태 검증 vs 행위 검증&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상태 기반 검증(state-based-verification)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 값을 시스템에 넣었을 때, 나오는 결괏값을 기댓값과 비교하는 방식&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;행위 기반 검증(behaviour-based-verification)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;== 상호 작용 테스트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 값을 시스템에 넣었을 때, 협력 객체의 어떤 메서드를 실행하는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;BDD에서 말하는 `행위`와는 다른 의미. 행위 기반 검증이 BDD를 말하는 것이 아님.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;테스트 픽스처&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fixture = 비품, 설비&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트를 하기 위해 필요한 자원이 있다면 미리 생성해 두는 것을 테스트 픽스처라고 함&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`@Before`를 적용한 메서드에 테스트를 하기 위한 `sut`을 미리 생성해 둠 &amp;rArr; `sut = 테스트 픽스처`&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 테스트 픽스처는 sut가 될 수 있고, sut에 들어가야 하는 의존성 일부가 될 수도 있음. &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;비욘세 규칙&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구글에서 만든 규칙(대중적이지 않음)이지만, 꽤 의미 있는 규칙&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;유지하고 싶은 상태나 정책이 있다면, 알아서 테스트를 만들어야 한다.&quot;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유지하고 싶은 상태가 있으면 전부 테스트로 작성 &amp;rarr; 그게 곧 정책이 됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유저 아이디가 이메일 형식이길 원한다면, 유저 아이디가 이메일이 아닐 때 예외를 던지는 테스트를 작성하면 됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마일리지 쿠폰이 5만 원 이상일 때만 사용 가능하게 하고 싶다면, 5만원 미만일 때 예외를 던지는 테스트를 작성하면 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;협업 시에도 큰 도움이 됨&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발 문서보다 더 효율적일 때도 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;Testability&lt;/span&gt; &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(다른 글에서 다시 정리)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 가능성. 소프트웨어가 테스트 가능한 구조인가?&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Test Double&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;double == 스턴트맨과 같은 '대역'&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;회원 가입을 할 때 인증 메일을 보내는 시스템이 있을 때, 회원 가입하는 코드를 테스트할 때마다 인증 메일이 발송되어야 할까? 그럴 수는 없음 &amp;rarr; 대신 이메일을 발송하는 부분에 &lt;b&gt;가짜 객체(Test Double)&lt;/b&gt;를 넣어준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;대역 종류&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dummy&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일을 시켜도 아무런 동작도 하지 않는 객체&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Fake&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Dummy와 달리 자체 로직을 갖고 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;회원 가입 메일 내용이 제대로 만들어졌는지 테스트하고 싶음 &lt;span style=&quot;text-align: left;&quot;&gt;&amp;rarr; Fake 사용&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;발송한 메시지를 기록하는 로직을 가지고 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;잘 만들어진 Fake는 테스트할 때 말고도, 로컬 개발 시에도 사용할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;Stub&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;주로 외부 연동하는 컴포넌트에 많이 사용함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;객체에 어떤 일을 시켰을 때 미리 준비된 값을 제공&lt;/span&gt;
&lt;pre id=&quot;code_1727845530237&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SubUserRepository implements UserRepository {
	public User getByEmail(String email) {
		if (email.equals(&quot;foo@bar.com&quot;)) {
			return User.Builder()
					.email(&quot;foo@bar.com&quot;)
					.status(&quot;PENDING&quot;)
					.build();
			}
		throw new UsernameNotFoundException(email);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;보통 `mokito` 프레임워크를 이용하여 구현됨&lt;/span&gt;
&lt;pre id=&quot;code_1727845629434&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//given
given(userRepository.getByEmail(&quot;foo@bar.com&quot;)).willReturn(User.builder())
		.emamil(&quot;foo@bar.com&quot;)
		.status(&quot;PENDING&quot;)
		.build();
// when
// ...

// then
// ...&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Mock&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실상 테스트 더블과 동일한 의미로 사용됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;stub &amp;rarr; mock, dummy &amp;rarr; mock, fake &amp;rarr; mock으로 부름&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메서드 호출을 확인하기 위한 객체&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spy&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 메서드 호출을 낱낱이 기록해 두고 있는 객체&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메서드가 몇 번 호출됐는지, 잘 호출됐는지 등을 검증&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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>Java &amp;amp; Kotlin</category>
      <category>java</category>
      <category>tdd</category>
      <category>test</category>
      <category>테스트</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/177</guid>
      <comments>https://backend-jaamong.tistory.com/177#entry177comment</comments>
      <pubDate>Wed, 2 Oct 2024 20:30:14 +0900</pubDate>
    </item>
    <item>
      <title>[Gradle] gradle build와 gradle bootJar의 차이</title>
      <link>https://backend-jaamong.tistory.com/176</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Dockerfile` 안에서 스프링 부트 프로젝트를 `gradlew`로 빌드하려고 찾아보니 `build` 말고 `bootJar`를 추천했다. 이유가 궁금해져서 이렇게 글을 정리하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;build&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`build`는&lt;/span&gt; &lt;a href=&quot;https://docs.gradle.org/current/userguide/base_plugin.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Base Plugin&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 제공하는 &lt;b&gt;생명주기 작업&lt;/b&gt;으로, 대상 소스의 생명주기 &lt;i&gt;확인&lt;/i&gt;이나&lt;i&gt; 어셈블 &lt;/i&gt;작업을 수행한다. 여기에는 테스트 실행이나 프로덕션 아티팩트 생성 등이 포함되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;bootJar&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`bootJar`는 대상 소스를 &lt;b&gt;실행가능한 `jar` 파일로 빌드&lt;/b&gt;하는 것으로, 빌드 속도가 `build` 작업에 비해 빠르고 당연히 생성되는 것도 적다. 이렇게 빌드된 파일은 `build/libs/*.jar`에 위치하게 된다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 생성된 파일을 대상으로 `jar -xf {대상 jar 파일}` 명령어를 실행하여 압축을 풀면 `BOOT-INF`, `META-INF`, `org` 폴더가 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 폴더들이 무엇을 의미하는 지는 여기&lt;/span&gt; &lt;a href=&quot;https://elsboo.tistory.com/27&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 참고하자. 잘 정리되어 있다.&amp;nbsp;&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;&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.devkuma.com/docs/gradle/bootjar-jar/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.devkuma.com/docs/gradle/bootjar-jar/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/64747475/difference-between-gradle-build-and-gradle-bootjar&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/64747475/difference-between-gradle-build-and-gradle-bootjar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>gradle bootjar</category>
      <category>gradle build</category>
      <category>jar</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/176</guid>
      <comments>https://backend-jaamong.tistory.com/176#entry176comment</comments>
      <pubDate>Fri, 6 Sep 2024 16:45:11 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 임베디드 타입(@Embeddable, @Embedded)에 관하여</title>
      <link>https://backend-jaamong.tistory.com/175</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;@Embeddable, @Embedded&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`@Embeddable` 애노테이션이 있는 클래스는 기본 생성자가 필요함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;임베디드 타입은 값 타입에 속하는데, 이러한 값 타입은 여러 엔티티에서 공유하면 side effect를 일으킬 수 있다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 &lt;b&gt;불변 객체(Immutable Object)로 설정&lt;/b&gt;하여 이러한 문제를 방지하는 것이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`@Embeddable` 클래스에 수정자(setter)를 생성하지 않고 생성자만 두어, 생성자로만 값을 설정하게 한다&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정자가 없으므로 해당 임베디드 값 타입을 필드로 갖고 있는(@Embedded) 엔티티에서는 특정 값만 수정하는 것이 불가능. &lt;b&gt;객체를 새로 생성&lt;/b&gt;해야 함.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723364074537&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String args[]) {

    Address address = new Address(&quot;city&quot;, &quot;zipcode&quot;); //임베디드 값 타입
    
    Member member = new Member(); // 멤버 엔티티
    member.setAddress(address);
    
    //만일 address의 city 값을 수정하고 싶다면 아래와 같이 해야함
    
    Address newAddress = new Address(&quot;newCity&quot;, &quot;zipcode&quot;); //새로운 객체
    member.setAddress(newAddress);
    
    //Address 클래스에는 수정자가 없고 생성자만 존재하므로, 특정 값만 수정하고 싶어도 위와 같이 새로운 객체를 생성해야 함

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>@Embeddable</category>
      <category>@Embedded</category>
      <category>jpa</category>
      <category>값 타입</category>
      <category>임베디드 타입</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/175</guid>
      <comments>https://backend-jaamong.tistory.com/175#entry175comment</comments>
      <pubDate>Fri, 23 Aug 2024 09:15:30 +0900</pubDate>
    </item>
    <item>
      <title>[Java] new Integer와 Integer.valueOf()의 차이점</title>
      <link>https://backend-jaamong.tistory.com/174</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`new Integer(int)`는 새로운 객체 인스턴스를 매번 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Integer.valueOf(int)`은 지정된 `int` 값을 나타내는 `Integer` 인스턴스를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;새로운 `Integer` 인스턴스가 필요한 경우가 아니라면, 일반적으로 생성자 `Integer(int)` 보다 해당 메서드를 우선적으로 사용하는 것이 메모리 측면에서 권장된다. 이는 자주 요청되는 값을 캐싱하여 더 나은 공간 및 시간 성능을 제공할 가능성이 높기 때문이다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;다음은 예시 코드다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723362603104&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    
    Integer a = new Integer(10);
    Integer b = new Integer(10);
    
    System.out.println(&quot;a==b : &quot; + (a==b));
    
    Integer c = Integer.valueOf(10);
    Integer d = Integer.valueOf(10);
    
    System.out.println(&quot;c==d : &quot; + (c==d));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723362688808&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;a==b : false
c==d : true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;강의를 듣다가 이야기가 나와서 한 번 알아보았다. 근데 `new Integer(int)` 이 생성자는 버전 9부터 &lt;i&gt;deprecated&lt;/i&gt; 상태다. 따라서 해당 버전 이상을 사용할 경우 강제로 `Integer.valueOf(int)` 사용하게 된다 :)&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;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>Integer</category>
      <category>java</category>
      <category>valueof</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/174</guid>
      <comments>https://backend-jaamong.tistory.com/174#entry174comment</comments>
      <pubDate>Sun, 11 Aug 2024 16:55:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / Error] Required request body is missing</title>
      <link>https://backend-jaamong.tistory.com/173</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Required reuqest body is missing`라는 문구와 함께 &lt;span style=&quot;text-align: start;&quot;&gt;`HttpMessageNotReadableException` 타입의 에러가 발생했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;`Controller`에 존재하는 메서드에서 `request body`를 인식하지 못하는 것으로 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720249961312&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class RestController {

    @PostMapping(&quot;/aaa&quot;)
    public ResponseEntity&amp;lt;Void&amp;gt; aaa(@RequestBody @Valid final RequestDto dto) {
				...
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

		...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 메서드에 `@RequestBody`를 적용하여 JSON 데이터를 받고 있다. 메서드 내부에서 로그를 찍어봤을 때 해당 값이 잘 넘어오는 것을 확인했으며 디버깅을 했을 때도 존재하는 것을 확인했다. 그러나 반환값이 `201`이 아닌 `403`으로 확인된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;디버깅을 진행했을 때 직접 작성한 코드 내에서는 반환값이 정상적으로 201로 저장되었다. 그러다가 신기하게도 &lt;b&gt;컨트롤러 전에 이미 거쳤을 `Filter`를 컨트롤러 이후에 또 방문&lt;/b&gt;하는 것을 발견했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;계속 디버깅을 하다 보니 `OncePerRequestFilter`를 상속받는 커스텀 필터인 `JwtAuthenticationFilter`에 문제가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring에서 모든 요청들은 컨트롤러에 도달하기 전에 Filter, Interceptor 등을 거친다. 직접 정의한 필터도 예외는 아니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 코드는 직접 정의한 필터이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720250209749&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        if(특정 조건) {
            filterChain.doFilter(request, response); //first doFilter
        }

        String token = getJwtFromRequest(request);
        if (StringUtils.hasText(token)) {
            ...
        }
        filterChain.doFilter(request, response); //second doFilter
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특정 조건인 경우에 토큰 검증 로직을 수행하지 않도록 바로 `doFilter`를 호출하는 코드인데, 여기에 문제가 있었다. 아무리 여기서 `doFilter`를 먼저 호출한다고 해도 결국은 메서드이므로 해당 메서드를 통해 컨트롤러까지 도달하여 나머지 로직을 수행하고 나면 다시 해당 필터로 돌아오게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제로 아래와 같이 로깅을 해놓고 문제가 되는 API를 호출하면 다음과 같이 콘솔창에 출력되는 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720250342585&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        if (특정 조건) {
             log.info(&quot;before first doFilter&quot;);
             filterChain.doFilter(request, response); //first doFilter
             log.info(&quot;after first doFilter&quot;);
        }
				
        log.info(&quot;here&quot;);
				
        String token = getJwtFromRequest(request);
        if (StringUtils.hasText(token)) { //특정 조건의 경우 토큰이 필요없는 요청이므로 통과 못함
            ...
        }
       
        log.info(&quot;before second doFilter&quot;);
        filterChain.doFilter(request, response); //second doFilter
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1720250368138&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
before first doFilter
after first doFilter
here
before second doFilter
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;첫 번째 `doFilter` 이후로도 두 번째 `doFilter`에 방문하는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 그다음 로직을 수행하게 되어 가장 아래에 있는 `doFilter`를 거치게 된다. 이렇게 필터를 두 번 거치고 나니 `ResponseEntity`의 `status code`가 `403`으로 변하게 되었다. 내부 로직을 세세하게 보지는 못했으나, 필터를 또 거치면서 어떤 조건을 충족하게 되어 상태 코드가 변한 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해결방법은 간단하다. 필터를 두 번 거치지 않도록 하면 되므로 특정 조건의 경우 `doFilter`를 수행하게 했던 코드를 제거한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720250679903&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        String token = getJwtFromRequest(request);
        if (StringUtils.hasText(token)) {
            ...
        }
        filterChain.doFilter(request, response); //second doFilter
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존에 특정 조건을 확인했던 코드는 쓸데없이 검증하지 않도록 작성했던 것이다. 그러나 이미 아래에 `if`문으로 토큰을 확인하며, 반드시 `doFilter`를 수행하므로 해당 코드는 필요가 없는 것으로 판단하여 제거했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 원하는 대로 정상 작동했다!&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;&lt;/b&gt;&lt;i&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://upcurvewave.tistory.com/614&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://upcurvewave.tistory.com/614&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>@RequestBody</category>
      <category>filter</category>
      <category>httpmessagenotreadableexception</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/173</guid>
      <comments>https://backend-jaamong.tistory.com/173#entry173comment</comments>
      <pubDate>Sat, 13 Jul 2024 09:26:20 +0900</pubDate>
    </item>
    <item>
      <title>Java에서 외부 프로그램 실행하기 (zt-exec)</title>
      <link>https://backend-jaamong.tistory.com/172</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트를 진행하면서 Java 환경에서 Python 프로그램을 실행해야 할 일이 생겼다. 해본 적이 없어서 할 수 있는지가 걱정이었는데 역시 안될 건 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;종류&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일단 외부 프로그램을 실행할 때 쓰이는 방법들을 찾아보았다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JSR-233 Scripting Engine&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jython&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ProcessBuilder&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Apache Common Exec(thrid-party lib)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ZT Process Executor&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTP (python server)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;JSR-233 Scripting Engine&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java 6 부터 지원&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;set of scripting APIs&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`CLASSPATH`에 `Jython`이 있어야 함&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jython의 경우 파이썬 라이브러리를 한정적으로 사용하게 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Jython&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바 코드에 파이썬 코드를 직접적으로 임베딩할 수 있음&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바 코드에 파이썬 코드가 섞여서 유지보수가 어려워짐&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Jython의 경우 파이썬 라이브러리를 한정적으로 사용하게 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ProcessBuilder&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바에서 외부 프로그램을 실행하고 관리하기 위한 도구&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작성된 파이썬 파일을 실행하는 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`assumption` PATH 변수를 통해 파이썬을 사용할 수 있어야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;안전하게 스트림을 처리하려면 3~4개 이상의 전용 클래스를 구현해야 함&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;&quot;Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, or even deadlock.&quot;&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Apache Common Exec&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ProcessBuilder 코드와 비슷함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작성된 파이썬 파일을 실행하는 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;일관된 API로 폭넓은 OS를 지원하는 것이 주요 철학&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ZT Process Executor&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`ProcessBuilder`, `common-exec` 기능 사용 가능&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Improved handling of stream&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;One liners to get process output into a String&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Improved logging with SLF4J API&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;only 3rd-party dependency is SLF4J&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  &lt;/span&gt;&lt;a href=&quot;https://github.com/zeroturnaround/zt-exec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃허브 주소&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;HTTP&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720247841495&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;python -m http.server 9000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또는 Flask나 Django를 이용하여 HTTP 통신을 하는 방법&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;&lt;span style=&quot;color: #000000;&quot;&gt;나는 이 중에서 `ZT Process Executor`를 선택했다. 제공하는 기능이 폭넓고 예제 코드도 나와있어서 좋았다. 또한 사용하기 위해 어떤 추가적인 라이브러리 등이 필요하지 않다는 점이 좋았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ZT Process Executor&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행될 파이썬 프로그램은 파일을 인자로 받으며, 자바는 파이썬을 호출하고 나서 해당 결과를 파싱 하여 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720248233255&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class FileHandler {

    private final FileService fileService;
	...

    public returnType pythonExecutor(MultipartFile file) {

        File saveFile = fileService.saveFile(file);
        String savedPath = saveFile.getSavedPath();
        final String pyPath = &quot;...&quot;;

        String[] commands = {&quot;python&quot;, pyPath, savedPath};
        String output = null;

        //ZT Process Executor를 이용한 python 파일 호출
        try {
            output = new ProcessExecutor().command(commands)
                    .readOutput(true).execute()
                    .outputUTF8();
        } catch (IOException | InterruptedException | TimeoutException e) {
            log.error(&quot;Process Executor Error occur: &quot;, e);
            throw new RuntimeException(e);
        }
        // 결과 파싱
        JSONObject result = parseJson(output);
        String field = result.get(&quot;원하는 필드&quot;); //이런식으로 파싱한 결과의 필드를 꺼낼 수 있음
        
		...
    }

    private JSONObject parseJson(String output) {
        final String jsonOuterKey = &quot;field&quot;;

        JSONParser parser = new JSONParser();
        JSONObject object;

        try {
            object = (JSONObject) parser.parse(output);
        } catch (ParseException e) {
            log.error(&quot;JSON Parse Error occur: &quot;, e);
            throw new RuntimeException(e);
        }

        return (JSONObject) object.get(jsonOuterKey);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`parseJson` 메서드에서 `jsonOuterKey`는 이런 경우를 위함이다. 겉에 아무것도 없다면 바로 꺼내면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720248493408&quot; class=&quot;json, jsonc jboss-cli&quot; data-ke-language=&quot;json, jsonc&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;field&quot; : {
	&quot;aaa&quot;: ...,
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자세한 ZT Process Executor 사용법은 깃허브 링크를 참고!&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;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>PYTHON</category>
      <category>zt process executor</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/172</guid>
      <comments>https://backend-jaamong.tistory.com/172#entry172comment</comments>
      <pubDate>Sat, 6 Jul 2024 15:51:11 +0900</pubDate>
    </item>
    <item>
      <title>[TIL / JPA] 데이터베이스 스키마 자동 생성, UNIQUE 제약 조건 관련 TIL</title>
      <link>https://backend-jaamong.tistory.com/171</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;JPA 데이터베이스 스키마 자동 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발 서버에서는 가급적 `none`&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 서버와 스테이징 서버에 `validate` 정도는 괜찮은 듯. 그래도 가급적 `none`을 권장. 혼자 개발하는 환경에서는 `validate` 정도는 괜찮지만, &lt;b&gt;여러 명이 함께 개발하는 곳에서는 `none`.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;실제 운영서버에서는 아예 `none`&lt;/b&gt;. 수많은 데이터가 있는 운영 서버에서 `ALTER` 쿼리 등을 잘못 입력하면 시스템이 중단 상태가 될 수도 있음. 그래서 가급적 &lt;b&gt;작성한 쿼리가 잘 동작하는지 테스트 서버에서 확인&lt;/b&gt;해봐야 함.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;운영 서버에 스크립트를 반영할 때 가급적이면 쿼리를 다듬어서 넘긴다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;필드와 컬럼 매핑&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`@Column`의 `unique=true` 제약 조건은 잘 사용하지 않는다. 사용 시 제약 조건 이름이 랜덤 생성되어 알아볼 수가 없어서 운영 환경에서 좋지 않음.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;UNIQUE 제약 조건을 거는 다른 방법은 `@Table(uniqueConstraints)`를 사용하는 것. 이 방식은 이름을 정할 수 있으므로 사용하기 좋다.&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;size14&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;출처&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인프런 - 김영한님의 자바 ORM 표준 JPA 프로그래밍 기본편&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>jpa 데이터베이스 스키마 자동 생성</category>
      <category>unique 제약 조건</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/171</guid>
      <comments>https://backend-jaamong.tistory.com/171#entry171comment</comments>
      <pubDate>Sun, 30 Jun 2024 08:07:10 +0900</pubDate>
    </item>
    <item>
      <title>[TIL / JPA] 영속성 컨텍스트</title>
      <link>https://backend-jaamong.tistory.com/170</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 글은 아래 강의를 바탕으로 공부한 내용을 정리하는 글입니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719031314038&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;자바 ORM 표준 JPA 프로그래밍 - 기본편 | 김영한 - 인프런&quot; data-og-description=&quot;김영한 | JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 실무에서도&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/ORM-JPA-Basic/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/ORM-JPA-Basic&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bq8Qnf/hyWoMdjPJZ/mkG4J39HtJU9naNISKKXuK/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/95T3n/hyWoI9PeDu/SayV1KR4xRDyA45RroYKO0/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/lUAKC/hyWoCu14ig/3Thi4jpRf4yLx2JK9VgkyK/img.png?width=1200&amp;amp;height=628&amp;amp;face=731_255_826_360&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/ORM-JPA-Basic/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/ORM-JPA-Basic/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bq8Qnf/hyWoMdjPJZ/mkG4J39HtJU9naNISKKXuK/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/95T3n/hyWoI9PeDu/SayV1KR4xRDyA45RroYKO0/img.png?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/lUAKC/hyWoCu14ig/3Thi4jpRf4yLx2JK9VgkyK/img.png?width=1200&amp;amp;height=628&amp;amp;face=731_255_826_360');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;자바 ORM 표준 JPA 프로그래밍 - 기본편 | 김영한 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;김영한 | JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 실무에서도&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 목차 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#no1&quot;&gt;영속성 컨텍스트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;#no2&quot;&gt;엔티티의 상태&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;#no3&quot;&gt;1차 캐시&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;#no4&quot;&gt;엔티티 등록 - 쓰기 지연&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;#no5&quot;&gt;변경 감지(Dirty Checking)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;#no6&quot;&gt;SUMMARY&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;no1&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;영속성 컨텍스트&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;엔티티를 영구 저장하는 환경&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;논리적 개념, 눈에 보이지 않음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager`를 통해서 접근&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager`는 엔티티를 조작하고 데이터베이스와의 통신을 수행하는 인터페이스. 엔티티의 영속성 컨텍스트를 관리하며, 데이터베이스로의 변경사항을 자동으로 동기화함.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager`를 생성하면 그 안에 영속성 컨텍스트가 1:1로 생성됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719031070069&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.*;

import java.util.List;

public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory(&quot;hello&quot;); 
        EntityManager em = emf.createEntityManager(); 
        
        EntityTransaction tx = em.getTransaction(); //JPA는 반드시 트랜잭션 안에서 작업
        tx.begin();

        try {
            ...
            em.persist(entity);
            tx.commit(); //정상 종료가 되면 커밋
        } catch (Exception e) {
            tx.rollback(); //문제가 생기면 롤백
        } finally {
            em.close(); //작업 종료가 되면 entity manager 닫기
        }

        emf.close(); //애플리케이션이 모두 종료되면 entity manager factory 닫기
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManagerFactory`는 애플리케이션(DB) 당 1개만 생성해야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager`는 고객 요청이 올 때마다 쓰고 닫아야 함&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주의! 스레드 간에 공유 X&amp;nbsp;&amp;rarr; 장애 발생. 사용하고 버려야 함.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`em.persist(entity)`&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;영속성 컨텍스트를 통해서 엔티티를 영속화하는 코드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엔티티를 DB에 저장하는 것이 아닌 영속성 컨텍스트에 저장하는 것&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager`는 데이터베이스 커넥션을 가지고 동작하기 때문에 작업이 끝나면 반드시 닫아줘야 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;no2&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;엔티티의 상태&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;비영속&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;객체 생성 후 `EntityManager`에 넣지 않음. JPA와 전혀 관계없는 상태.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;영속&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager`안에는 영속성 컨텍스트가 있다. `EntityManager.persist(Entity)`를 하면 해당 엔티티가 영속성 컨텍스트에 들어가면서 영속 상태가 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719031954065&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        try {
            // ---비영속 상태---
            Member member = new Member();
            member.setId(1L);
            member.setName(&quot;member&quot;);

            // --- persist -&amp;gt; 영속 상태 ---
            System.out.println(&quot;=== BEFORE ===&quot;);
            em.persist(member); //영속성 컨텍스트를 통해서 member 객체가 관리됨. 여기서 insert 쿼리 안나감
            System.out.println(&quot;=== AFTER ===&quot;);

            tx.commit(); //영속 상태가 되고 바로 DB에 저장되는 것이 아닌 트랜잭션을 커밋하는 시점에 영속성 컨텍스트에 있는 쿼리가 DB로 나감
        }
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;준영속 상태&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;객체를 아래 코드처럼 영속성 컨텍스트에서 지우면(detach) 아무런 관계가 없게 됨&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719032115613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EntityManager.detach(Entity);&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;i&gt;Note&lt;/i&gt;&lt;/span&gt;&amp;nbsp; &lt;/b&gt;em.persist()를 해도 영속 상태가 되고, JPA를 통해 조회(em.find()) 했을 때 해당 객체가 영속성 컨텍스트에 없어서 1차 캐시에 넣는 것도 영속 상태가 되는 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;no3&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;1차 캐시&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다. 예를 들어 객체를 생성한 상태는 비영속, `EntityManager.persist(Entity)`를 하면 영속 상태가 된다. 이렇게 `EntityManager`를 통해 영속성 컨텍스트에 넣으면 내부에 존재하는 1차 캐시에 엔티티를 넣어둔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1차 캐시에서 조회&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1719032542663&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Member member = new Member();
member.setId(&quot;member1&quot;);
member.setName(&quot;회원1&quot;);

//1차 캐시에 저장
EntityManager.persist(member); 
//1차 캐시에서 조회
EntityManager.find(Member.class, &quot;member1&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager.find()`를 하면 DB에서 찾는 것이 아닌 먼저 1차 캐시에서 찾는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1차 캐시에 찾는 값이 존재하면 DB가 아닌 여기에서 값을 조회한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1차 캐시에 없으면 DB에서 조회하고 1차 캐시에 저장한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 해당 객체를 찾으면 1차 캐시에서 조회할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager`는 DB 트랜잭션 단위로 생성되고 트랜잭션이 종료되면 EntityManager도 같이 종료된다. 즉, 고객 요청이 &lt;b&gt;들어와서 비즈니스 로직이 끝나면 해당 영속성 컨텍스트를 지운다는 의미이다. 따라서 1차 캐시도 사라지게 된다.&lt;/b&gt; 이러한 찰나의 순간에서만 이점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;no4&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;엔티티 등록 - 쓰기 지연&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;영속성 컨텍스트에는 `1차 캐시` 외에&amp;nbsp; `쓰기 지연 SQL 저장소`라는 것이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager.persist(entity1)`를 하면 1차 캐시에 `entity1`가 들어간다. 동시에 JPA가 해당 엔티티를 분석하여 `INSERT` 쿼리를 생성하고 이를 쓰기 지연 SQL 저장소에 쌓아둔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`EntityManager.persist(entity2)`를 하면 1차 캐시에 `entity2`가 들어간다. 동시에 JPA가 해당 엔티티를 분석하여 `INSERT` 쿼리를 생성하고 마찬가지로 이를 쓰기 지연 SQL 저장소에 쌓아둔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저장소에 있던 쿼리들은 트랜잭션을 커밋하는 시점에 DB로 날아간다&lt;span style=&quot;color: #666666;&quot;&gt;(&lt;i&gt;flush&lt;/i&gt;)&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쓰기 지연 SQL 저장소에 원하는 만큼 쿼리를 모아서 한 번에 DB에 보내는 방법도 있다. 이를 `JDBC Batch`라고 한다. 하이버네이트에는 아래와 같은 옵션이 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719033144855&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 하면 해당 사이즈만큼 쿼리를 모아서 한 번의 네트워크로 DB에 쿼리를 보낸다. &lt;span style=&quot;color: #666666;&quot;&gt;(버퍼링 같은 기능)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;no5&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;변경 감지(Dirty Checking)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JPA는 DB 트랜잭션 커밋 시점에 영속성 컨텍스트 내부에 있는 `flush()`라는 것이 호출된다. 이때 엔티티와 스냅샷을 비교한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1차 캐시 안에는 PK인 `@Id`와 `Entity`, `스냅샷`이 있다. 스냅샷은 값이 영속성 컨텍스트에 들어온 &lt;b&gt;최초 시점의 상태&lt;/b&gt;를 저장해 둔 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 상태에서 엔티티의 값이 변경되면 트랜잭션 커밋 시점에 `flush()`가 호출되면서 JPA가 Entity와 스냅샷을 비교한다. 비교하고 값이 변경되었으면 `UPDATE` 쿼리를 쓰기 지연 SQL 저장소에 둔다. 그리고 해당 쿼리를 DB에 반영하고 커밋하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;flush&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`flush`는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 것으로, 영속성 컨텍스트의 쿼리들을 DB에 날리는 것이다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;`flush`가 발생하면 아래와 같은 일이 일어난다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB 트랜잭션이 커밋되면 `flush`가 자동으로 발생&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;변경 감지(dirty checking)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정된 엔티티와 관련된 update 쿼리를 쓰기 지연 SQL 저장소에 등록&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쓰기 지연 SQL 저장소의 쿼리를 DB에 전송(등록, 수정, 삭제 쿼리 등)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;i&gt;Note&lt;/i&gt;&lt;/span&gt;&amp;nbsp; &lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;flush를 해도 1차 캐시는 유지된다. 영속성 컨텍스트의 변경내용을 DB에 동기화하는 것뿐.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;no6&quot; data-ke-size=&quot;size26&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;SUMMARY - 배운 것 조합하기&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;`em.persist()`를 하고 `em.find()`를 바로 호출하면, 영속성 컨텍스트에 있는 데이터를 가지고 오기 때문에 조회 쿼리를 볼 수 없다. 이때 `em.flush()`, `em.clear()`를 하면 DB에 데이터를 반영하고, 영속성 컨텍스트를 초기화한다. 따라서 `em.find()`를 호출하면 영속성 컨텍스트에 데이터가 없으므로 DB에서 데이터를 조회하게 된다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin</category>
      <category>1차 캐시</category>
      <category>entitymanager</category>
      <category>flush</category>
      <category>jpa</category>
      <category>변경 감지</category>
      <category>쓰기 지연 SQL 저장소</category>
      <category>영속성 컨텍스트</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/170</guid>
      <comments>https://backend-jaamong.tistory.com/170#entry170comment</comments>
      <pubDate>Sat, 29 Jun 2024 09:47:10 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] 예외 처리하기</title>
      <link>https://backend-jaamong.tistory.com/169</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffc9af; color: #000000;&quot;&gt;&lt;b&gt;Notice&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&amp;nbsp;&lt;/i&gt;&lt;span style=&quot;background-color: #ffffff; color: #ef6f53; text-align: start;&quot;&gt;Spring Boot 3.2.5&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #ef6f53; text-align: start;&quot;&gt;Spring Security 6.2.5&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기반으로 작성한 글입니다.&lt;/span&gt;&lt;/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;span style=&quot;color: #000000;&quot;&gt;아래 포스팅들을 먼저 보면 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/166&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Security 6 - Architecture&lt;/a&gt; &lt;i&gt;&amp;rarr; Security Exception 처리하기&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://backend-jaamong.tistory.com/167&quot;&gt;[Spring Security] 토큰 기반 로그인/로그아웃 구현하기&lt;/a&gt; &amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 이 글의 목표는 Spring Security에서 토큰 기반 인증을 진행할 때 발생한 예외를 `@RestControllerAdvice`로 처리하는 것입니다.&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;시큐리티 예외 처리 구현&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security에서 예외 처리는 다음과 같은 아키텍처로 이루어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;FilterChain&lt;br /&gt;&lt;/b&gt;Spring Security는 `FilterChain`으로 구성되어 있으며, 각 필터는 요청을 처리하거나 다음 필터로 전달한다. 예외가 발생하면 예외 처리 필터가 작동한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ExceptionTranslationFilter&lt;br /&gt;&lt;/b&gt;해당 필터는 예외를 Spring Security 예외로 변환하고, 적절한 `AuthenticationEntryPoint` 또는 `AccessDeniedHandler`를 호출한다. 이 필터는 이 둘을 주입받아 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;AuthenticationEntryPoint&lt;br /&gt;&lt;/b&gt;`AuthenticationEntryPoint`는 인증되지 않은 요청에 대한 처리를 담당한다. 일반적으로 로그인 페이지로 리디렉션 하거나 &lt;i&gt;401 Unauthorized&lt;/i&gt; 응답을 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;AccessDeniedHandler&lt;br /&gt;&lt;/b&gt;`AccessDeniedHandler`는 인가되지 않은 요청(권한 부족)에 대한 처리를 담당한다. 일반적으로 &lt;i&gt;403 Forbidden&lt;/i&gt; 응답을 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 Spring Security 예외 처리 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청이 들어온다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;FilterChain에서 예외가 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ExceptionTranslationFilter가 예외를 캐치하고, Spring Security 예외로 변환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AuthenticationEntryPoint 또는 AccessDeniedHandler가 호출되어 예외를 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 과정처럼 Spring Security는 요청이 컨트롤러에 도달하기 전에 `FilterChain`에서 예외를 발생시킨다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`@RestControllerAdvice`는 컨트롤러 계층에서 발생하는 예외를 처리하는데, 시큐리티 예외는 컨트롤러에 도달하기 전에 발생하므로 해당 애노테이션으로 처리할 수 없다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 원하는 대로 처리하려면 예외를 가로채는 `AuthenticationEntryPoint`와 `AccessDeniedHandler`를 사용자 정의하여 구현하면 된다. 본격적으로 구현하기 전에 Security Configuration을 설정하자.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SecurityConfig&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1717741533267&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    ...

    private final CustomAuthenticationEntryPoint authenticationEntryPoint;
    private final CustomAccessDeniedHandler accessDeniedHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                ...
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(e -&amp;gt; e
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler)
                )
                ...
                ;

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000;&quot;&gt;Custom AuthenticationEntryPoint&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`AuthenticationEntryPoint` 인터페이스는 인증되지 않은 사용자가 인증이 필요한 요청 엔드포인트로 접근하려 할 때 발생하는 &lt;i&gt;401 Unauthorized&lt;/i&gt; 예외를 핸들링 할 수 있도록 도와준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717741677706&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final HandlerExceptionResolver resolver;

    public CustomAuthenticationEntryPoint(@Qualifier(&quot;handlerExceptionResolver&quot;) HandlerExceptionResolver resolver) {
        this.resolver = resolver;
    }

    @Override
    public void commence(@NonNull HttpServletRequest request,
                         @NonNull HttpServletResponse response,
                         @NonNull AuthenticationException authException) {

        resolver.resolveException(request, response, null, authException);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`commence` 메서드는 인증되지 않은 요청이 발생했을 때 호출된다. 여기에서 예외를 처리하는 것이 아닌 `HandlerExceptionResolver`로 넘긴다. 관련 내용은 아래에서 자세하게 다룬다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Custom AccessDeniedHandler&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`AccessDeniedHandler` 인터페이스는 권한이 없는 사용자가 권한이 필요한 요청 엔드포인트로 접근하려 할 때 발생하는 &lt;i&gt;403 Forbidden&lt;/i&gt; 예외를 핸들링 할 수 있도록 도와준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717741758893&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final HandlerExceptionResolver resolver;

    public CustomAccessDeniedHandler(@Qualifier(&quot;handlerExceptionResolver&quot;) HandlerExceptionResolver resolver) {
        this.resolver = resolver;
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        resolver.resolveException(request, response, null, accessDeniedException);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`handle` 메서드는 권한이 없는 요청이 발생했을 때 호출된다. `AccessDeniedHandler`와 마찬가지로 여기에서 예외를 처리하는 것이 아닌 `HandlerExceptionResolver`로 넘긴다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;HandlerExceptionResovler&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`HandlerExceptionResolver`는 Spring Security의 영역이 아닌 Spring MVC 영역에 속해있는 컴포넌트이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring MVC에서 `HandlerExceptionResolver`는 `DispatcherServlet`의 `HandlerExceptionResolver` 체인(예외 처리 체인)에 등록되어 있다. 이 체인은 &lt;b&gt;컨트롤러에서 발생한 예외를 처리&lt;/b&gt;하는 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 각 커스텀 구현한 `AuthenticationEntryPoint`와 `AccessDeniedHandler`에서 `HandlerExceptionResolver`를 호출하여 컨트롤러에서 예외를 처리할 수 있도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717741858691&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HandlerExceptionResolver.resolveException(request, response, null, accessDeniedException);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로 위 코드처럼 `handler`를 `null`로 반환하면 다음 `ExceptionResolver`를 찾아서 실행한다. 만약 처리할 수 없는 `ExceptionResolver`가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리는 `null`을 반환하여 `@RestControllerAdvice`와 `@ExceptionHandler`를 사용하는 클래스에서 예외를 처리하도록 한다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Custom ExceptionHandler&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이곳에서 Spring Security 예외를 처리한다. 보다 깔끔하게 예외를 가공하고 처리하기 위해서 컨트롤러 계층까지 예외를 가져왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`@RestControllerAdvice`를 사용하여 반환하는 값을 Response Body로 설정하고, 이를 클라이언트에게 전달하도록 한다. `@ExceptionHandler`로 각 적절한 예외를 처리할 수 있도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717742003507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler
    public ResponseEntity&amp;lt;ExceptionResponseDto&amp;gt; handleAuthenticationException(AuthenticationException e) {
        ExceptionResponseDto dto = new ExceptionResponseDto(HttpStatus.UNAUTHORIZED.name(), ExceptionCode.INVALID_TOKEN.getMessage());
        HttpStatus httpStatus = HttpStatus.UNAUTHORIZED;
        return ResponseEntity.status(httpStatus).body(dto);
    }

    @ExceptionHandler
    public ResponseEntity&amp;lt;ExceptionResponseDto&amp;gt; handleAccessDeniedException(AccessDeniedException e) {
        ExceptionResponseDto dto = new ExceptionResponseDto(HttpStatus.FORBIDDEN.name(), ExceptionCode.ACCESS_DENIED.getMessage());
        HttpStatus httpStatus = HttpStatus.FORBIDDEN;
        return ResponseEntity.status(httpStatus).body(dto);
    }
		
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트에게 예외를 반환할 때 필요한 정보만 담을 수 있도록 별도로 DTO를 생성했다.&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;&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;i&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 참고&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-security-exceptionhandler&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.baeldung.com/spring-security-exceptionhandler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@jsang_log/Security-Filter-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-JWT&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@jsang_log/Security-Filter-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-JWT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://thalals.tistory.com/451&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://thalals.tistory.com/451&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://beaniejoy.tistory.com/93&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://beaniejoy.tistory.com/93&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hoonsmemory.tistory.com/16&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hoonsmemory.tistory.com/16&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>@exception handler</category>
      <category>AccessDeniedException</category>
      <category>AccessDeniedHandler</category>
      <category>AuthenticationEntryPoint</category>
      <category>AuthenticationException</category>
      <category>exception</category>
      <category>spring security 6</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/169</guid>
      <comments>https://backend-jaamong.tistory.com/169#entry169comment</comments>
      <pubDate>Fri, 21 Jun 2024 21:39:59 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] 역할/권한 구현하기</title>
      <link>https://backend-jaamong.tistory.com/168</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffc9af; color: #000000;&quot;&gt;&lt;b&gt;Notice&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&amp;nbsp;&lt;/i&gt;&lt;span style=&quot;background-color: #ffffff; color: #ef6f53; text-align: start;&quot;&gt;Spring Boot 3.2.5&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #ef6f53; text-align: start;&quot;&gt;Spring Security 6.2.5&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기반으로 작성한 글입니다.&lt;/span&gt;&lt;/p&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: #000000;&quot;&gt;지난 포스팅에서 이어집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring Security]토큰 기반 로그인/로그아웃 구현하기&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;역할과 권한&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자의 역할(role)과 권한(permission/authority)을 구현하여 기능 접근에 제약을 둘 수 있다. 만일 권한이 없는 기능에 접근하면 &lt;i&gt;403 Forbidden&lt;/i&gt;이 발생한다. 역할 및 권한 구현 시 다음과 같은 것들을 고려해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한 역할은 여러 개의 권한을 가질 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러 명의 사용자는 여러 개의 역할을 가질 수 있다.&amp;nbsp;&amp;rarr; 다대다 관계&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;역할을 하나의 Entity로 두어 구현할 수 있지만, 이 경우에 고려할 것이 많아진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다대다 관계 매핑 &amp;rarr; `@ManyToMany`&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중간 테이블을 두어 일대다, 다대일로 풀어내기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다대다 관계를 중간 테이블로 해결하는 것이 권장되지만, 이렇게 하면 역할과 관련된 로직이 복잡해진다. 그래서 나는 역할을 Enum 클래스로 구현했다. 마찬가지로 권한도 Enum 클래스로 구현한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Enum 클래스로 구현해도 역할은 여러 권한을 가질 수 있고, 사용자도 여러 역할을 갖도록 구현할 수 있다. 여기에서는 사용자가 한 역할만 갖고 있도록 구현했으나, 권한을 역할에 따라 부여하도록 구현했으므로 문제없다. 만일 권한을 세부적으로 정하지 않고 역할로만 기능 접근을 설정한다면 사용자가 여러 역할을 갖도록 하는 것이 좋다.&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구현하기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Permission Enum class&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 기능을 수행할 수 있는지 나타내는 Permission(권한) 클래스이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717735192776&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public enum Permission {

    READ(&quot;READ_AUTHORITY&quot;),
    CREATE(&quot;CREATE_AUTHORITY&quot;),
    UPDATE(&quot;UPDATE_AUTHORITY&quot;),
    DELETE(&quot;DELETE_AUTHORITY&quot;);

    private final String permission;

    Permission(String permission) {
        this.permission = permission;
    }
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Role Enum class&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 역할 별로 어떤 권한이 있는지 나타낸다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717735424093&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public enum Role {
    ADMIN(Set.of(
            READ,  //Permission enum class
            CREATE,
            UPDATE,
            DELETE
    )),
    MANAGER(Set.of(
            READ,
            CREATE,
            UPDATE
    )),
    USER(Set.of(
            READ
    ));

    private final Set&amp;lt;Permission&amp;gt; permissions;  //Permission enum class

    Role(Set&amp;lt;Permission&amp;gt; permissions) {
        this.permissions = permissions;
    }

    public List&amp;lt;SimpleGrantedAuthority&amp;gt; getAuthorities() {
        List&amp;lt;SimpleGrantedAuthority&amp;gt; authorities = getPermissions()
                .stream()
                .map(permission -&amp;gt; new SimpleGrantedAuthority(permission.getPermission()))
                .collect(Collectors.toList());

        authorities.add(new SimpleGrantedAuthority(&quot;ROLE_&quot; + this.name()));

        return authorities;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`getAuthorities()`는 역할이 어떤 권한(permission)을 가지고 있는지 반환하는 메서드이다. 이는 `UserDetailsService` 구현체의 `loadUserByUsername`에서 `User` 객체를 생성할 때 사용자가 어떤 권한을 가지고 있는지 반환할 때 사용될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717735547453&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Slf4j
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserEntity user = userRepository.findByUsername(username)
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));

        return new User( //사용자 정보를 담는 객체에 권한도 포함
                user.getUsername(),
                user.getPassword(),
                mapAuthorities(user.getRole()) 
        );
    }

    private Collection&amp;lt;? extends GrantedAuthority&amp;gt; mapAuthorities(Role role) {
        return role.getAuthorities();  //here
    }
}&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;User Entity class&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 구현된 역할은 `User` 엔티티의 필드가 된다. DB에 저장할 때는 문자열로 저장되도록 설정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717735584532&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Entity
@Table(name = &quot;users&quot;)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;user_id&quot;)
    private Long id;

		...

    @Enumerated(EnumType.STRING)
    private Role role;

    @Builder
    public UserEntity(String username, String password, Role role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로 `User` 엔티티가 여러 역할(Role Enum class)를 갖도록 할 때는 아래처럼 하면 되지 않을까?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717735948088&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = &quot;users&quot;)
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
	...

    @Enumerated(EnumType.STRING)
    private Set&amp;lt;Role&amp;gt; roles = new HashSet&amp;lt;&amp;gt;();

    // Constructors, getters, and setters
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다시 돌아와서.. 프로젝트 요구사항에 역할 변경 기능이 없어서 별도로 구현하지 않았으나, 필요하다면 `setter`처럼 구현하면 될 것 같다. (사용자가 여러 개의 역할을 가질 수 있으면 역할 추가, 삭제로 나뉠 수 있다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 부분은 프로젝트 마다 다르므로, 프로젝트 요구사항을 바탕으로 설계할 때 적절하게 코드를 작성하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 `User` 엔티티의 로직과 별개로 이를 수행할 서비스 계층의 비즈니스 로직이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 해당 역할 및 권한을 가진 사용자만 엔드포인트에 접근할 수 있도록 설정한다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Security Configuration&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1717738151675&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final String[] PERMIT_ALL_URL = {...};

    ...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                ...
                .authorizeHttpRequests(req -&amp;gt; req
                                .requestMatchers(PERMIT_ALL_URL).permitAll()
                                .requestMatchers(HttpMethod.GET, &quot;/api/boards/**&quot;).hasAnyAuthority(READ.name())
                                .requestMatchers(HttpMethod.POST, &quot;/api/boards/**&quot;).hasAnyAuthority(CREATE.name())
                                .requestMatchers(HttpMethod.PUT, &quot;/api/boards/**&quot;).hasAnyAuthority(UPDATE.name())
                                .requestMatchers(HttpMethod.DELETE, &quot;/api/boards/**&quot;).hasAnyAuthority(DELETE.name())
                                .anyRequest()
                                .authenticated()
                )
                ...;

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`requestMatchers`에 HTTP 메소드와 엔드포인트에 따라 어떤 권한(Authority)이 필요한지 `hasAnyAuthority`를 사용하여 설정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 security configuration 클래스에서 설정하는 방식 외에 컨트롤러에서 설정할 수 있는 방법도 있다.&amp;nbsp;&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1717738610055&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/boards&quot;)
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    @GetMapping
    @PreAuthorize(&quot;hasAuthority('READ_AUTHORITY')&quot;)
    public ResponseEntity&amp;lt;..&amp;gt; readAllBoard() {
        // 필요 로직 실행
        return ResponseEntity.ok(..);
    }

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 게시물을 조회할 수 있는 `GET /api/boards` URL이 매핑된 메서드는 &lt;i&gt;READ_AUTHORITY&lt;/i&gt;가 없으면 접근할 수 없도록 `@PreAuthorize`를 사용하여 설정했다. 이렇게 각 메서드 별로 필요한 권한을 지정해 줄 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 애노테이션을 사용할 때는 `@Configuration`이 적용된 security configuration 클래스에 `@EnabledMethodSecurity` 애노테이션을 추가해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717738810674&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableMethodSecurity  //add this
public class SecurityConfig {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 컨트롤러에 적용했으면 configuration 클래스에 작성한 `requestMatcher` 부분은 제거해도 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717738892079&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final String[] PERMIT_ALL_URL = {...};

    ...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                ...
                .authorizeHttpRequests(req -&amp;gt; req
                                .requestMatchers(PERMIT_ALL_URL).permitAll()
//                        .requestMatchers(HttpMethod.GET, &quot;/api/boards/**&quot;).hasAnyAuthority(READ.name())
//                        .requestMatchers(HttpMethod.POST, &quot;/api/boards/**&quot;).hasAnyAuthority(CREATE.name())
//                        .requestMatchers(HttpMethod.PUT, &quot;/api/boards/**&quot;).hasAnyAuthority(UPDATE.name())
//                        .requestMatchers(HttpMethod.DELETE, &quot;/api/boards/**&quot;).hasAnyAuthority(DELETE.name())
                                .anyRequest()
                                .authenticated()
                )
                ...;

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;@EnableMethodSecurity&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`@Configuration` 클래스에 해당 어노테이션을 달면 Spring이 관리하는 클래스 또는 메소드에 `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, `@PostFilter` 어노테이션을 달아서 입력 매개변수와 반환값을 포함한 메서드 호출을 승인(authorize)할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 특정 컨트롤러에 관리자(ADMIN) 역할을 지닌 사용자만 접근하도록 하고 싶다면 아래와 같이 작성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717739575431&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@PreAuthorize(&quot;hasRole('ADMIN')&quot;)
public class AdminController {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;관리자 역할을 지닌 사용자 중에서도 특정 권한을 갖고 있지 않으면 메서드에 접근하지 못하도록 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717739604528&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@PreAuthorize(&quot;hasRole('ADMIN')&quot;)
public class AdminController {
	
	...
	
	@PostMapping
	@PreAuthorize(&quot;hasAuthority('ADMIN_CREATE')&quot;)
	public void create(...) {
		...
	}
}&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;&lt;i&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; request-level(declared in a config class) 인증과의 차이점은 메서드 방식이 좀 더 세밀하게 인증 형식을 설정할 수 있다는 정도다. 애노테이션 인증 관련 자세한 내용은 여기&lt;/span&gt;&amp;nbsp;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#request-vs-method&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 참고!&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;&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;color: #000000;&quot;&gt; &lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;참고&lt;/i&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kimji0139.tistory.com/104&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kimji0139.tistory.com/104&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/mq5oUXcAXL4?feature=shared&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/mq5oUXcAXL4?feature=shared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/role-and-privilege-for-spring-security-registration&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.baeldung.com/role-and-privilege-for-spring-security-registration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>@enablemethodsecurity</category>
      <category>AUTHORITY</category>
      <category>Role</category>
      <category>spring security 6</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/168</guid>
      <comments>https://backend-jaamong.tistory.com/168#entry168comment</comments>
      <pubDate>Fri, 14 Jun 2024 20:00:58 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] 토큰 기반 로그인/로그아웃 구현하기</title>
      <link>https://backend-jaamong.tistory.com/167</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Notice&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&amp;nbsp;&lt;/i&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;Spring Boot 3.2.5&lt;/span&gt;, &lt;span style=&quot;color: #ef6f53;&quot;&gt;Spring Security 6.2.5&lt;/span&gt; 기반으로 작성한 글입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;i&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Notice&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&lt;/i&gt; 24.07.06 로그인 구현 부분 `JwtAuthenticationFilter` 잘못된 내용 수정 &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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Security Authentication Architecture&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddBRTq/btsHPQ2WBB4/KB2mLAcZYIB45FR4KZXgsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddBRTq/btsHPQ2WBB4/KB2mLAcZYIB45FR4KZXgsK/img.png&quot; data-alt=&quot;https://memodayoungee.tistory.com/135&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddBRTq/btsHPQ2WBB4/KB2mLAcZYIB45FR4KZXgsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddBRTq%2FbtsHPQ2WBB4%2FKB2mLAcZYIB45FR4KZXgsK%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;600&quot; height=&quot;415&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://memodayoungee.tistory.com/135&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security를 기반으로 보안 관련 기능을 구현하기 위해서는 위의 그림을 이해해야 한다. 아래는 로그인을 예시로 한 Spring Security 인증 과정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 사용자가 로그인 정보와 함께 인증을 요청한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. `AuthenticationFilter`가 요청을 가로채어 인증에 사용될&lt;/span&gt; &lt;a href=&quot;https://docs.spring.io/spring-security/site/docs/4.0.x/apidocs/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;UsernamePasswordAuthenticationToken&lt;/a&gt; &lt;span style=&quot;color: #000000;&quot;&gt;객체를 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717727096191&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;
    private final AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        String token = getJwtFromRequest(request);
        if (StringUtils.hasText(token)) {

            /*
	            JWT 유효성 검사 로직
            */
            
            String username = tokenProvider.extractUsername(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            // 인증용 토큰 객체 생성
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    userDetails.getPassword(),
                    userDetails.getAuthorities()
            );
	          ...
           }
       ...
       }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.`AuthenticationManager`의 구현체인 `ProviderManager`에게 인증 토큰 객체를 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;&lt;i&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; &lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-authenticationmanager&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AuthenticationManager&lt;/a&gt;는 Spring Security 필터가 어떻게 인증을 수행해야 하는지 정의한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717727254977&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AuthenticationManager의 구현체인 ProviderManager를 빈으로 등록
// ProviderManager에게 커스텀 AuthenticationProvider를 전달

    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
        JwtAuthenticationProvider provider = new JwtAuthenticationProvider(userDetailsService, passwordEncoder());
        return new ProviderManager(provider);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. `ProviderManager`는 등록된 `AuthenticationProvider`를 조회하고, 인증을 시도한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5mf10/btsHRnd3Mmo/B0oEOdMBFyFkVASvZLnvvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5mf10/btsHRnd3Mmo/B0oEOdMBFyFkVASvZLnvvK/img.png&quot; data-alt=&quot;ProviderManager&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5mf10/btsHRnd3Mmo/B0oEOdMBFyFkVASvZLnvvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5mf10%2FbtsHRnd3Mmo%2FB0oEOdMBFyFkVASvZLnvvK%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;600&quot; height=&quot;292&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ProviderManager&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 `AuthenticationProvider`는 특정 타입의 인증을 어떻게 수행하는지 알고 있으므로 어떤 `AuthenticationProvider`는 username/password 검증을 하거나 SAML assertion을 인증할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717727415023&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ProviderManager(AuthenticationProvider... providers) {	
	...
	public List&amp;lt;AuthenticationProvider&amp;gt; getProviders() {
        return this.providers;
  }
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5, 실제 DB를 통해 사용자의 인증 정보를 가져오는 `UserDetailsService`에게 인증 토큰 객체에 담긴 사용자 정보를 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717727467370&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;

        String username = authToken.getName();
        if (username == null) {
            throw new UsernameNotFoundException(&quot;Invalid username or password&quot;);
        }

        // UserDetailsService에게 사용자 정보 전달
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
       
	      ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 받은 사용자 정보를 이용하여 DB에서 해당 사용자 정보를 찾는다. 찾은 사용자 정보를 바탕으로 `UserDetails` 객체를 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717727535739&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

       //DB에서 사용자 정보 조회
        UserEntity user = userRepository.findByUsername(username)
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));

        //UserDetails 객체 생성
        return new User(
                user.getUsername(),
                user.getPassword(),
                mapAuthorities(user.getRole())
        );
    }

    //사용자가 가진 권한 반환
    private Collection&amp;lt;? extends GrantedAuthority&amp;gt; mapAuthorities(Role role) {
        return role.getAuthorities();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. `AuthenticationProvider`는 생성한 `UserDetails` 객체를 반환받아, 해당 객체와 인증 토큰 객체에 담긴 사용자 정보를 비교한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717727597317&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        ...        
        // UserDetailsService에게 사용자 정보 전달
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        String password = userDetails.getPassword();
       
        // 사용자 정보 비교
        // verifyCredentials: 비밀번호를 검증하는 커스텀 메서드 - 보통 PasswordEncoder#matches 사용
        verifyCredentials(password, (String) authToken.getCredentials());
        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. 인증이 완료되면 권한을 포함하여 사용자 정보를 담은 `Authentication` 객체를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성할 `Authentication` 객체에는 보안을 위해 비밀번호(credentials)를 담지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 `Authentication` 객체는 `UsernamePasswordAuthenticationToken`으로 구현할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1717727722567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

	      ...        
	      // UserDetailsService에게 사용자 정보 전달
	      ...
       
	      // 사용자 정보 비교
	      ...
				
	      // 인증 토큰 발급 및 반환
	      return new UsernamePasswordAuthenticationToken(
                 username,
                 null,
                 userDetails.getAuthorities()
	      );				
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9. 돌아가서 `AuthenticationFilter`에 `Authentication` 객체가 반환된다.&lt;/span&gt; &lt;span style=&quot;color: #666666;&quot;&gt;[인증 완료]&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717727793854&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;
    private final AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        String token = getJwtFromRequest(request);
        if (StringUtils.hasText(token)) {
						
            ...
						            
            // 인증용 토큰 객체 생성: authenticationToken 
            ...
            
            // 인증 시도 -&amp;gt; 인증 완료: authentication 객체 받음
            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            
            ...
           }
       ...
       }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. `SecurityContextHolder`의 세션 영역에 있는 `SecurityContext`에 `Authentication` 객체를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;&lt;i&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 사용자 정보를 저장하는 것은 Spring Security가 &lt;b&gt;전통적인 세션-쿠키 기반의 인증 방식&lt;/b&gt;을 사용함을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717727913418&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;
    private final AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        String token = getJwtFromRequest(request);
        if (StringUtils.hasText(token)) {
            ...
						            
            // 인증용 토큰 객체 생성: authenticationToken 
            ...
            
            // 인증 시도 -&amp;gt; 인증 완료: authentication 객체 받음
            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            
            // 인증 객체 설정
            SecurityContextHolder.getContext().setAuthentication(authentication);

            ...
           }
       ...
       }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;rArr; 위 모든 과정을 거치고 나서 `Dispatcher Servlet`으로 요청을 넘긴다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717728034260&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;
    private final AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        String token = getJwtFromRequest(request);
        if (StringUtils.hasText(token)) {
                // 4 ~ 10 과정
        }
        //요청 넘김 (애플리케이션의 나머지 동작 수행)
        filterChain.doFilter(request, response);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;모든 필터를 통과하고 애플리케이션에서 할 일을 마친 후 바로 클라이언트로 응답이 가는 것이 아닌, 다시 필터를 통해서 돌아가는 것을 기억합시다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;토큰 기반 로그인(인증) 구현&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security는 기본적으로 `formLogin`을 제공하는데 이는 HTML 폼 기반으로 REST API가 필요할 때는 별도로 구현해야 한다. 또한 JWT를 사용하므로 security configuration 클래스에 관련 설정을 해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717740410079&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    ...
   
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .csrf(AbstractHttpConfigurer::disable)
                .headers(headers -&amp;gt; headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))  // H2 웹콘솔 iframe 정상 작동을 위해 같은 Origin에 대해 허용
                .sessionManagement(session -&amp;gt; session.sessionCreationPolicy(STATELESS)) //here
                ...
                ;

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 토큰 기반의 인증을 구현하므로 사용자의 로그인 요청이 성공하면 Access Token과 Refresh Token을 발급하도록 구현한다. 로그인 요청 시에는 토큰 인증이 아닌 로그인 요청 정보(Response Body)만 검증하도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 로그인 요청인 경우에는 Header에 토큰이 없으므로 &lt;span style=&quot;text-align: start;&quot;&gt;바로 다음 필터로 넘어가게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717729075715&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    ...
    
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

				
                // JWT 검사 및 인증 객체 설정
                String token = getJwtFromRequest(request);
                if (StringUtils.hasText(token)) {
                	//Header에 JWT가 있는 요청들은 이 부분 실행
                	...
                }
                //Header에 JWT가 없는 요청들은 바로 doFilter() 실행. Ex) 회원가입 등...
                filterChain.doFilter(request, response);		
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;토큰 인증 등의 과정을 진행하지 않고 바로 `FilterChain.doFilter(request, response)`를 호출한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;&lt;i&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; 참고로 spring security configuration에서 해당 URI를 `permitAll()` 처리해도 이는 `filter`를 거칩니다. 아예 해당 URI를 무시하는 방법도 있으나 이는 보안 측면에서 권장되지 않는 방법입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이제 로그인 요청을 처리하는 컨트롤러 코드를 구현한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717729244662&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Slf4j
@RestController
@RequestMapping(&quot;/api/auth&quot;)
@RequiredArgsConstructor
public class AuthenticationController {

    private final AuthenticationService authService;

    @PostMapping(&quot;/sign-in&quot;)
    public ResponseEntity&amp;lt;AuthenticationRespDto&amp;gt; authenticate(@RequestBody AuthenticationReqDto dto) {

        log.info(&quot;[authenticate] login request: {}&quot;, dto);
        authService.isValidCredentials(dto); //here
				
				...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인 정보와 함께 `/api/auth/sign-in`으로 요청이 들어오면 `AuthService#isValidCredentials` 메서드에서 로그인 정보를 검증한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717729302517&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional
@RequiredArgsConstructor
public class AuthenticationService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    ...
    
    public AuthenticationRespDto authenticate(AuthenticationReqDto dto) {

        //jwt, refresh token 발급
        UserDetails userDetails = userDetailsService.loadUserByUsername(dto.username());
        String jwt = tokenProvider.generateToken(userDetails);
        String refreshToken = tokenProvider.generateRefreshToken(userDetails);

        //이전 토큰 무효화 및 새 토큰 저장
        UserEntity user = userRepository.findByUsername(dto.username())
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));
        revokeUserAllTokens(user);
        saveUserToken(user, jwt);

        return new AuthenticationRespDto(jwt, refreshToken);
    }
			
    public void isValidCredentials(AuthenticationReqDto dto) {
        UserEntity user = userRepository.findByUsername(dto.username())
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));

        if (user == null || !passwordEncoder.matches(dto.password(), user.getPassword())) {
            throw new BadCredentialsException(&quot;Invalid username or password&quot;);
        }
    }
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`username`이 DB에 있는지 확인한다. 없으면 `UsernameNotFoundException`을 던지고, 있으면 해당 정보를 가진 객체를 꺼낸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB에서 꺼낸 객체의 `password`와 사용자가 입력한 `password`를 비교한다. 일치하지 않으면 `BadCredentialsException`을 던진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;검증이 성공적으로 완료되면 다시 컨트롤러로 돌아간다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717732095348&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Slf4j
@RestController
@RequestMapping(&quot;/api/auth&quot;)
@RequiredArgsConstructor
public class AuthenticationController {

    private final AuthenticationService authService;

    @PostMapping(&quot;/sign-in&quot;)
    public ResponseEntity&amp;lt;AuthenticationRespDto&amp;gt; authenticate(@RequestBody AuthenticationReqDto dto) {

        log.info(&quot;[authenticate] login request: {}&quot;, dto);
        authService.isValidCredentials(dto);
        
        AuthenticationRespDto response = authService.authenticate(dto); //here
        log.info(&quot;[authenticate] login response: {}&quot;, response);
				
				...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`AuthService#authenticate`를 호출하여 JWT 토큰을 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717732140756&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional
@RequiredArgsConstructor
public class AuthenticationService {

    private final UserDetailsService userDetailsService;
    private final TokenProvider tokenProvider;
    private final UserRepository userRepository;
    private final TokenRepository tokenRepository;
    ...
    
    public AuthenticationRespDto authenticate(AuthenticationReqDto dto) {

        //jwt, refresh token 발급
        UserDetails userDetails = userDetailsService.loadUserByUsername(dto.username());
        String jwt = tokenProvider.generateToken(userDetails);
        String refreshToken = tokenProvider.generateRefreshToken(userDetails);

        //이전 토큰 무효화 및 새 토큰 저장
        UserEntity user = userRepository.findByUsername(dto.username())
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));
        revokeUserAllTokens(user);
        saveUserToken(user, jwt);

        return new AuthenticationRespDto(jwt, refreshToken);
    }	
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`UserDetailsService#loadUserByUsername`를 이용하여 사용자 정보를 담는 객체인 `UserDetails`를 생성한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 객체를 `TokenProvider#generateToken`으로 넘겨 사용자 정보를 바탕으로 Access Token을 생성한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그다음에 Refresh Token을 생성하는 `TokenProvider#generateRefreshToken`을 호출한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;토큰 생성 과정에서 예외 발생 없이 잘 넘어가면 사용자가 이전에 발급받은 모든 토큰을 무효화(invalide) 처리한다.&lt;/span&gt;
&lt;pre id=&quot;code_1717732474635&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private void revokeUserAllTokens(UserEntity user) {
        List&amp;lt;Token&amp;gt; userTokens = tokenRepository.findAllValidTokenByUser(user.getId());

        if (userTokens.isEmpty()) return;

        // 사용자 모든 토큰 만료시키기
        userTokens.forEach(token -&amp;gt; {
            token.configureRevoked(true);
            token.configureExpired(true);
        });

        tokenRepository.saveAll(userTokens);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;이제 새롭게 발급한 토큰을 저장한다.
&lt;pre id=&quot;code_1717732578550&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private void saveUserToken(UserEntity user, String jwt) {
        Token token = Token.builder()
                .user(user)
                .token(jwt)
                .expired(false)
                .revoked(false)
                .build();
        tokenRepository.save(token);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 과정이 성공적으로 완료되면 생성된 Access Token, Refresh Token을 DTO 객체에 담아 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다시 컨트롤러로 돌아온다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717732726980&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Slf4j
@RestController
@RequestMapping(&quot;/api/auth&quot;)
@RequiredArgsConstructor
public class AuthenticationController {

    private final AuthenticationService authService;

    @PostMapping(&quot;/sign-in&quot;)
    public ResponseEntity&amp;lt;AuthenticationRespDto&amp;gt; authenticate(@RequestBody AuthenticationReqDto dto) {

        log.info(&quot;[authenticate] login request: {}&quot;, dto);
        authService.isValidCredentials(dto);
        
        AuthenticationRespDto response = authService.authenticate(dto); //here
        log.info(&quot;[authenticate] login response: {}&quot;, response);
				
        return ResponseEntity.ok(response); //here
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반환받은 DTO를 `ResponseEntity`에 담아서 사용자에게 반환한다.&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;토큰 기반 로그아웃 구현&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security에 JWT 기반 로그아웃을 위한 내장된 구현은 별도로 없다. JWT의 &lt;b&gt;stateless&lt;/b&gt; 특성으로 인하여 서버는 사용자의 세션을 저장하거나 추적하지 않기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 클라이언트가 토큰 관리와 로그아웃 과정 초기화를 담당하게 된다. 프론트엔트의 경우 사용자는 토큰을 로컬 스토리지에서 제거하거나 토큰이 만료되도록 만료 날짜를 변경 또는 설정하면 된다. 또는 백엔드에서 이러한 토큰에 관한 보안이나 무효화하는 메커니즘을 구현할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기에서는 로그아웃을 구현할 때 Spring Security에게 로그아웃을 위한 &lt;b&gt;엔드포인트&lt;/b&gt;를 제공하고, Spring은 개발자에게 &lt;b&gt;로그아웃 핸들러&lt;/b&gt;를 제공한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선 Security Configuration에서 로그아웃 설정을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717733060196&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Configuration
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfig {

		...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                ...
                .logout(logout -&amp;gt; logout.logoutUrl(&quot;/api/auth/sign-out&quot;)
                        .addLogoutHandler(logoutHandler)
                        .logoutSuccessHandler((request, response, authentication) -&amp;gt; SecurityContextHolder.clearContext())
                );

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그아웃 요청을 받을 URL을 지정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그아웃을 처리할 `LogoutHandler`를 등록한다. 이 핸들러는 `LogoutHandler` 인터페이스를 구현한 구현체이다.&lt;/span&gt;
&lt;pre id=&quot;code_1717733177708&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

@Service
@RequiredArgsConstructor
public class LogoutHandlerImpl implements LogoutHandler {

    private final TokenRepository tokenRepository;

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String token = getJwtFromRequest(request);

        Token storedToken = tokenRepository.findByToken(token)
                .orElseThrow(() -&amp;gt; new ResponseStatusException(HttpStatus.NOT_FOUND, &quot;존재하지 않는 토큰(JWT)입니다.&quot;));

        //토큰 무효화하기
        storedToken.configureExpired(true);
        storedToken.configureRevoked(true);

        tokenRepository.save(storedToken);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);

        if (StringUtils.hasText(bearerToken) &amp;amp;&amp;amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청에서 토큰을 꺼낸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 토큰이 DB에 있는지 확인한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;토큰이 없는 경우 `404 NOT FOUND` 에러가 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB에 토큰이 있으면 토큰을 무효화 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설정 후에 해당 변경을 저장한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그아웃이 성공하면 호출되는 `logoutSuccessHandler`를 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기의 로그아웃 구현은 로그아웃 핸들러 외에 특별히 설정할 것이 없다. 관련하여 자세한 내용은 아래 링크를 참고!&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1717733483905&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Handling Logouts :: Spring Security&quot; data-og-description=&quot;If you are using Java configuration, you can add clean up actions of your own by calling the addLogoutHandler method in the logout DSL, like so: Custom Logout Handler CookieClearingLogoutHandler cookies = new CookieClearingLogoutHandler(&amp;quot;our-custom-cookie&amp;quot;&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html&quot; data-og-url=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Handling Logouts :: Spring Security&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;If you are using Java configuration, you can add clean up actions of your own by calling the addLogoutHandler method in the logout DSL, like so: Custom Logout Handler CookieClearingLogoutHandler cookies = new CookieClearingLogoutHandler(&quot;our-custom-cookie&quot;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;번외. JWT Authentication&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기간 내에 구현하려고 하다 보니 공식 문서를 꼼꼼히는 뒤늦게 보게 됐는데, 이런 부분이 있더라... 나중에 참고하기 위해 남겨두기..&lt;/p&gt;
&lt;figure id=&quot;og_1717733616738&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;OAuth 2.0 Resource Server JWT :: Spring Security&quot; data-og-description=&quot;Most Resource Server support is collected into spring-security-oauth2-resource-server. However, the support for decoding and verifying JWTs is in spring-security-oauth2-jose, meaning that both are necessary in order to have a working resource server that s&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-architecture&quot; data-og-url=&quot;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-architecture&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eL61P/hyWg4EYWPV/QVZRdxuffbcaHNEw5jeKU0/img.png?width=932&amp;amp;height=584&amp;amp;face=0_0_932_584&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-architecture&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eL61P/hyWg4EYWPV/QVZRdxuffbcaHNEw5jeKU0/img.png?width=932&amp;amp;height=584&amp;amp;face=0_0_932_584');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;OAuth 2.0 Resource Server JWT :: Spring Security&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Most Resource Server support is collected into spring-security-oauth2-resource-server. However, the support for decoding and verifying JWTs is in spring-security-oauth2-jose, meaning that both are necessary in order to have a working resource server that s&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEM8De/btsHR5c1OEO/js6wCQafOrlckLz6X8KkJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEM8De/btsHR5c1OEO/js6wCQafOrlckLz6X8KkJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEM8De/btsHR5c1OEO/js6wCQafOrlckLz6X8KkJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEM8De%2FbtsHR5c1OEO%2Fjs6wCQafOrlckLz6X8KkJK%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;600&quot; height=&quot;376&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;color: #666666;&quot;&gt; &lt;i&gt;참고&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://memodayoungee.tistory.com/135&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://memodayoungee.tistory.com/135&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/mq5oUXcAXL4?feature=shared&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/mq5oUXcAXL4?feature=shared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/0GGFZdYe-FY?feature=shared&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/0GGFZdYe-FY?feature=shared&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>authentication</category>
      <category>jwt</category>
      <category>spring security 6</category>
      <category>로그아웃</category>
      <category>로그인</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/167</guid>
      <comments>https://backend-jaamong.tistory.com/167#entry167comment</comments>
      <pubDate>Sat, 8 Jun 2024 10:25:14 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 6 - Architecture</title>
      <link>https://backend-jaamong.tistory.com/166</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security 6 - Architecture 공식 문서를 번역했습니다. 필요에 의해 설명을 추가한 부분도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1717723737896&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Architecture :: Spring Security&quot; data-og-description=&quot;The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-security/reference/servlet/architecture.html&quot; data-og-url=&quot;https://docs.spring.io/spring-security/reference/servlet/architecture.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/be59Jq/hyWgWtmY2I/yBBQkLyjy4kL59yBJexNC0/img.png?width=698&amp;amp;height=610&amp;amp;face=0_0_698_610,https://scrap.kakaocdn.net/dn/ceTYlI/hyWg5qjP6i/7nWzyV5KaQ4vqltC2UgkVk/img.png?width=686&amp;amp;height=508&amp;amp;face=0_0_686_508,https://scrap.kakaocdn.net/dn/5XWzK/hyWg0WPACU/k1I03Y0bEKPfsyzc1oqmH1/img.png?width=656&amp;amp;height=508&amp;amp;face=0_0_656_508&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/architecture.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-security/reference/servlet/architecture.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/be59Jq/hyWgWtmY2I/yBBQkLyjy4kL59yBJexNC0/img.png?width=698&amp;amp;height=610&amp;amp;face=0_0_698_610,https://scrap.kakaocdn.net/dn/ceTYlI/hyWg5qjP6i/7nWzyV5KaQ4vqltC2UgkVk/img.png?width=686&amp;amp;height=508&amp;amp;face=0_0_686_508,https://scrap.kakaocdn.net/dn/5XWzK/hyWg0WPACU/k1I03Y0bEKPfsyzc1oqmH1/img.png?width=656&amp;amp;height=508&amp;amp;face=0_0_656_508');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Architecture :: Spring Security&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;span style=&quot;color: #000000;&quot;&gt;목차&lt;/span&gt;&lt;/b&gt; &lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#title&quot;&gt;Spring Security Architecture&lt;/a&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#no1&quot;&gt;Filter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no2&quot;&gt;DelegatingFilterProxy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no3&quot;&gt;FilterChainProxy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no4&quot;&gt;SecurityFilterChain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no5&quot;&gt;다중 SecurityFilterChain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no6&quot;&gt;Security Filters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no7&quot;&gt;Security Filter 출력하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no8&quot;&gt;Filter Chain에 사용자 정의 필터 추가하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no9&quot;&gt;Security Exception 처리하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no10&quot;&gt;인증&amp;nbsp;간&amp;nbsp;Request&amp;nbsp;저장하기&amp;nbsp; &lt;br /&gt;&lt;/a&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#no10-1&quot;&gt;RequestCache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no10-2&quot;&gt;요청이 저장되지 않도록 방지하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no10-3&quot;&gt;RequestCacheAwareFilter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no11&quot;&gt;Logging&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;title&quot; style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Security Architecture&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security는 인증, 검증, 권한 부여를 편리하게 할 수 있도록 도와주는 Spring Framework의 라이브러리이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no1&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Filter&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security는 Spring MVC의 `Dispatcher Servlet` 보다 앞인 `Servlet Container` 내부에 존재하는 `Filter`에서 시작한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wjYCr/btsHQ9UqTqn/o5h8RM8HWFWc9C6irrKijk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wjYCr/btsHQ9UqTqn/o5h8RM8HWFWc9C6irrKijk/img.png&quot; data-alt=&quot;Servlet Container를 보면 Filter가 Dispatcher Servlet 보다 앞에 위치하고 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wjYCr/btsHQ9UqTqn/o5h8RM8HWFWc9C6irrKijk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwjYCr%2FbtsHQ9UqTqn%2Fo5h8RM8HWFWc9C6irrKijk%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;600&quot; height=&quot;305&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Servlet Container를 보면 Filter가 Dispatcher Servlet 보다 앞에 위치하고 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 `Filter`의 역할을 먼저 찾아보는 것이 이해에 도움 된다. 아래 그림은 단일 HTTP 요청에 대한 핸들러의 일반적인 계층을 보여준다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cs9MyZ/btsHRNQTYWb/II2Kd9KGHvXHIvvPjspff0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cs9MyZ/btsHRNQTYWb/II2Kd9KGHvXHIvvPjspff0/img.png&quot; data-alt=&quot;FilterChain&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cs9MyZ/btsHRNQTYWb/II2Kd9KGHvXHIvvPjspff0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcs9MyZ%2FbtsHRNQTYWb%2FII2Kd9KGHvXHIvvPjspff0%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;300&quot; height=&quot;433&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FilterChain&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트가 애플리케이션으로 요청을 보내면 컨테이너는 요청 URI 경로를 따라 `HttpSevletRequest`를 처리해야 하는 필터 인스턴스와 서블릿을 포함하는 `FtilerChain`을 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하나 이상의 `Filter`를 사용하여 다음을 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다운 스트림 필터 인스턴스(Downstream Filter Instance) 또는 서블릿이 호출되지 않도록 한다. 이 경우 필터는 일반적으로 `HttpServletResponse`를 작성한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다운 스트림 필터 인스턴스 및 서블릿에서 사용하는 `HttpServletRequest` 또는 `HttpServletResponse`를 수정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;&lt;i&gt;Note&lt;/i&gt;&lt;/span&gt;&lt;/b&gt; 자신보다 먼저 호출되는 필터를 `Upstream Filter`, 나중에 호출되는 필터를 `Downstream Filter`라고 한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;여러 개의 필터가 연결되어 이루어진 `FilterChain`은 서블릿 컨테이너 내부에서 클라이언트의 요청이 서블릿에 도달하기 전에 인가 처리, 권한 부여, 요청 로깅, 예외 처리 등과 같은 작업을 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 코드는 `FilterChain` 예제이다. 모든 필터를 통과하고 애플리케이션에서 할 일을 마친 후 바로 클라이언트로 응답이 가는 것이 아닌, 다시 필터를 통해서 돌아가는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717717128326&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    // do something before the rest of the application
    chain.doFilter(request, response); // invoke the rest of the application
    // do something after the rest of the application
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필터는 다운 스트림 필터 인스턴스와 서블릿에만 영향을 미치므로 &lt;b&gt;각 필터가 호출되는 순서는 매우 중요&lt;/b&gt;하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no2&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;DelegatingFilterProxy&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring은 서블릿 컨테이너 생명 주기와 Spring의 `ApplicationContext` 사이를 연결해 주는 `DelegatingFilterProxy`라는 필터 구현체를 제공한다. 일반적인 필터들은 Spring에서 정의된 `Bean`을 인지할 수 없지만, `DelegatingFilterProxy`는 필터이면서 `Bean`에게 작업을 위임할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FUD4O/btsHPvY0XJo/7l9rQj6KZoi8OMlYz8bbh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FUD4O/btsHPvY0XJo/7l9rQj6KZoi8OMlYz8bbh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FUD4O/btsHPvY0XJo/7l9rQj6KZoi8OMlYz8bbh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFUD4O%2FbtsHPvY0XJo%2F7l9rQj6KZoi8OMlYz8bbh0%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;300&quot; height=&quot;476&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;320&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;&lt;span style=&quot;color: #000000;&quot;&gt;아래 코드는 `DelegatingFilterProxy`의 의사 코드(pseudo code)이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717717313555&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    Filter delegate = getFilterBean(someBeanName);
    delegate.doFilter(request, response);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 코드의 첫 줄은 `Bean`으로 등록된 필터를 &lt;b&gt;&lt;i&gt;Lazy하게&lt;/i&gt;&lt;/b&gt; 가져오는 작업을 수행한다. `DelegatingFilterProxy`는 필터 빈 인스턴스 조회를 지연시킬 수 있는 장점을 갖고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필터 인스턴스는 컨테이너가 시작되기 전에 등록되어야 한다. 그러나 Spring은 일반적으로 `ContextLoadListener`를 사용하여 `Spring Bean`을 로드하는데, 이는 필터 인스턴스를 등록한 후에 수행되므로 필터 빈 인스턴스 조회를 지연시킬 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no3&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;FilterChainProxy&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SpringSecurity가 제공하는 `FilterChainProxy`는 `SecurityFilterChain`을 통해 많은 필터 인스턴스에 위임할 수 있게 하는 필터이다. 또한 `FilterChainProxy`는 `Bean`이기 때문에 일반적으로 `DelegatingFilterProxy`로 감싸지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkdDOt/btsHPf29mD1/J9R1kZxJa0bbzaiurAL4KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkdDOt/btsHPf29mD1/J9R1kZxJa0bbzaiurAL4KK/img.png&quot; data-alt=&quot;FilterChainProxy&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkdDOt/btsHPf29mD1/J9R1kZxJa0bbzaiurAL4KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkdDOt%2FbtsHPf29mD1%2FJ9R1kZxJa0bbzaiurAL4KK%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;600&quot; height=&quot;465&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FilterChainProxy&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no4&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SecurityFilterChain&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`FilterChainProxy`는 현재 요청에 대해 호출되어야 하는 Spring Security 필터 인스턴스를 결정하기 위해 `SecurityFilterChain`을 사용한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IdUMg/btsHRlmYiCo/Rx06Qly1K5krFe7Kabpvk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IdUMg/btsHRlmYiCo/Rx06Qly1K5krFe7Kabpvk0/img.png&quot; data-alt=&quot;SecurityFilterChain&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IdUMg/btsHRlmYiCo/Rx06Qly1K5krFe7Kabpvk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIdUMg%2FbtsHRlmYiCo%2FRx06Qly1K5krFe7Kabpvk0%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;600&quot; height=&quot;444&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SecurityFilterChain&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`SecurityFilterChain` 내부의 `Security Filter`들은 일반적으로 `Bean`이지만, `DelegatingFilterProxy`가 아닌 `FilterChainProxy`에 등록된다. `FilterChainProxy`는 서블릿 컨테이너 또는 `DelegatingFilterProxh`에 직접 등록하는 것보다 여러 가지 이점을 제공한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security가 서블릿 기반으로 제공하는 모든 기능의 시작점이므로, 트러블 슈팅 시 `FilterChainProxy`에 디버그 포인트를 추가하기 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`FilterChainProxy`는 Spring Security 사용의 핵심이다. 이는 `SecurityContext`를 초기화하여 메모리 누수를 피하거나, `HttpFirewall`을 적용하여 특정 유형의 공격으로부터 애플리케이션을 보호하도록 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`SecurityFilterChain`이 언제 호출되어야 하는지 보다 유연하게 결정할 수 있도록 한다. 서블릿 컨테이너 안에서 필터 인스턴스는 URL만을 기반으로 호출되지만, `FilterChainProxy`는 `RequestMatcher` 인터페이스를 사용하여 `HttpServletRequest`의 모든 것을 기반으로 호출을 결정할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no5&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;다중 SecurityFilterChain&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`FilterChainProxy`는 멤버 변수로 `List&amp;lt;SecurityFliterChain&amp;gt; filterChains`를 가직 ㅗ있어 여러 개의 `SecurityFilterChain`을 모두 저장할 수 있으며, 현재 요청에 따라 어떤 `SecurityFilterChain`을 사용할지 결정한다&lt;span style=&quot;color: #666666;&quot;&gt;(위 FilterChainProxy의 이점 3번)&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 그림을 보면, `SecurityFilterChain 0`은 총 3개의 필터로, `SecurityFilterChain n`은 4 개의 필터로 구성되어 있는 것을 확인할 수 있다. 이를 통해 각 `SecurityFilterChain`은 유니크하고 독립적으로 구성될 수 있음을 알 수 있다. `SecurityFilter`를 통해 검사할 필요가 없는 요청에 대해서는 0개의 필터로 구성된 `SecurityFilterChain`을 적용할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OMxLW/btsHRIhTzM4/KtyTvGZVA3oKoJuOWIeuc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OMxLW/btsHRIhTzM4/KtyTvGZVA3oKoJuOWIeuc1/img.png&quot; data-alt=&quot;Multiple SecurityFilterChain&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OMxLW/btsHRIhTzM4/KtyTvGZVA3oKoJuOWIeuc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOMxLW%2FbtsHRIhTzM4%2FKtyTvGZVA3oKoJuOWIeuc1%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;600&quot; height=&quot;411&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Multiple SecurityFilterChain&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 코드는 실제 Spring Security의 `FilterChainProxy` 코드 중 일부이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717719381322&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private List&amp;lt;Filter&amp;gt; getFilters(HttpServletRequest request) {
        int count = 0;
        Iterator var3 = this.filterChains.iterator();

        SecurityFilterChain chain;
        do {
            if (!var3.hasNext()) {
                return null;
            }

            chain = (SecurityFilterChain)var3.next();
            if (logger.isTraceEnabled()) {
                ++count;
                logger.trace(LogMessage.format(&quot;Trying to match request against %s (%d/%d)&quot;, chain, count, this.filterChains.size()));
            }
        } while(!chain.matches(request));

        return chain.getFilters();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`while`문의 조건을 보면, &lt;b&gt;요청과 일치하는 SecurityFilterChain의 차례가 되면 반복문이 종료&lt;/b&gt;되는 것을 확인할 수 있다. 이를 통해 &lt;b&gt;가장 먼저 매칭되는 단 하나의 `SecurityFilterChain`만이 호출됨&lt;/b&gt;을 알 수 있다. 위에서 언급한 것처럼 순서가 매우 중요하다는 의미이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 그림을 다시 보면, `SecurityFilterChain 0`는 `/api/**`에 대해, `SecurityFilterChain n`은 `/**`에 매칭되도록 설정되어 있다. 예를 들어, `/api/message/` URL이 요청되면 `SecurityFilterChain 0`이 가장 처음으로 매칭되므로 `SecurityFilterChain 0`만이 호출되어 이를 통한 검증이 이루어지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no6&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Security Filters&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Security Filter`는 `SecurityFilterChain API`를 사용하여 `FilterChainProxy`에 삽입된다 `Security Filter`는 다양한 목적(인증, 인가, 악용 방지 등)으로 사용될 수 있으며, 적시에 호출되도록 &lt;b&gt;특정 순서로 실행&lt;/b&gt;된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어, 인증(Authentication)을 수행하는 필터는 권한 부여(Authorization)를 수행하는 필터보다 호출되어야 한다. 두 필터가 반대의 순서로 호출되면 정상적인 작동을 보장할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717719822817&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults()) //CsrfFilter
            .authorizeHttpRequests(authorize -&amp;gt; authorize //AuthorizationFilter
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults()) //BasicAuthenticationFilter
            .formLogin(Customizer.withDefaults()); //UsernamePasswordAuthenticationFilter
        return http.build();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같이 설정할 경우, 아래와 같은 순서대로 필터가 실행될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CSRF 공격에 대항하여 `CsrfFilter`가 호출된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청을 인증하기 위해 인증 필터(authentication filters)가 호출된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청을 인가하기 위해 `AuthorizationFilter`가 호출된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no7&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Security Filter 출력하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가한 필터가 `Security Filter` 목록에 있는지 확인하고 싶을 때와 같이, 특정 요청에 대해 호출되는 `Security Filter` 목록 확인이 필요할 때가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필터 목록은 `INFO` 레벨 수준에서 출력되므로, 다음과 같은 결과를 콘솔창에서 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717720002558&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2023-06-14T08:55:22.321-03:00  INFO 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;뿐만 아니라 각 요청에 대해 각 필터의 호출을 출력하도록 애플리케이션을 구성할 수도 있다. 이렇게 하면 추가한 필터가 특정 요청에 대해 호출되는지 확인하거나, 예외가 발생하는 위치를 찾는 데 유용하다. 이를 위해 보안 이벤트(security event)를 기록하도록 애플리케이션을 구성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no8&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Filter Chain에 사용자 정의 필터 추가하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 대부분 기본 필터로도 충분하지만, 커스텀/사용자 정의 필터가 필요할 수도 있다. 예를 들어 `tenant id header`를 가져오는 필터를 추가하여 현재 사용자가 해당 `tenant`에 액세스 할 수 있는지 확인하려고 한다고 가정해 보자. 이전 설명에서 이미 필터를 추가할 위치에 대한 단서를 제공했는데, 현재 사용자를 알아야 하므로 인증 필터(authentication filter) 다음에 해당 기능을 수행하는 필터를 추가해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717720126560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;

public class TenantFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String tenantId = request.getHeader(&quot;X-Tenant-Id&quot;); (1)
        boolean hasAccess = isUserAllowed(tenantId); (2)
        if (hasAccess) {
            filterChain.doFilter(request, response); (3)
            return;
        }
        throw new AccessDeniedException(&quot;Access denied&quot;); (4)
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`request header`에서 `tenant id`를 가져온다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 사용자가 가져온 `tenant id`에 대해 접근 권한이 있는지 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;권한이 있으면, `chain`에 남은 필터들을 호출한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;권한이 없으면, `AccessDeniedException`을 던진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;&lt;i&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; `Filter`를 구현하는 대신 `OncePerRequestFilter`를 구현해도 된다. 이는 요청 당 한 번만 호출되는 필터의 베이스 클래스이며, `HttpServletRequest` 및 `HttpServletResponse` 매개 변수와 함께 `doFilterInternal` 메서드를 제공한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;이제 위 필터를 `security filter chain`에 등록해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`HttpSecurity#addFilterBefore`를 사용하여 사용자 정의 필터인 `TenantFilter`를 `AuthorizationFilter` 앞에 등록한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717720326168&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`TenantFilter`가 인증 필터 뒤에 호출되도록 `AuthorizationFilter` 앞에 해당 필터를 등록했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`HttpSecurity#addFilterAfter`를 사용하여 특정 필터 뒤에 호출되도록 하거나 `HttpSecurity#addFilterAt`을 사용하여 `filter chain`의 특정 필터 위치에 필터를 등록할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Filter`를 구현한 사용자 정의 필터를 `@Bean` 또는 `@Component`을 사용하여 `Spring Bean`으로 등록할 때 주의해야 할 점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Boot는 해당 커스텀 필터를 자동으로&lt;/span&gt; &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.1.1/reference/html/web.html#web.servlet.embedded-container.servlets-filters-listeners.beans&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;임베디드 컨테이너에 등록&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한다. 이로 인해 필터가 다른 순서로 두 번 호출되게 할 수도 있다; 컨테이너에 의해 한 번, Spring Security에 의해 한 번.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 해당 커스텀 필터가 의존관계 주입이 필요한 경우에는 `Spring Bean`으로 명시적으로 선언해야 한다. 이는 위에서 언급한 `Security Filter`와 `Spring Bean` 사이의 초기화 순서와 연관되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Security Filter 초기화&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Filter`나 `OncePerRequestFilter`를 구현한 커스텀 필터는 &lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;메인 애플리케이션 컨텍스트가 완전히 로드되고 모든 `bean`이 인스턴스화되기도 전에&lt;/span&gt;&amp;nbsp;애플리케이션 시작 프로세스에서 매우 일찍 초기화되고 등록된다.&amp;nbsp;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Bean 초기화&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커스텀 필터가 필요로 하는 의존관계를 포함한 `Spring Bean`은 `security filter`가 등록된 후 시작 프로세스 후반에 초기화되고 인스턴스화된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;의존관계 주입&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커스텀 필터가 의존관계 주입을 필요로 하면 Spring은 의존관계 주입 없이는 필터를 자동으로 인스턴스화하고 등록할 수 없다. 이는 Spring Security가 해당 필터를 초기화하고 등록할 때 아직 의존 관계는 사용할 수 없기 때문이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Spring Bean으로 등록하기&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커스텀 필터를 `Spring Bean`으로 명시적으로 등록하면, Spring은 커스텀 필터 `bean`의 생명 주기를 관리하고 필터의 의존 관계를 올바르게 해결할 수 있게 된다. 메인 애플리케이션 컨텍스트가 완전히 로드될 때 Spring은 커스텀 필터 `bean`을 인스턴스화하고 필요한 의존 관계를 주입할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의존 관계 주입을 활용하고 중복 호출을 피해야 하는 경우, `FilterRegistrationBean`을 `bean`으로 선언하고 해당 `enabled` 속성을 `false`로 설정하여 컨테이너에 등록하지 않도록 Spring Boot에게 지시할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717721002699&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public FilterRegistrationBean&amp;lt;TenantFilter&amp;gt; tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean&amp;lt;TenantFilter&amp;gt; registration = new FilterRegistrationBean&amp;lt;&amp;gt;(filter);
    registration.setEnabled(false);
    return registration;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;`OncePerRequestFilter`와 중복 호출에 관련된 개인적인 고찰 &lt;br /&gt;`OncePerRequestFilter`를 구현한 커스텀 필터의 경우에는 `FilterRegistrationBean` 빈을 선언하고 `enabled` 속성을 `false`로 설정할 필요는 없는 것 같다. `OncePerRequestFilter`는 한 요청 당 한 번만 호출되도록 Spring Security에서 내부적으로 처리하기 때문이다.&lt;br /&gt;실제로 해당 커스텀 필터가 호출될 때 로그를 찍어보면 한 번만 출력되는 것을 확인할 수 있다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no4=9&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Security Exception 처리하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`ExceptionTranslationFilter`는 `AccessDeniedException`과 `AuthenticationException`을 HTTP Response로 변환할 수 있다. 해당 예외 필터는 `security filter` 중 하나로 `FilterChainProxy`에 삽입된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnfbYu/btsHQpp4IAM/ijMQ2k2qZF2mCmu3ug0bY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnfbYu/btsHQpp4IAM/ijMQ2k2qZF2mCmu3ug0bY1/img.png&quot; data-alt=&quot;Relationship of `ExceptionTranslationFilter` to other components&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnfbYu/btsHQpp4IAM/ijMQ2k2qZF2mCmu3ug0bY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnfbYu%2FbtsHQpp4IAM%2FijMQ2k2qZF2mCmu3ug0bY1%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;600&quot; height=&quot;524&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Relationship of `ExceptionTranslationFilter` to other components&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`ExceptionTranslationFilter`가 `FilterChain.doFilter(request, response)`를 호출한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인증되지 않은 사용자 거나 `AuthenticationException`이 발생한 경우, 인증 프로세스를 시작한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-securitycontextholder&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SecurityContextHolder&lt;/a&gt;&lt;/i&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 비운다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인증이 성공하면 원래의 요청을 다시 실행할 수 있도록 `HttpServletRequest`를 저장해둔다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`AuthenticationEntryPoint`는 클라이언트로부터 자격 증명(request credentails)을 요청하는 데 사용된다. 예를 들어 로그인 페이지로 리디렉션 되거나 `WWW-Authenticate` 헤더를 보낼 수도 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2번이 아닌 `AccessDeniedException`인 경우, 접근이 거부된다&lt;i&gt;(Access Denied)&lt;/i&gt;. 거부된 접근을 처리하기 위해 `AccessDeniedHandler`가 호출된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;Note&lt;/span&gt;&amp;nbsp;&lt;/b&gt;&lt;/i&gt;애플리케이션이 `AccessDeniedException` 또는 `AuthenticationException`을 던지지 않으면, `ExceptionTranslationFilter`는 아무것도 하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래는 `ExceptionTranslationFilter` 의사코드이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717722913316&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    filterChain.doFilter(request, response); //(1)
} catch (AccessDeniedException | AuthenticationException ex) {
    if (!authenticated || ex instanceof AuthenticationException) {
        startAuthentication(); //(2)
    } else {
        accessDenied(); //(3)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;Filter&lt;/i&gt;에서 설명한 것처럼 `FilterChain.doFilter(request, response)`는 나머지 애플리케이션을 호출하는 것과 동일하다. 이는 애플리케이션의 다른 부분(FilterSecurityIntercepter 또는 method security)이 `AuthenticationException` 또는 `AccessDeniedException`을 던지는 경우, 여기서 `catch`되어 처리될 수 있음을 의미한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자가 인증되지 않았거나 `AuthenticationException`이 발생한 경우, 인증 프로세스를 시작한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2번이 아니라면, 접근이 거부된다&lt;i&gt;(Access Denied)&lt;/i&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no10&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인증 간 Request 저장하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 요청이 인증되지 않았는데 인증이 필요한 자원에 관한 것일 때 인증 성공 후 다리 요청하기 위해 인증된 자원에 대한 요청을 저장해두어야 한다. Spring Security에서는 `RequestCahe` 구현체를 사용한 `HttpServletRequest`를 저장하여 이 작업을 수행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;no10-1&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; RequestCache&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`HttpServletRequest`는`RequestCache`에 저장된다. 사용자가 인증을 성공할 때 `RequestCache`는 원래의 요청을 다시 실행하는 데 사용된다. `RequestCacheAwareFilter`는 사용자가 인증한 후 `RequestCache`를 사용하여 저장된 `HttpServletRequest`를 가져오고, `ExceptionTranslationFilter`는 사용자를 로그인 엔드포인트로 리디렉션 하기 전에&amp;nbsp; `AuthenticationException`을 감지한 후 `RequestCache`를 사용하여 `HttpServletRequest`를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적으로 하나의 `HttpSessionRequestCache`가 사용된다. 아래의 코드는 `continue`라는 매개변수가 있는 경우, 저장된 요청에 대해 `HttpSession`을 확인하는 데 사용되는 `RequestCache` 구현체를 사용자 정의(customize)하는 방법을 보여 준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717723323493&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
	HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
	requestCache.setMatchingRequestParameterName(&quot;continue&quot;);
	http
		// ...
		.requestCache((cache) -&amp;gt; cache
			.requestCache(requestCache)
		);
	return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;no10-2&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;요청이 저장되지 않도록 방지하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;세션에 인증되지 않은 요청을 저장하지 않고, 대신 브라우저나 DB로 옮겨 저장하고 싶을 수도 있다. 또는 로그인하지 않은 사용자가 방문하려는 페이지 대신 홈페이지로 리디렉션 하는 것을 원하면 이 기능을 사용하지 않을 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 경우에는&lt;/span&gt; &lt;a href=&quot;https://docs.spring.io/spring-security/site/docs/6.3.0/api/org/springframework/security/web/savedrequest/NullRequestCache.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;`NullRequestCache` 구현체&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 사용하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717723432322&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    RequestCache nullRequestCache = new NullRequestCache();
    http
        // ...
        .requestCache((cache) -&amp;gt; cache
            .requestCache(nullRequestCache)
        );
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;no10-3&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;RequestCacheAwareFilter&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`RequestCacheAwareFilter`는 `RequestCache`를 사용하여 원래의 요청을 다시 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;no11&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Logging&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Spring Security는 모든 보안 관련 이벤트에 대해 `DEBUG` 및 `TRACE` 레벨에서 포괄적인 로깅을 제공한다. Spring Security는 요청이 거부된 자세한 이유를 response body에 담지 않기 때문에 이는 애플리케이션을 디버깅할 때 유용하다.&amp;nbsp; 401/403 에러가 발생하면 무슨 일이 일어나고 있는지 이해에 도움이 되는 로그를 발견할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717723558663&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;logging:
  level:
    org:
      springframework:
        security: TRACE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>architecture</category>
      <category>filter</category>
      <category>spring security 6</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/166</guid>
      <comments>https://backend-jaamong.tistory.com/166#entry166comment</comments>
      <pubDate>Fri, 7 Jun 2024 20:00:58 +0900</pubDate>
    </item>
    <item>
      <title>[TIL / SQL] relation, type, constraint, FK (MySQL)</title>
      <link>https://backend-jaamong.tistory.com/165</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 글은 아래 강의를 바탕으로 작성했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1715414417827&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[지금 무료] 시니어 백엔드 개발자가 알려주는 데이터베이스 개론 &amp;amp; SQL | 쉬운코드 - 인프런&quot; data-og-description=&quot;쉬운코드 | 백엔드 개발자라면 꼭 알아야 할 데이터베이스와 SQL! 이해하기 쉽게 설명하는 것을 최우선으로 합니다., 데이터베이스 + SQL, 누구나 쉽게!   왕초보도 이해하기 쉬운&amp;nbsp;DB + SQL 기본기!&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B0%9C%EB%A1%A0/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B0%9C%EB%A1%A0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b9ORSp/hyV2qBvm9Z/xbKDuUhacTA1Nqxb5lKNm0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/NFjts/hyV2xObwNm/05ifjgt67Fh0y5kcPYUxNK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bDUk2A/hyV2r8fqLU/TxW1PFjWDA8picdPe2lDl0/img.png?width=2400&amp;amp;height=1256&amp;amp;face=0_0_2400_1256&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B0%9C%EB%A1%A0/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B0%9C%EB%A1%A0/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b9ORSp/hyV2qBvm9Z/xbKDuUhacTA1Nqxb5lKNm0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/NFjts/hyV2xObwNm/05ifjgt67Fh0y5kcPYUxNK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bDUk2A/hyV2r8fqLU/TxW1PFjWDA8picdPe2lDl0/img.png?width=2400&amp;amp;height=1256&amp;amp;face=0_0_2400_1256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[지금 무료] 시니어 백엔드 개발자가 알려주는 데이터베이스 개론 &amp;amp; SQL | 쉬운코드 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;쉬운코드 | 백엔드 개발자라면 꼭 알아야 할 데이터베이스와 SQL! 이해하기 쉽게 설명하는 것을 최우선으로 합니다., 데이터베이스 + SQL, 누구나 쉽게!   왕초보도 이해하기 쉬운&amp;nbsp;DB + SQL 기본기!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;relation in SQL&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Relational Data Model의 relation은 SQL에서 table을 의미한다(완전히 구분 짓지는 않음). 그리고 SQL에서 relation이란 `multiset of tuples`로 &lt;b&gt;중복된 tuple을 허용&lt;/b&gt;한다는 의미이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`multiset`은 중복을 하용하지 않는 `set`과 달리 중복을 허용하므로 각 tuple이 중복될 수 있다.&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Type in SQL(MySQL)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;char vs varchar&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MySQL에서는 휴대전화 번호와 같이 길이가 일정한 경우에는 `char`, 그렇지 않은 경우에는 `varchar`를 쓰는 것이 좋다. `char`가 `varchar`보다 속도가 빠르기 때문에 무조건 `varchar`를 쓰는 것보다 위와 같은 경우를 따져보고 선택하자.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;byte-string&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 타입은 문자열이 아닌 byte string을 저장하는데 쓰인다. MySQL에는 `BINARY`, `VARBINARY`, `BLOB` 타입이 해당된다. 이는 암호화할 때의 키를 저장할 때 쓰인다.&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Constraint&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;UNIQUE&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`unique`는 중복을 허용하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;`NULL` 중복 허용&lt;/b&gt;은 DB마다 다르다. MySQL은 `NULL` 중복을 허용한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보통 `NULL`을 허용하지 않는 `NOT NULL`과 같이 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CHECK&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;attribute의 값을 제한하고 싶을 때 사용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Ex.&lt;/b&gt; CHECK (age &amp;gt; 20)&amp;nbsp; &amp;rarr;&amp;nbsp; 20살 이하는 저장하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;둘 이상의 attribute로 구성될 때는 attribute의 바로 옆이 아닌, 맨 아래에 기입한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;constraint 이름 명시하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이름을 붙이면 어떤 constraint를 위반했는지 쉽게 파악할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;constraint를 삭제하고 싶을 때 해당 이름으로 삭제할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Example SQL&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715413814887&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create table TEST (
	age INT CONSTRAINT age_over_20 CHECK(age&amp;gt;20)
);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CHECK(age&amp;gt;20): 나이가 20살이 넘도록 constraint를 걸어 놓음&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이에 대해 이름 붙이기: `CONSTRAINT {constraint 이름}`&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이름을 붙인 상태로 constraint를 위반하면 다음과 같이 에러가 난다&amp;nbsp;&amp;nbsp; &lt;span style=&quot;text-align: left;&quot;&gt;&amp;rarr;&amp;nbsp; `Check constraint 'age_over_20' is violated.`&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;FOREIGN KEY 선언 방법&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;MySQL&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715413405330&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create table EMPLOYEE ( 
	... 
	dept_id INT, FOREIGN KEY (dept_id) references DEPARTMENT(id) 
            on delete reference_option 
            on update reference_option 
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Notice!&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;b&gt; &amp;nbsp;&lt;/b&gt;참고하고 있는 값이 삭제되거나 변경될 때 FK의 값은 어떻게 할 것인지 지정해야 함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; `on delete reference_option`&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; `on update reference_option`&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;옵션&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle; color: #24292f; text-align: left;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CASCADE: 참조값의 삭제/변경을 그대로 반영&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SET NULL: 참조값이 삭제/변경 시 `NULL`로 변경&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RESTRICT: 참조값이 삭제/변경되는 것을 금지&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;NO ACTION: `RESTRICT`와 유사&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MySQL은 `NO ACTION`과 `RESTRICT` 완전히 동일&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;표준 SQL: 한 트랜잭션 내의 여러 개의 SQL이 실행되는 동안에는 참조값이 삭제/변경되는 것을 허용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SET DEFAULT: 참조값이 삭제/변경 시 `default` 값으로 변경&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: unset;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MySQL은 제대로 지원하지 않음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;&amp;rarr; MySQL에서 지원하는 &lt;b&gt;reference_option&lt;/b&gt;은 `CASCADE`, `SET NULL`, `RESTRICT`&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;&amp;rarr; PostgreSQL은 5가지 모두 지원&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ALTER&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테이블이 생성된(create) 후에 테이블의 스키마를 변경할 때는 `ALTER TABLE` 문을 사용한다.&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;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;FK 옵션을 추가하는 경우의 예시&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715414132450&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE {테이블 명} ADD FOREIGN KEY (FK를 설정할 컬럼) REFERENCES {참조할 테이블}(참조할 테이블의 참조할 컬럼명)
    on update CASCADE
    on delete SET NULL;
    
# Example
ALTER TABLE department ADD FOREIGN KEY (leader_id) REFERENCES employee(id)
    on update CASCADE
    on delete SET NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`department` 테이블의 `leader_id`에 `FK`를 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`leader_id`는 `employee` 테이블의 `id` 컬럼을 참조한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 변경/삭제 시의 설정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database</category>
      <category>constraint</category>
      <category>FK</category>
      <category>rdbms</category>
      <category>relation</category>
      <category>sql</category>
      <category>type</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/165</guid>
      <comments>https://backend-jaamong.tistory.com/165#entry165comment</comments>
      <pubDate>Sat, 11 May 2024 17:05:47 +0900</pubDate>
    </item>
    <item>
      <title>윈도우에 프로메테우스 설치하기</title>
      <link>https://backend-jaamong.tistory.com/164</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 아래 링크에서 각자 OS 맞는 프로메테우스를 다운로드&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;rarr;&lt;/span&gt; &lt;a href=&quot;https://prometheus.io/download/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://prometheus.io/download/&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;2. &lt;span style=&quot;color: #000000;&quot;&gt;다운로드 후 압축 풀기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 압축 푼 폴더에 들어가서 실행 프로그램인 `prometheus.exe` 클릭&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 첫 실행인 경우, 클릭 시 아래와 같은 화면이 나타날 수 있다. 여기에서 `추가 정보`를 클릭.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;611&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kN44M/btsG5Gtg77G/jk8ouJ23WoA45gsMKLqkI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kN44M/btsG5Gtg77G/jk8ouJ23WoA45gsMKLqkI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kN44M/btsG5Gtg77G/jk8ouJ23WoA45gsMKLqkI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkN44M%2FbtsG5Gtg77G%2Fjk8ouJ23WoA45gsMKLqkI1%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;372&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;611&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. `실행` 클릭&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dC2yf5/btsG75FjdTn/bLv6XJcPcKCtjKoMh6nKbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dC2yf5/btsG75FjdTn/bLv6XJcPcKCtjKoMh6nKbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dC2yf5/btsG75FjdTn/bLv6XJcPcKCtjKoMh6nKbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdC2yf5%2FbtsG75FjdTn%2FbLv6XJcPcKCtjKoMh6nKbk%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;377&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 실행하면 아래와 같은 터미널 창이 뜬다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1811&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYgMVG/btsG6JitHZ0/QHIhkBwlwEw25pA9U5wo9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYgMVG/btsG6JitHZ0/QHIhkBwlwEw25pA9U5wo9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYgMVG/btsG6JitHZ0/QHIhkBwlwEw25pA9U5wo9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYgMVG%2FbtsG6JitHZ0%2FQHIhkBwlwEw25pA9U5wo9K%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;560&quot; height=&quot;169&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1811&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 프로메테우스의 기본 포트 `9090`, http://localhost:9090으로 접속&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k2CcW/btsG76YxjaB/UcEX3T36rrkJR7cu6MCitK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k2CcW/btsG76YxjaB/UcEX3T36rrkJR7cu6MCitK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k2CcW/btsG76YxjaB/UcEX3T36rrkJR7cu6MCitK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk2CcW%2FbtsG76YxjaB%2FUcEX3T36rrkJR7cu6MCitK%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;560&quot; height=&quot;156&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 끝!&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;</description>
      <category>Architecture &amp;amp; Tool</category>
      <category>Prometheus</category>
      <category>윈도우10</category>
      <category>프로메테우스</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/164</guid>
      <comments>https://backend-jaamong.tistory.com/164#entry164comment</comments>
      <pubDate>Fri, 3 May 2024 09:42:42 +0900</pubDate>
    </item>
    <item>
      <title>[TIL / Spring] 설정 파일과 프로필</title>
      <link>https://backend-jaamong.tistory.com/163</link>
      <description>&lt;figure id=&quot;og_1713939478393&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;스프링 부트 - 핵심 원리와 활용 | 김영한 - 인프런&quot; data-og-description=&quot;김영한 | 실무에 필요한 스프링 부트는 이 강의 하나로 모두 정리해드립니다., 백엔드 개발자를 위한 스프링 부트 끝판왕!&amp;nbsp;실무에 필요한 내용을 모두 담았습니다.&amp;nbsp; [임베딩 영상] 김영한의 스&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B5%EC%8B%AC%EC%9B%90%EB%A6%AC-%ED%99%9C%EC%9A%A9/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B5%EC%8B%AC%EC%9B%90%EB%A6%AC-%ED%99%9C%EC%9A%A9&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/7g3up/hyVVKMC46f/EoklYrbdRCKmiCmpTeaVx1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bKH6lQ/hyVSVvu948/Rm08SKbJKgmKwMasmqI6B1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/cnXJ7J/hyVS2g4Ha8/8nLDvo1Q9uXXXlrktVOWCK/img.png?width=1200&amp;amp;height=740&amp;amp;face=984_526_1051_599&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B5%EC%8B%AC%EC%9B%90%EB%A6%AC-%ED%99%9C%EC%9A%A9/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B5%EC%8B%AC%EC%9B%90%EB%A6%AC-%ED%99%9C%EC%9A%A9/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/7g3up/hyVVKMC46f/EoklYrbdRCKmiCmpTeaVx1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bKH6lQ/hyVSVvu948/Rm08SKbJKgmKwMasmqI6B1/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/cnXJ7J/hyVS2g4Ha8/8nLDvo1Q9uXXXlrktVOWCK/img.png?width=1200&amp;amp;height=740&amp;amp;face=984_526_1051_599');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;스프링 부트 - 핵심 원리와 활용 | 김영한 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;김영한 | 실무에 필요한 스프링 부트는 이 강의 하나로 모두 정리해드립니다., 백엔드 개발자를 위한 스프링 부트 끝판왕!&amp;nbsp;실무에 필요한 내용을 모두 담았습니다.&amp;nbsp; [임베딩 영상] 김영한의 스&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;외부설정과 프로필1 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오랜만에 Spring을 공부하고 있는데 설정 분리 등을 다 잊어버린 것 같아서 복기 겸 간단하게 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB 설정값을 코드 내부가 아닌 개발 서버와 운영 서버에 `application.properties`와 같은 파일을 두어 설정을 주입할 수 있다. 하지만 각 서버에 저장된 설정 파일을 관리하거나 변경 이력을 확인하기 어렵다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 문제점은 설정 파일을 프로젝트 내부에 포함해서 관리하여 해결할 수 있다. 설정 파일도 &lt;span style=&quot;text-align: start;&quot;&gt;코드와 함께 빌드되게 하는 것이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트 내부에 코드와 함께 각 환경에 필요한 설정 데이터를 포함 및 관리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발, 운영 설정 파일을 모두 포함하여 빌드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`app.jar`는 개발, 운영 두 설정 파일을 모두 가지고 배포됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 시 어떤 설정 데이터를 읽어야 하는지 구분&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스프링은 `&lt;b&gt;프로필`&lt;/b&gt;이라는 개념을 지원하여 4번 과정을 해결한다.&lt;/span&gt;&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;application-xxx.properties&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트 src &amp;gt; main &amp;gt; resources 디렉토리에 `application-xxx.properties` 파일을 생성한다. `xxx`에는 서버 환경을 구분하는 `dev`나 `prod` 등의 단어들이 보통 들어간다.&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-filename=&quot;blob&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpY9gi/btsGSkxfCf8/fnICCO1XAPQudrocE6bjo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpY9gi/btsGSkxfCf8/fnICCO1XAPQudrocE6bjo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpY9gi/btsGSkxfCf8/fnICCO1XAPQudrocE6bjo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpY9gi%2FbtsGSkxfCf8%2FfnICCO1XAPQudrocE6bjo0%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;340&quot; height=&quot;291&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;351&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 파일에 필요한 설정들을 입력한다. 아래 파일은 `application-dev.properties`이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713939772951&quot; class=&quot;properties&quot; data-ke-language=&quot;properties&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;url=dev.db.com
username=dev_user
password=dev_pw&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;프로필 설정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;dev 외에도 prod 설정 파일이 있을 수 있으므로, 이를 구분하기 위해 프로필을 설정해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;아래 이미지에서 `Edit Configurations...`를 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9wzdF/btsGT7XJ7qf/46U8a3XwZ8JEyaAoV5mmy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9wzdF/btsGT7XJ7qf/46U8a3XwZ8JEyaAoV5mmy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9wzdF/btsGT7XJ7qf/46U8a3XwZ8JEyaAoV5mmy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9wzdF%2FbtsGT7XJ7qf%2F46U8a3XwZ8JEyaAoV5mmy0%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;440&quot; height=&quot;320&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;412&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클릭 후 `Program arguments(CLI arguments)`에 다음과 같이 입력한다.&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-filename=&quot;blob&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFNN3A/btsGTFtFlFw/rQB0ARKmDyKGmzwf5MGYfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFNN3A/btsGTFtFlFw/rQB0ARKmDyKGmzwf5MGYfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFNN3A/btsGTFtFlFw/rQB0ARKmDyKGmzwf5MGYfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFNN3A%2FbtsGTFtFlFw%2FrQB0ARKmDyKGmzwf5MGYfk%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;500&quot; height=&quot;172&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1713940105363&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;--spring.profiles.active=dev

# 또는

-Dspring.profiles.active=dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 옵션에 `dev`를 입력하면 `application-dev.properties` 파일에 적힌 설정값이 활성화된다. 마찬가지로 `prod`를 입력하면 `application-prod.properties` 파일에 적힌 값이 활성화된다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;한 파일에서 설정값 작성하기(application.properties)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`application.properties`에 모두 작성하는 방법도 있다. 아래와 같이 작성하면 dev와 prod 프로필의 설정값을 한 파일에 담을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713940298349&quot; class=&quot;properties&quot; data-ke-language=&quot;properties&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.config.activate.on-profile=dev
url=dev.db.com
username=dev_user
password=dev_pw
#---
spring.config.activate.on-profile=prod
url=prod.db.com
username=prod_user
password=prod_pw&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 프로필을 구분하기 위해 각 프로필 설정값 맨 위에 `spring.config.activate.on-profile=xxx`을 작성해야 하며, 프로필 설정값이 바뀌는 구간에서는 `#---`를 작성하여 구분해야 한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같이 작성하고 Configuration에 동일하게 프로필을 지정해 주면 해당 설정값이 활성화된다.&amp;nbsp;&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;default profile&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Configuration에 아무런 프로필도 지정해주지 않으면 default로 설정된다. default는 `application.properties`에서 아래와 같이 지정하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1713940481934&quot; class=&quot;properties&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;properties&quot;&gt;&lt;code&gt;url=local.db.com
username=local_user
password=local_pw
#---
spring.config.activate.on-profile=dev
url=dev.db.com
username=dev_user
password=dev_pw
#---
spring.config.activate.on-profile=prod
url=prod.db.com
username=prod_user
password=prod_pw​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;맨 위에 아무런 프로필도 지정되지 않은 것이 default이다. 따라서 프로필을 지정하지 않으면 맨 위에 설정이 기본값으로 설정된다.&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;</description>
      <category>Java &amp;amp; Kotlin/Spring</category>
      <category>application.properties</category>
      <category>spring</category>
      <category>Spring.profiles.active</category>
      <category>외부 설정</category>
      <category>프로필</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/163</guid>
      <comments>https://backend-jaamong.tistory.com/163#entry163comment</comments>
      <pubDate>Fri, 26 Apr 2024 09:40:33 +0900</pubDate>
    </item>
    <item>
      <title>롤링 무중단 배포</title>
      <link>https://backend-jaamong.tistory.com/161</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;적용 전 준비할 코드&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;nginx.conf&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710294763876&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;worker_processes auto;

events {
    worker_connections 1024;
}

http {
    ...

    # for load balancing
    upstream project_server {
        server project_server2:8000;
        server project_server1:8000;
        keepalive 1024;
    }

    server {
        listen 80;  
        
        location / {
            proxy_pass http://project_server/;    # upstream 이름
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            ...
        }

        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;무중단 배포의 전제조건은 이중화(로드밸런서)로, `upstream` 지시어로 설정할 수 있다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;docker-compose.yml&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710295344561&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.5&quot;

services:
  _build_image:
    image: project_django:4.2.6
    build: .
    
  project_server1: 
    image: project_django:4.2.6
    restart: always
    command: gunicorn project_server.backend.wsgi --bind 0.0.0.0:8000 --log-level debug --timeout=120
    ports:
      - &quot;8000&quot;  
    depends_on:
      - _build_image

  project_server2: 
    image: project_django:4.2.6
    restart: always
    command: gunicorn project_server.backend.wsgi --bind 0.0.0.0:8000 --log-level debug --timeout=120
    ports:
      - &quot;8000&quot; 
    depends_on:
      - _build_image

  nginx:
    image: project-nginx:latest    # tag the image with the name &quot;nginx&quot;
    container_name: project-nginx
    build: ./nginx  # Dockerfile location for Nginx
    restart: alway
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # read-only
    depends_on:
      - project_server1
      - project_server2
    ports:
      - &quot;8000:80&quot;
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;수동으로 하는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✅&amp;nbsp;&lt;b&gt;전체 과정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포 전 상태로  project_server1, project_server2 모두 로드밸런서(nginx)에 연결되어 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server2를 로드밸런서에서 제거하고, project_server2에 배포를 진행한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server2에 배포가 완료되면, 다시 project_server2를 로드밸런서에 연결한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server1을 로드밸런서에서 제거하고, project_server1에 배포를 진행한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server1에 배포가 완료되면, 다시 project_server1을 로드밸런서에 연결한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step1.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포가 진행되는 동안 서비스가 멈추지 않는지(무중단) 확인하기 위해서 터미널 하나를 열여서 주기적으로 Nginx에 요청을 보낸다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710295756446&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1초 간격으로 요청 (무한루프)
$ while true; do curl localhost:4100; ehco &quot;&quot;; sleep1; done&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step2.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server2를 로드밸런서에서 제거한다. &lt;b&gt;nginx.conf&lt;/b&gt;의 `upstream` 서버 그룹에 아래와 같이 `down`을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710295825015&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

upstream project_server {
    server project_server2:8000 down; 
    server project_Server1:8000;
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Nginx 컨테이너에 아래와 같은 명령어를 전달하여 멈추지 않고 설정을 리로드 한다. 리로드 시 `exec`의 다음 인자는 &lt;b&gt;docker-compose.yml&lt;/b&gt;의 `container_name`을 적어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710296032662&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker compose exec project-nginx service nginx reload&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step1&lt;/b&gt;에서 열어둔 터미널에서 project_server1에서만 응답이 오는 것을 확인하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server2의 배포를 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710296084223&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker compose up --build -d project_server2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step3.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server2의 배포가 완료되면 project_server2를 로드밸런서에 연결하고 Nginx를 리로드 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710296127691&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

upstream project_server {
    server project_server2:8000; 
    server project_Server1:8000;
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1710296149714&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker compose exec project-nginx service nginx reload&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리로드 이후, &lt;b&gt;Step1&lt;/b&gt;에서 열어둔 터미널에서 project_server1, project_server2로부터 응답이 오는 것을 확인한다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step4.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 project_server1의 배포를 진행한다. &lt;b&gt;nginx.conf&lt;/b&gt;의 `upstream` 부분을 다음과 같이 수정하고 Nginx를 리로드 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710296127691&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

upstream project_server {
    server project_server2:8000; 
    server project_Server1:8000 down;
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마찬가지로 &lt;b&gt;Step1&lt;/b&gt;에서 열어둔 터미널에서 project_server2에서만 응답이 오는 것을 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server1이 로드밸런서에서 제거됐으므로 project_server1의 배포를 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710296385647&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker compose up --build -d project_server1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Step5.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;project_server1의 배포가 완료되었으므로 다시 로드밸런서에 연결한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710296430644&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

upstream project_server {
    server project_server2:8000; 
    server project_Server1:8000;
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로 Nginx를 리로드 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710296469041&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker compose exec project-nginx service nginx reload&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 열어둔 터미널에서 두 서버로부터 응답이 오는지 확인한다. 응답이 잘 온다면 모든 과정이 끝났다!&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Deploy Shell Script&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위에서 수동으로 진행한 롤링 무중단 배포 과정을 bash shell script로 작성했다. 수동으로 할 필요 없이 배포해야 할 때 아래와 같이 터미널에 입력하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710296586384&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ bash {쉘 스크립트 위치}/{쉘 스크립트 파일 이름}.sh
$ bash ./deploy.sh&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1710296755166&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

# cut connection to a server from the load-balancer(nginx)
# args: server name, server port, docker container name for nginx
cut_reload_nginx() {
	src=&quot;$1:$2&quot;

	dest=$src
	dest+=&quot; down;&quot;

	src+=&quot;;&quot;

	sed -i &quot;s/$src/$dest/g&quot; ./nginx/nginx.conf
	docker compose exec $3 nginx -s reload 
	sleep 10s
}

# connect a server to the load-balancer(nginx)
# args: server name, server port, docker container name for nginx
connect_reload_nginx() {
	dest=&quot;$1:$2&quot;

	src=$dest
	src+=&quot; down;&quot;

	dest+=&quot;;&quot;

	sed -i &quot;s/$src/$dest/g&quot; ./nginx/nginx.conf
	docker compose exec $3 nginx -s reload 
	sleep 10s
}

# deploy a server
# args: container name, env file path
deploy_server() {
	# 1. stop container 
	# 2. remove container
	# 3. up and build container

	echo &quot;&amp;gt;&amp;gt; [deploy_server] Stop &amp;amp; Remove $1...&quot;
	
	docker compose stop $1
	sleep 5s
	
	docker compose rm $1
	sleep 10s

	echo &quot;&amp;gt;&amp;gt; [deploy_server] Up $1...&quot;

	sudo docker compose --env-file $2 up $1 --build -d 
}

# health check for a container(server)
# args: url for health check(pass a url which is located at after localhost/)
health_check() {
	UP_CHECK=&quot;&quot;
	URL=&quot;http://localhost/$1&quot;

	echo &quot;&amp;gt;&amp;gt; [health_check] URL=$URL&quot;
	echo &quot;&amp;gt;&amp;gt; [health_check] start...&quot;

	while [ -z &quot;$UP_CHECK&quot; ]
	do 
		UP_CHECK=$(curl -s $URL) 
	done 

	echo &quot;&amp;gt;&amp;gt; [health_check] finish...&quot;
}

echo &quot;&quot;
echo &quot;&quot;
echo &quot;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++&quot;
echo &quot;&quot;
echo &quot;Rolling Deployment Start&quot;
echo &quot;Author: Jaamong&quot;
echo &quot;Last Updated Date: 2024-0X-XX&quot;
echo &quot;&quot;
echo &quot;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++&quot;
echo &quot;&quot;
echo &quot;&quot;


# ----- for project_server2 container -----
echo &quot;&amp;gt; Nginx: Cut connection to project_server2...&quot;
cut_reload_nginx &quot;project_server2&quot; &quot;8000&quot; &quot;project-nginx&quot;
echo &quot;&quot;

echo &quot;&amp;gt; Deploy only project_server2...&quot;
deploy_server &quot;project_server2&quot; &quot;./project_server/config/.env&quot;
echo &quot;&quot;

echo &quot;&amp;gt; Health Check for project_server2...&quot;
health_check &quot;api/v1/health-check&quot;
echo &quot;&quot;

echo &quot;&amp;gt; Nginx: Connect to project_server2...&quot;
connect_reload_nginx &quot;project_server2&quot; &quot;8000&quot; &quot;project-nginx&quot;
echo &quot;&quot;


sleep 10s


# ----- for project_server1 container -----
echo &quot;&amp;gt; Nginx: Cut connection to project_server1...&quot;
cut_reload_nginx &quot;project_server1&quot; &quot;8000&quot;
echo &quot;&quot;

echo &quot;&amp;gt; Deploy only project_server1...&quot;
deploy_server &quot;project_server1&quot; &quot;./project_server/config/.env&quot;
echo &quot;&quot;

echo &quot;&amp;gt; Health Check for project_server1...&quot;
health_check &quot;api/v1/health-check&quot;
echo &quot;&quot;

echo &quot;&amp;gt; Nginx: Connect to project_server1...&quot;
connect_reload_nginx &quot;project_server1&quot; &quot;8000&quot;
echo &quot;&quot;


sleep 10s


echo &quot;&quot;
echo &quot;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++&quot;
echo &quot;&quot;
echo &quot;Rolling Deploy Finish&quot;
echo &quot;&quot;
echo &quot;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++&quot;
echo &quot;&quot;
echo &quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;주의&lt;/b&gt;&lt;/span&gt; 사용 시 인자(argument)의 순서를 지켜서 입력해야 한다. 본인의 nginx.conf, docker-compose.yml과 맞지 않는 내용이 있을 수도 있으므로 반드시 스크립트 코드를 확인할 것.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cut_reload_nginx args
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;server name: nginx와 연결을 끊을 서버 이름(컨테이너 명)&lt;/li&gt;
&lt;li&gt;server port: nginx와 연결을 끊을 서버 port&lt;/li&gt;
&lt;li&gt;nginx container name: docker-compose.yml에서 정의된 Nginx용 도커 컨테이너 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;connect_reload_nginx args
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;server name: nginx와 연결할 서버 이름(컨테이너 명)&lt;/li&gt;
&lt;li&gt;server port: nginx와 연결할 서버 port&lt;/li&gt;
&lt;li&gt;nginx container name: docker-compose.yml에서 정의된 Nginx용 도커 컨테이너 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;deploy_server args
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;container name: 배포할 컨테이너의 이름&lt;/li&gt;
&lt;li&gt;env file path: `.env` 파일 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;health_check args
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;health check URL: 헬스체크용 URL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool</category>
      <category>bash</category>
      <category>Docker</category>
      <category>nginx</category>
      <category>rolling deploy</category>
      <category>zero downtime</category>
      <category>롤링 배포</category>
      <category>무중단 배포</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/161</guid>
      <comments>https://backend-jaamong.tistory.com/161#entry161comment</comments>
      <pubDate>Fri, 19 Apr 2024 08:54:06 +0900</pubDate>
    </item>
    <item>
      <title>[NGINX] HTTPS 서버 구성하기 (Let's Encrypt)</title>
      <link>https://backend-jaamong.tistory.com/160</link>
      <description>&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-size: 14px; background: linear-gradient(to top, #aacf89 40%, transparent 40%); display: inline-block;&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SSL/TLS&lt;/li&gt;
&lt;li&gt;Certbot 설정과 SSL 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 과정&lt;/li&gt;
&lt;li&gt;더미 인증서 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인증서 갱신 자동화&lt;/li&gt;
&lt;li&gt;⭐️ 실제 진행 순서&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SSL/TLS&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전송 계승 상에서 클라이언트, 서버에 관한 인증 및 데이터 암호화 수행&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트와 서버 양단간 응용 계층 및 TCP 전송 계층 사이에서 수행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안용 프로토콜로 안전한 보안 채널을 형성하는 역할&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;SSL = Secure Socket Layer &lt;/i&gt;&lt;br /&gt;&lt;i&gt;TLS = Transport Layer Security &lt;/i&gt;&lt;br /&gt;&lt;i&gt;HTTPS = HTTP over SSL&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTP에 SSL이 적용된 HTTPS를 이용하여 통신을 암호화하면 보안을 높일 수 있다. 이를 위해서는 &lt;b&gt;SSL 인증서&lt;/b&gt;가 필요하다. 우리는 &lt;b&gt;CA&lt;/b&gt; 중 하나인 &lt;b&gt;Let's Encrypt&lt;/b&gt;를 통해 무료로 인증서를 발급받을 것이다. Let's Encrypt는 SSL 인증서를 발급해 주는 공인된 기관 중 하나이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Certbot&lt;/b&gt;은 Let's Encrypt에서 SSL 인증서 사용을 관리하는 무료 오픈소스 도구이다. certbot 명령을 사용하여 무료로 사용할 수 있는 SSL 인증서를 발급, 갱신, 삭제할 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;certbot을 활용하면 CLI를 통해 SSL 인증서를 발급받을 수 있지만, &lt;b&gt;90일마다 인증서를 재발급&lt;/b&gt;해야 하는 번거로움이 있다. 이러한 번거로움은&amp;nbsp;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;docker compose + nginx + certbot&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;을 사용하여 이 과정을 자동화한 설정 파일을 제공하는 곳을 통해 해결할 수 있다!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;  &lt;i&gt;&lt;b&gt;Guide&lt;/b&gt;&lt;/i&gt;: https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71&lt;br /&gt;  &lt;i&gt;&lt;b&gt;GitHub&lt;/b&gt;&lt;/i&gt;: https://github.com/wmnnd/nginx-certbot&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;SSL/TLS&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;설정 과정&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker-compose.yml에 아래와 같이 nginx와 certbot 이미지를 정의한다. `image` 태그 명은 원하는 대로 작성하면 된다.&lt;/span&gt;
&lt;pre id=&quot;code_1710223575131&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.5&quot;

services:
  ...

  nginx:
    image: project-nginx:latest
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # 호스트 머신에서의 nginx.conf 위치:도커 컨테이너에서의 nginx.conf 위치 

  certbot:
    image: certbot/certbot    

	...&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;`volumes` 블록 작성 시 nginx.conf의 위치를 확인하고 정확하게 작성한다. 나는 docker-compose.yml과 같은 위치에 nginx 디렉토리를 만들고 그 안에 nginx.conf를 만들었다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 &lt;b&gt;모든 요청을 HTTPS로 리다이렉트 하는&lt;/b&gt; 코드를 담은 nginx.conf를 작성한다. AWS EC2의 경우, 퍼블릭 IPv4 DNS가 아닌 Route53에 등록한 도메인을 입력한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710223809721&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
	listen 443 ssl;
	server_name {your_domain};  

	location / {
		proxy_pass http://{your_domain};  # upstream 사용 시 upstream 이름 작성
	}
}

server {
	listen 80;
	server_name {your_domain};  

	location / {
		return 301 https://$host$request_uri;  # redirects to https
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 nginx와 certbot을 연결한다. Let&amp;rsquo;s Encrypt는 도메인에서 `well-known URL`을 요청하여 도메인 검증을 수행한다. 도메인이 특정 응답(&amp;rdquo;&lt;i&gt;challenge&lt;/i&gt;&amp;rdquo;)을 받으면 유효한 것이다. 해당 응답 데이터는 certbot에서 제공되므로, nginx 컨테이너가 certbot의 파일을 제공할 수 있는 방법이 필요하다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;우선 두 개의 docker 볼륨이 필요하다. 하나는 유효성 검사용, 하나는 실제 인증서용이다. 1번의 docker-compose.yml 파일에 코드를 추가한다.&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1710224117570&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;`data` 디렉토리는 수동으로 생성하지 않아도 된다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1710224117570&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.5&quot;

services:
  ...

  nginx:
    image: project-nginx:latest
    volumes:
    	- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # 호스트 머신에서의 nginx.conf 위치:도커 컨테이너에서의 nginx.conf 위치 
        - ./data/certbot/conf:/etc/letsencrypt
        - ./data/certbot/www:/var/www/certbot

  certbot:
    image: certbot/certbot    
    volumes:
        - ./data/certbot/conf:/etc/letsencrypt
        - ./data/certbot/www:/var/www/certbot
	...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 nginx가 certbot에서 받은 challenge 파일을 제공할 수 있도록 하자. 아래 코드를 nginx.conf의 &lt;b&gt;port 80 섹션&lt;/b&gt;에 추가한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710224634652&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;location /.well-known/acme-challenge/ {
	root /var/www/certbot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1710224703900&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
	listen 443 ssl;
	server_name {your_domain};  

	location / {
    	proxy_pass http://{your_domain};  
	}
}

server {
	listen 80;
	server_name {your_domain};  

	location / {
		return 301 https://$host$request_uri;  
	}
		
	location /.well-known/acme-challenge/ {
		root /var/www/certbot;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 HTTPS 인증서를 참조해야 한다. 곧 `&lt;b&gt;생성될 인증서`&lt;/b&gt;와 인증서의 `private key`를 &lt;b&gt;port 443 섹션&lt;/b&gt;에 추가해야 한다.&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1710224893601&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
ssl_certificate_key /etc/letencrypt/live/example.org/privkey.pem;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1710224899250&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
	listen 443 ssl;
	server_name {your_domain};  

	ssl_certificate /etc/letsencrypt/live/{your_domain}/fullchain.pem;
	ssl_certificate_key /etc/letencrypt/live/{your_domain}/privkey.pem;

	location / {
		proxy_pass http://{your_domain};  
	}
}

server {
	listen 80;
	server_name {your_domain};  

	location / {
		return 301 https://$host$request_uri;  
	}
		
	location /.well-known/acme-challenge/ {
		root /var/www/certbot;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안과 관련된 코드 두 줄을 추가해 보자. 반드시 추가할 필요는 없으나, 보안을 생각한다면 넣는 것이 좋다.&lt;/span&gt;
&lt;pre id=&quot;code_1710224908017&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letencrypt/ssl-dhparams.pem;&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt; &lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;include /&amp;hellip;/options-ssl-nginx.conf&lt;/i&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; options-ssl-nginx.conf &lt;/b&gt;파일을 열어보면 SSL 관련 설정들이 적혀 있다.&lt;/span&gt;
&lt;pre id=&quot;code_1710226259368&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file. Contents are based on https://ssl-config.mozilla.org

ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_ciphers &quot;...&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;적혀있는 주석을 보면 알 수 있듯이, 해당 설정을 추가함으로써 &lt;b&gt;Certbot&lt;/b&gt;에서 &lt;b&gt;자동으로 보안 업데이트&lt;/b&gt;를 하도록 한다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;i&gt;ssl_dhparam&lt;/i&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; `dhparam`을 사용하면 client와 server 간의 key를 exchange 할 때 perfect security를 보장한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래가 최종 nginx.conf이다.&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1710226524149&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
	listen 443 ssl;
	server_name {your_domain};  

	# managed by Certbot
	ssl_certificate /etc/letsencrypt/live/{your_domain}/fullchain.pem;
	ssl_certificate_key /etc/letencrypt/live/{your_domain}/privkey.pem;
	include /etc/letsencrypt/options-ssl-nginx.conf;
	ssl_dhparam /etc/letencrypt/ssl-dhparams.pem;

	location / {
		proxy_pass http://{your_domain};  
	}
}

server {
	listen 80;
	server_name {your_domain};  

	location / {
		return 301 https://$host$request_uri;  
	}
		
	location /.well-known/acme-challenge/ {
		root /var/www/certbot;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;더미 인증서 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Nginx가 Let&amp;rsquo;s Encrypt 유효성 검증을 수행하도록 해야 하는데, Nginx는 인증서가 없으면 시작하지 않을 것이다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 더미 인증서를 먼저 생성해야 한다. 그리고 Nginx를 시작하고 더미 인증서를 지운 다음, 실제 인증서를 요청하는 과정으로 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다행인 점은! 위 가이드 링크에서 이 과정을 자동화한 스크립트를 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작업 디렉토리에 아래 명령어로 설치를 진행한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710226857531&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh &amp;gt; init-letsencrypt.sh&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도메인과 이메일 주소를 변경하려면 스크립트를 수정해야 한다. 도메인에는 nginx.conf에 적었던 도메인과 동일한 주소를 입력하고, 이메일은 인증서가 만료될 때 알림을 받을 용도이다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도커 볼륨의 디렉토리를 변경하는 경우, `data_path` 변수에도 적용해야 한다. 그러고 나서 아래 명령어를 실행한다.&amp;nbsp;&lt;/span&gt;
&lt;pre id=&quot;code_1710226865863&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ chmod +x init-letsencrypt.sh 
$ sudo ./init-letsencrypt.sh&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`./init-...` 명령으로 진행되지 않는다면 아래 `bash` 명령어로 진행한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710226977145&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo bash init-letsencyprt.sh&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인증서 갱신 자동화&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞에서 언급한 것처럼 인증서는 유효기간이 있으므로 갱신해줘야 한다. 그런데 생각보다 갱신하는 주기를 잊기가 쉬워서 나중에 문제가 발생하고 나서 조치를 취하는 경우가 많다. 그러한 문제를 방지할 겸 인증서 갱신을 자동화해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker-compose.yml의 `certbot` 섹션에 아래 코드를 추가한다. 이 코드는 Let&amp;rsquo;s Encrypt에서 권장하는 것처럼 12시간마다 인증서가 갱신되는지 확인한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710227225563&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;entrypoint: &quot;/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h &amp;amp; wait $${!}; done;'&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1710227232279&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.5&quot;

services:
  ...

  nginx:
    image: project-nginx:latest
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # 호스트 머신에서의 nginx.conf 위치:도커 컨테이너에서의 nginx.conf 위치 
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot

  certbot:
    image: certbot/certbot    
    volumes:
    	- ./data/certbot/conf:/etc/letsencrypt
        - ./data/certbot/www:/var/www/certbot
    entrypoint: &quot;/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h &amp;amp; wait $${!}; done;'&quot;

...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker-compose.yml의 `nginx` 섹션에 Nginx가 새로운 인증서를 리로드 하도록 코드를 추가해야 한다. &lt;/span&gt;
&lt;pre id=&quot;code_1710227240364&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;command: &quot;/bin/sh -c 'while :; do sleep 6h &amp;amp; wait $${!}; nginx -s reload; done &amp;amp; nginx -g \&quot;daemon off;\&quot;'&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1710227246663&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.5&quot;

services:
  ...

  nginx:
    image: project-nginx:latest
    command: &quot;/bin/sh -c 'while :; do sleep 6h &amp;amp; wait $${!}; nginx -s reload; done &amp;amp; nginx -g \&quot;daemon off;\&quot;'&quot;
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # 호스트 머신에서의 nginx.conf 위치:도커 컨테이너에서의 nginx.conf 위치 
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot

  certbot:
    image: certbot/certbot    
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    entrypoint: &quot;/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h &amp;amp; wait $${!}; done;'&quot;

	...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;⭐️ 실제 진행 순서&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기에서는 간략하게 실제 진행 순서를 다룬다. 위 순서대로 진행하면 좋겠지만, 더미 인증서를 발급받아야 하므로 그렇지 않다&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker-compose.yml을 작성한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710228721005&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.5&quot;

services:
  ...

  nginx:
    image: project-nginx:latest
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # 호스트 머신에서의 nginx.conf 위치:도커 컨테이너에서의 nginx.conf 위치 
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot

  certbot:
    image: certbot/certbot    
	volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
	...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;nginx.conf를 작성한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710228733366&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
	listen 80;
	server_name {your_domain};  
		
	location /.well-known/acme-challenge/ {
    	root /var/www/certbot;
	}

	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`docker compose up`을 진행한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 명령어를 입력하여 쉘 스크립트를 설치하고, 해당 스크립트를 수정한다. 스크립트에서 이메일과 도메인을 수정한 다음 실행한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710228747765&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh &amp;gt; init-letsencrypt.sh
$ chmod +x init-letsencrypt.sh
$ vi init-letsencrypt.sh 
$ sudo ./init-letsencrypt.sh&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스크립트 실행 후 로그를 보면서 인증서 발급이 성공했는지 확인한다. 에러 발생으로 발급되지 않았다면 로그를 보면서 수정해 나간다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인증서 발급이 완료되었으면 이제 HTTPS를 적용하자. nginx.conf를 수정한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710228756416&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
	listen 443 ssl;
	server_name {your_domain};  

	# managed by Certbot
	ssl_certificate /etc/letsencrypt/live/{your_domain}/fullchain.pem;
	ssl_certificate_key /etc/letencrypt/live/{your_domain}/privkey.pem;
	include /etc/letsencrypt/options-ssl-nginx.conf;
	ssl_dhparam /etc/letencrypt/ssl-dhparams.pem;

	location / {
		proxy_pass http://{your_domain};  
	}
}

server {
	listen 80;
	server_name {your_domain};  

	location / {
		return 301 https://$host$request_uri;  
	}
		
	location /.well-known/acme-challenge/ {
		root /var/www/certbot;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인증서를 자동 갱신하도록 설정하자. docker-compose.yml 파일을 수정한다.&lt;/span&gt;
&lt;pre id=&quot;code_1710228788067&quot; class=&quot;yml, yaml&quot; data-ke-language=&quot;yml, yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.5&quot;

services:
  ...

  nginx:
    image: project-nginx:latest
    command: &quot;/bin/sh -c 'while :; do sleep 6h &amp;amp; wait $${!}; nginx -s reload; done &amp;amp; nginx -g \&quot;daemon off;\&quot;'&quot;
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # 호스트 머신에서의 nginx.conf 위치:도커 컨테이너에서의 nginx.conf 위치 
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot

  certbot:
    image: certbot/certbot    
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    entrypoint: &quot;/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h &amp;amp; wait $${!}; done;'&quot;

	...&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수정 완료 후 다시 `docker compose up`을 실행한다. 그리고 HTTPS 프로토콜이 적용됐는지 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잘 적용됐다면 완료!&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool/Nginx</category>
      <category>Certbot</category>
      <category>https</category>
      <category>Let's Encrypt</category>
      <category>nginx</category>
      <category>SSL</category>
      <category>TLS</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/160</guid>
      <comments>https://backend-jaamong.tistory.com/160#entry160comment</comments>
      <pubDate>Fri, 12 Apr 2024 08:45:47 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] ACM(ELB)와 NGINX</title>
      <link>https://backend-jaamong.tistory.com/162</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;EC2를 사용하고 있어서 SSL 적용을 ACM으로 해보려고 했는데 실패하고 Let's Encrypt를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ACM SSL 인증서는 AWS Load Balancer, CloudFront, API Gateway를 통해서만 사용할 수 있다. 마침 로드 밸런싱을 Nginx로 하고 있었던 차에 ELB로 바꿔야지 하면서 ALB를 생성했었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성하고 난 다음에, 문제는 ALB에서 보내는 헬스 체크 요청이 Nginx(웹서버)에 도달하지 않았다. ALB의 리스너 설정이나 대상 그룹 설정 등을 모두 확인했으나 Nginx에 요청이 도달하지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 총 3개의 포트를 사용했던 것 같다. 아래 세 개 모두를 보안 그룹에 설정해 두었고, ALB 리스너 설정에도 추가해 두었다. (지금 생각해 보면 80, 443번 포트만 열어뒀어도 됐다) 요청을 처리하는 대상 그룹(target group) 문제인가 하고 확인해 봤는데, 인스턴스도 제대로 설정되어 있었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTP 요청을 위한 80번 포트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTPS 요청을 위한 443번 포트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Django 서버를 실행하고 있던 4100번 포트&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 와중에 4100번 포트의 헬스 체크는 `Healthy`로 잘 나타났고, 다른 포트들은 `Unhealthy`로 표시되었다.&amp;nbsp;&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;알게 된 것&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;진행하고 있던 프로젝트는 Django를 사용하고 있었고 Django는 배포하려면 웹서버가 필요하다. 이때 사용하는 대표적인 웹서버로 Apache, Nginx가 있지만, 아파치로 사용하는 방법은 더 이상 지원하지 않는 것 같아서 Nginx로 채택했었다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;문제는 Nginx로 SSL을 설정하려면 서버(EC2 인스턴스)에 설치된 SSL 인증서가 필요했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710306639461&quot; class=&quot;nginx, nginxconf&quot; data-ke-language=&quot;nginx, nginxconf&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssl_certificate /etc/letsencrypt/live/{domain}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{domain}/privkey.pem;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간혹 이런 &lt;b&gt;ACM + ELB + NGINX&lt;/b&gt;&amp;nbsp;조합으로도 SSL을 잘 적용한 글들이 보였는데, 또 다른 문제는 서버에서 실행되는 Nginx는 서버에 설치된 것이 아닌 도커 컨테이너 형태로 실행하고 있었다. (그래서였을까... 너무 힘들었다...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 끊임없는 삽질로 알게된 것은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;ACM으로 인증서를 얻어서 서버에 설치하는 것은 불가능&lt;/b&gt;&lt;/span&gt;하다는 사실이었다. 그래서 AWS에서 ELB나 CloudFront 등을 강제로 사용하게 하는 것 같다. 정확히는 &lt;i&gt;&quot;ELB에서 HTTPS를 사용하려면 ACM을 써라!&quot;&lt;/i&gt;인 것 같지만...&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #16191f;&quot;&gt;ELB에서 HTTPS 리스너를 생성하려면 로드 밸런서에 한 개 이상의 SSL 서버 인증서를 반드시 배포해야 합니다. ...(중략)... &lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; href=&quot;https://aws.amazon.com/certificate-manager/&quot;&gt;&lt;u&gt;&lt;span&gt;AWS Certificate Manager(ACM)&lt;/span&gt;&lt;/u&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;을 사용해 로드 밸런서를 위한 인증서를 생성하는 것이 좋습니다. ACM은 2048, 3072, 4096비트 길이의 RSA 인증서와 모든 ECDSA 인증서를 지원합니다. ACM은 Elastic Load Balancing과 통합하여 로드 밸런서에 인증서를 배포합니다. 자세한 내용은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;u&gt;&lt;a style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; href=&quot;https://docs.aws.amazon.com/acm/latest/userguide/&quot;&gt;AWS Certificate Manager 사용 설명서&lt;/a&gt;&lt;/u&gt;&lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;를 참조하세요.&lt;/span&gt;&lt;/i&gt;&lt;br /&gt;- &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/application/create-https-listener.html#https-listener-certificates&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS Document ALB&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결국 ACM과 ELB를 포기했다! 어찌 됐건 SSL 적용은 필요했기 때문에 Let's Encrypt를 사용하여 인증서를 발급하고 Nginx에 설정했다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;최근에는 &lt;b&gt;Nitro Enclaves&lt;/b&gt;라는 걸로 할 수 있는 것 같은데 별로 사용하지 않는 방법인 것 같다. 혹시 모르니까 링크 남겨두기...&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;  https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave-refapp.html&lt;br /&gt;  https://aws.amazon.com/ko/about-aws/whats-new/2020/10/announcing-aws-certificate-manager-for-nitro-enclaves/&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>ACM</category>
      <category>ELB</category>
      <category>nginx</category>
      <category>SSL</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/162</guid>
      <comments>https://backend-jaamong.tistory.com/162#entry162comment</comments>
      <pubDate>Sun, 7 Apr 2024 10:37:27 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] ACM을 이용한 HTTPS 적용</title>
      <link>https://backend-jaamong.tistory.com/158</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ACM&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ACM(AWS Certificate Manager)은 우리의 AWS 웹사이트와 애플리케이션을 보호하는 SSL/TLS 인증서를 생성 및 저장, 갱신한다. 비용은 무료이며, 연마다 갱신할 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;AWS Certificate Manager를 통해 프로비저닝 되는 퍼블릭 SSL/TLS 인증서는 무료이다. 애플리케이션 실행을 위해 생성한 AWS 리소스에 대해서만 비용을 지불하면 된다.&lt;/i&gt;&lt;br /&gt; &amp;nbsp;https://ap-northeast-2.console.aws.amazon.com/acm/home?region=ap-northeast-2#/welcome&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;HTTPS 적용&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;Note&lt;/b&gt;&lt;/span&gt;&amp;nbsp; ACM을 사용하려면 먼저 도메인이 등록되어 있어야 한다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ACM 요청&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS 메뉴에서&lt;/span&gt; &lt;a href=&quot;https://ap-northeast-2.console.aws.amazon.com/acm/home?region=ap-northeast-2#/certificates/list&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Certificate Manager&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에 접속한다. 그리고 아래 화면에서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;요청&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;2184&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dInh5j/btsFHn06bJv/cBcKL4ZqYcwI5H16T5QhKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dInh5j/btsFHn06bJv/cBcKL4ZqYcwI5H16T5QhKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dInh5j/btsFHn06bJv/cBcKL4ZqYcwI5H16T5QhKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdInh5j%2FbtsFHn06bJv%2FcBcKL4ZqYcwI5H16T5QhKk%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;600&quot; height=&quot;113&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;2184&quot; data-origin-height=&quot;410&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;span style=&quot;color: #000000;&quot;&gt;`퍼블릭 인증서 요청`을 선택하고 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&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-filename=&quot;Untitled.png&quot; data-origin-width=&quot;2132&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7IDtu/btsFHoZ0eep/ZfGfuPzABnwTRFfrqABeH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7IDtu/btsFHoZ0eep/ZfGfuPzABnwTRFfrqABeH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7IDtu/btsFHoZ0eep/ZfGfuPzABnwTRFfrqABeH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7IDtu%2FbtsFHoZ0eep%2FZfGfuPzABnwTRFfrqABeH0%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;600&quot; height=&quot;225&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;2132&quot; data-origin-height=&quot;800&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;span style=&quot;color: #000000;&quot;&gt;아래 화면에서 먼저 준비해 둔 도메인 이름을 작성한다. 이때 `*.example.co.kr`처럼 와일드카드를 적용할 수 있다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K7Oqn/btsFFKJKAMl/WWbZatkQk9JAYq5Mix2kY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K7Oqn/btsFFKJKAMl/WWbZatkQk9JAYq5Mix2kY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K7Oqn/btsFFKJKAMl/WWbZatkQk9JAYq5Mix2kY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK7Oqn%2FbtsFFKJKAMl%2FWWbZatkQk9JAYq5Mix2kY0%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;600&quot; height=&quot;190&quot; data-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;&lt;b&gt;완전히 정규화된 도메인 이름(FQDN)&lt;/b&gt;&lt;/i&gt; &lt;br /&gt;&lt;i&gt;완전히 정규화된 도메인 이름(FQDN)은 인터넷 상의 조직 또는 개인의 고유한 이름이다. .com 또는 .org와 같은 최상위 도메인 확장명이 마지막에 위치한다. SSL/TLS 인증서로 보호하려는 사이트의 정규 도메인 이름을 입력한다(예: &lt;a href=&quot;http://www.example.com).&quot;&gt;www.example.com).&lt;/a&gt; 동일한 도메인으로 여러 사이트를 보호하기 위해 와일드카드 인증서를 요청하려면 별표()를 사용한다. 예를 들어, &lt;a href=&quot;http://.example.com은&quot;&gt;.example.com은&lt;/a&gt; &lt;a href=&quot;http://www.example.com,&quot;&gt;www.example.com,&lt;/a&gt; &lt;a href=&quot;http://site.example.com&quot;&gt;site.example.com&lt;/a&gt; 및 &lt;a href=&quot;http://images.example.com을&quot;&gt;images.example.com을&lt;/a&gt; 보호한다.&amp;nbsp;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 화면에서 아래로 스크롤하고, &lt;b&gt;검증 방법&lt;/b&gt;과 &lt;b&gt;키 알고리즘&lt;/b&gt;을 아래 화면과 같이 선택한다. 그리고 맨 아래의 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;요청&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cY8glp/btsFDCTxyib/KssfO9qSBrbmDY82nqc9m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cY8glp/btsFDCTxyib/KssfO9qSBrbmDY82nqc9m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cY8glp/btsFDCTxyib/KssfO9qSBrbmDY82nqc9m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcY8glp%2FbtsFDCTxyib%2FKssfO9qSBrbmDY82nqc9m1%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;600&quot; height=&quot;370&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;952&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;span style=&quot;color: #000000;&quot;&gt;이후 아래와 같은 화면이 나오면서 인증서 요청이 진행된다. 발급이 완료되면 상태가 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;발급됨&lt;/b&gt;&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-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8Repk/btsFF3hYnzI/KIuaqha6UK9jvGIWL8EvVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8Repk/btsFF3hYnzI/KIuaqha6UK9jvGIWL8EvVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8Repk/btsFF3hYnzI/KIuaqha6UK9jvGIWL8EvVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8Repk%2FbtsFF3hYnzI%2FKIuaqha6UK9jvGIWL8EvVk%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;600&quot; height=&quot;154&quot; data-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;556&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;span style=&quot;color: #000000;&quot;&gt;다음으로 로드 밸런서를 생성해야 한다. 생성 방법은 아래 포스트를 참고!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;  &lt;/span&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/159&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024.04.05 - [Architecture &amp;amp; Tool/AWS] - [AWS] ELB 생성&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;검증 상태가 보류 중으로 지속되는 경우&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;참고 링크&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;  &lt;a href=&quot;https://repost.aws/ko/knowledge-center/acm-certificate-pending-validation&quot;&gt;https://repost.aws/ko/knowledge-center/acm-certificate-pending-validation&lt;/a&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위의 마지막 화면에서 &lt;b&gt;인증서 ID&lt;/b&gt;를 클릭한다. &lt;span style=&quot;text-align: start;&quot;&gt;이후 도메인 영역에서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;Route 53에서 레코드 생성&lt;/b&gt;&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-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RYGDY/btsFGTFY9Vq/n9ykkzXbslvnzJkx5FEKuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RYGDY/btsFGTFY9Vq/n9ykkzXbslvnzJkx5FEKuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RYGDY/btsFGTFY9Vq/n9ykkzXbslvnzJkx5FEKuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRYGDY%2FbtsFGTFY9Vq%2Fn9ykkzXbslvnzJkx5FEKuk%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;600&quot; height=&quot;125&quot; data-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;454&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;span style=&quot;color: #000000;&quot;&gt;이후 Route 53으로 이동하면 유형이 `CNAME`인 레코드가 생성되어 있는 것을 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;1018&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctkEg2/btsFGo7grqw/t41bNDMUUsc1CEkrgdXXH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctkEg2/btsFGo7grqw/t41bNDMUUsc1CEkrgdXXH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctkEg2/btsFGo7grqw/t41bNDMUUsc1CEkrgdXXH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctkEg2%2FbtsFGo7grqw%2Ft41bNDMUUsc1CEkrgdXXH0%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;600&quot; height=&quot;244&quot; data-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;1018&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>ACM</category>
      <category>AWS Certificate Manager</category>
      <category>https</category>
      <category>SSL</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/158</guid>
      <comments>https://backend-jaamong.tistory.com/158#entry158comment</comments>
      <pubDate>Fri, 5 Apr 2024 09:44:55 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] ELB 생성</title>
      <link>https://backend-jaamong.tistory.com/159</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS의 ELB를 이용하여 로드밸런싱을 해보자! 그전에 &lt;b&gt;ACM을 먼저 요청&lt;/b&gt;해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;  &lt;/span&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/158&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024.04.05 - [Architecture &amp;amp; Tool/AWS] - [AWS] ACM을 이용한 HTTPS 적용&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ELB 생성&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt; &amp;nbsp;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://aws-hyoh.tistory.com/147&quot;&gt;https://aws-hyoh.tistory.com/147&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ELB란?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://aws-hyoh.tistory.com/128&quot;&gt;AWS Elastic Load Balancer(ELB) 쉽게 이해하기 #1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/userguide/what-is-load-balancing.html&quot;&gt;Elastic Load Balancing이란 무엇인가요? - Elastic Load Balancing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ALB용 HTTPS 리스너 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kjw1313.tistory.com/m/121&quot;&gt;AWS CM과 로드 밸런스로 HTTPS로 변환하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/application/create-https-listener.html#https-listener-certificates&quot;&gt;Application Load Balancer용 HTTPS 리스너 생성 - Elastic Load Balancing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS EC2 왼쪽 메뉴에서&lt;/span&gt; &lt;b&gt;&lt;a title=&quot;AWS ELB&quot; href=&quot;https://ap-northeast-2.console.aws.amazon.com/ec2/home?region=ap-northeast-2#LoadBalancers:&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;로드 밸런서&lt;/a&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 클릭한다. 아래 화면에서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;로드 밸런서 생성&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pYtgm/btsFGnvcDzc/2akmn4pK74Y1hlSYBFrd91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pYtgm/btsFGnvcDzc/2akmn4pK74Y1hlSYBFrd91/img.png&quot; data-alt=&quot;create load balancer&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pYtgm/btsFGnvcDzc/2akmn4pK74Y1hlSYBFrd91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpYtgm%2FbtsFGnvcDzc%2F2akmn4pK74Y1hlSYBFrd91%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;600&quot; height=&quot;101&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;create load balancer&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 유형 중에서 `Application Load Balancer 아래의 &lt;b&gt;생성&lt;/b&gt; 버튼을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHqpmk/btsFKLHTOpQ/TKwBpk5LnEFnYVqcrGfnF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHqpmk/btsFKLHTOpQ/TKwBpk5LnEFnYVqcrGfnF1/img.png&quot; data-alt=&quot;Select load balancer type&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHqpmk/btsFKLHTOpQ/TKwBpk5LnEFnYVqcrGfnF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHqpmk%2FbtsFKLHTOpQ%2FTKwBpk5LnEFnYVqcrGfnF1%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;600&quot; height=&quot;543&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;1166&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Select load balancer type&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;로드 밸런서 이름&lt;/b&gt;을 입력한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rSXMA/btsFJA1bceu/pvt50xazt4uuTEePNAt1t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rSXMA/btsFJA1bceu/pvt50xazt4uuTEePNAt1t1/img.png&quot; data-alt=&quot;Basic configuration&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rSXMA/btsFJA1bceu/pvt50xazt4uuTEePNAt1t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrSXMA%2FbtsFJA1bceu%2Fpvt50xazt4uuTEePNAt1t1%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;600&quot; height=&quot;363&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;1006&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Basic configuration&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 매핑 영역에서 &lt;b&gt;EC2 인스턴스와 동일한 네트워크&lt;/b&gt;를 포함한 2개 이상의 영역을 선택한다. 예를 들어, 현재 사용 중인 인스턴스가 `ap-northeast-2a` 네트워크를 사용하고 있으면 해당 네트워크를 포함하여 선택하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2144&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcJGgb/btsFJnHMOZM/AyQKM41DmKNcdkzLQPwiVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcJGgb/btsFJnHMOZM/AyQKM41DmKNcdkzLQPwiVk/img.png&quot; data-alt=&quot;network region&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcJGgb/btsFJnHMOZM/AyQKM41DmKNcdkzLQPwiVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcJGgb%2FbtsFJnHMOZM%2FAyQKM41DmKNcdkzLQPwiVk%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;600&quot; height=&quot;20&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2144&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;network region&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잘 모르겠다면 모두 선택해도 괜찮다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1640&quot; data-origin-height=&quot;1090&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lLJBR/btsFJ5s5VmR/hd1yNhagwdPnFUCAtam8x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lLJBR/btsFJ5s5VmR/hd1yNhagwdPnFUCAtam8x1/img.png&quot; data-alt=&quot;Network mapping&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lLJBR/btsFJ5s5VmR/hd1yNhagwdPnFUCAtam8x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlLJBR%2FbtsFJ5s5VmR%2Fhd1yNhagwdPnFUCAtam8x1%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;600&quot; height=&quot;399&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1640&quot; data-origin-height=&quot;1090&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Network mapping&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;보안 그룹도 EC2 인스턴스와 동일한 것&lt;/b&gt;으로 선택한다. 모르겠다면 EC2의 ID를 클릭하여 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/span&gt; 탭에서 확인하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nfbff/btsFJ9vtzYj/aR6mDVEZmZ7yAwM0STm8o0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nfbff/btsFJ9vtzYj/aR6mDVEZmZ7yAwM0STm8o0/img.png&quot; data-alt=&quot;instance security group&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nfbff/btsFJ9vtzYj/aR6mDVEZmZ7yAwM0STm8o0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnfbff%2FbtsFJ9vtzYj%2FaR6mDVEZmZ7yAwM0STm8o0%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;600&quot; height=&quot;341&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;instance security group&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSwWdj/btsFGB8kjNL/6KSgk88T8mSvKWSdpoi7v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSwWdj/btsFGB8kjNL/6KSgk88T8mSvKWSdpoi7v0/img.png&quot; data-alt=&quot;Security groups&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSwWdj/btsFGB8kjNL/6KSgk88T8mSvKWSdpoi7v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSwWdj%2FbtsFGB8kjNL%2F6KSgk88T8mSvKWSdpoi7v0%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;600&quot; height=&quot;178&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Security groups&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리스너 및 라우팅 차례이다. 여기에서 HTTP 80 포트와 HTTPS 443 포트를 설정한다. 우선 아래 화면에서 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;대상 그룹 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2212&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baC3Nr/btsFKPp3BGa/BmKs9Dm45ZYi2xGuMxdrXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baC3Nr/btsFKPp3BGa/BmKs9Dm45ZYi2xGuMxdrXk/img.png&quot; data-alt=&quot;Listeners and routing&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baC3Nr/btsFKPp3BGa/BmKs9Dm45ZYi2xGuMxdrXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaC3Nr%2FbtsFKPp3BGa%2FBmKs9Dm45ZYi2xGuMxdrXk%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;600&quot; height=&quot;277&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2212&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Listeners and routing&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;그룹 세부 정보 지정의 기본 구성&lt;/b&gt;에서 &lt;b&gt;대상 유형&lt;/b&gt;은 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;인스턴스&lt;/b&gt;&lt;/span&gt;로 선택하고, &lt;b&gt;대상 그룹 이름&lt;/b&gt;을 입력한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIuNeo/btsFJ9I0IrV/V54jGhQZGj9pZh5sHD7sK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIuNeo/btsFJ9I0IrV/V54jGhQZGj9pZh5sHD7sK0/img.png&quot; data-alt=&quot;Specify group details - Basic configuration&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIuNeo/btsFJ9I0IrV/V54jGhQZGj9pZh5sHD7sK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIuNeo%2FbtsFJ9I0IrV%2FV54jGhQZGj9pZh5sHD7sK0%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;600&quot; height=&quot;581&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Specify group details - Basic configuration&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지는 그대로 두고, 맨 아래로 스크롤을 내려서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다음&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;1108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mR8ZB/btsFGNmSsWs/Gg25dZrZ1Anl7DoCDxdWCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mR8ZB/btsFGNmSsWs/Gg25dZrZ1Anl7DoCDxdWCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mR8ZB/btsFGNmSsWs/Gg25dZrZ1Anl7DoCDxdWCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmR8ZB%2FbtsFGNmSsWs%2FGg25dZrZ1Anl7DoCDxdWCk%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;600&quot; height=&quot;532&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;1108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;1042&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l3CN1/btsFGo8ImdT/DNGnFV1uPbDfPheE1cL4Y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l3CN1/btsFGo8ImdT/DNGnFV1uPbDfPheE1cL4Y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l3CN1/btsFGo8ImdT/DNGnFV1uPbDfPheE1cL4Y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl3CN1%2FbtsFGo8ImdT%2FDNGnFV1uPbDfPheE1cL4Y0%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;600&quot; height=&quot;510&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;1042&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;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&lt;span&gt;  &lt;/span&gt;&lt;i&gt;상태 검사 경로&lt;/i&gt;&lt;/b&gt;&lt;i&gt;는 로드 밸런서와 연결되어 있는 인스턴스가 살아있는지 정기적으로 확인하는 health check를 위한 경로이다. 만일 접속이 불가능한 경우 해당 인스턴스를 불능상태로 인식하고 트래픽을 보내지 않는다.&lt;/i&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 화면에서 로드 밸런서를 적용할 인스턴스를 선택하고, &lt;b&gt;아래에 보류 중인 것으로 포함&lt;/b&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;1098&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNqdlb/btsFGDEZZKF/RJghTSpgigSVlivPWfwL6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNqdlb/btsFGDEZZKF/RJghTSpgigSVlivPWfwL6k/img.png&quot; data-alt=&quot;Register targets&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNqdlb/btsFGDEZZKF/RJghTSpgigSVlivPWfwL6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNqdlb%2FbtsFGDEZZKF%2FRJghTSpgigSVlivPWfwL6k%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;600&quot; height=&quot;309&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;1098&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Register targets&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그다음 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;대상 그룹 생성&lt;/b&gt;&lt;/span&gt;을 클릭한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2098&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zUCWs/btsFJ2iQJx7/BoKK2ZPHu84xFqwuZUWeqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zUCWs/btsFJ2iQJx7/BoKK2ZPHu84xFqwuZUWeqk/img.png&quot; data-alt=&quot;Register targets - create target group&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zUCWs/btsFJ2iQJx7/BoKK2ZPHu84xFqwuZUWeqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzUCWs%2FbtsFJ2iQJx7%2FBoKK2ZPHu84xFqwuZUWeqk%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;600&quot; height=&quot;327&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2098&quot; data-origin-height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Register targets - create target group&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생성이 완료되면 아래와 같은 화면이 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2364&quot; data-origin-height=&quot;1150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buPvZ2/btsFJnVhGoM/cT3B6p0vkJxPV3IuukD3yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buPvZ2/btsFJnVhGoM/cT3B6p0vkJxPV3IuukD3yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buPvZ2/btsFJnVhGoM/cT3B6p0vkJxPV3IuukD3yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuPvZ2%2FbtsFJnVhGoM%2FcT3B6p0vkJxPV3IuukD3yk%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;600&quot; height=&quot;292&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2364&quot; data-origin-height=&quot;1150&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;span style=&quot;color: #000000;&quot;&gt;다시 리스너 및 라우팅으로 돌아와서 리스너 설정을 진행한다. &lt;b&gt;리스너 태그 추가&lt;/b&gt;를 클릭하여 HTTP, HTTPS 모두 생성하며, &lt;b&gt;모두 HTTP 대상 그룹을 선택&lt;/b&gt;한다. 생성한 대상 그룹이 보이지 않으면 옆에 새로 고침 버튼을 클릭하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;1154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqNQ5B/btsFGShHixD/WLRKUkQagSgpVnxZubUJDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqNQ5B/btsFGShHixD/WLRKUkQagSgpVnxZubUJDK/img.png&quot; data-alt=&quot;Listners and routing&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqNQ5B/btsFGShHixD/WLRKUkQagSgpVnxZubUJDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqNQ5B%2FbtsFGShHixD%2FWLRKUkQagSgpVnxZubUJDK%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;600&quot; height=&quot;396&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;1154&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Listners and routing&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;보안 리스너 설정&lt;/b&gt;에서 EC2 인스턴스에 적용한 ACM을 선택한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1960&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZn9PE/btsFJnAY7BK/XnF9Wk9xmd8y2XTq8JKGck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZn9PE/btsFJnAY7BK/XnF9Wk9xmd8y2XTq8JKGck/img.png&quot; data-alt=&quot;Secure listener settings&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZn9PE/btsFJnAY7BK/XnF9Wk9xmd8y2XTq8JKGck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZn9PE%2FbtsFJnAY7BK%2FXnF9Wk9xmd8y2XTq8JKGck%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;600&quot; height=&quot;343&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1960&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Secure listener settings&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;로드 밸런서 생성&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dant23/btsFGMBwQOE/xkddDU7VMUrbjHUCyavqhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dant23/btsFGMBwQOE/xkddDU7VMUrbjHUCyavqhK/img.png&quot; data-alt=&quot;create load balancer&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dant23/btsFGMBwQOE/xkddDU7VMUrbjHUCyavqhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdant23%2FbtsFGMBwQOE%2FxkddDU7VMUrbjHUCyavqhK%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;600&quot; height=&quot;112&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;create load balancer&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 아래 화면에서 아래로 스크롤하고, &lt;b&gt;리스너 및 규칙&lt;/b&gt;에서 `&lt;b&gt;HTTPS&lt;/b&gt;`를 선택합니다. 선택한 상태에서 &lt;b&gt;리스너 관리 &amp;gt; SNI용 SSL 인증서 추가&lt;/b&gt;를 클릭합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2694&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X29su/btsFHmWTXse/lusN6BvfPuhZJ5GIITsCfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X29su/btsFHmWTXse/lusN6BvfPuhZJ5GIITsCfk/img.png&quot; data-alt=&quot;Listeners and rules&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X29su/btsFHmWTXse/lusN6BvfPuhZJ5GIITsCfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX29su%2FbtsFHmWTXse%2FlusN6BvfPuhZJ5GIITsCfk%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;600&quot; height=&quot;249&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2694&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Listeners and rules&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이동된 화면에서 이전에 발급받은 ACM을 선택하고 &lt;b&gt;아래에 보류 중인 것으로 포함&lt;/b&gt;을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EdEIq/btsFI3QnjrD/2L3xH1Ve6Mttsf0QeohfL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EdEIq/btsFI3QnjrD/2L3xH1Ve6Mttsf0QeohfL0/img.png&quot; data-alt=&quot;Add certificate to listener&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EdEIq/btsFI3QnjrD/2L3xH1Ve6Mttsf0QeohfL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEdEIq%2FbtsFI3QnjrD%2F2L3xH1Ve6Mttsf0QeohfL0%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;600&quot; height=&quot;239&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Add certificate to listener&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;보류 중인 인증서 추가&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;2586&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yzBpM/btsFGI6Zj3d/LhIy3eTZwrYYxjYkfs8ukk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yzBpM/btsFGI6Zj3d/LhIy3eTZwrYYxjYkfs8ukk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yzBpM/btsFGI6Zj3d/LhIy3eTZwrYYxjYkfs8ukk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyzBpM%2FbtsFGI6Zj3d%2FLhIy3eTZwrYYxjYkfs8ukk%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;600&quot; height=&quot;174&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2586&quot; data-origin-height=&quot;748&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;번외.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나중에 필요한 리스너나 규칙을 추가하려면 로드 밸런서 창으로 돌아와 &lt;b&gt;리스너 및 규칙&lt;/b&gt;에서 &lt;b&gt;리스너 추가&lt;/b&gt;를 클릭하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lRSbY/btsFF4CCW7k/yWgS25d9kGgdBxG4usn9sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lRSbY/btsFF4CCW7k/yWgS25d9kGgdBxG4usn9sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lRSbY/btsFF4CCW7k/yWgS25d9kGgdBxG4usn9sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlRSbY%2FbtsFF4CCW7k%2FyWgS25d9kGgdBxG4usn9sk%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;600&quot; height=&quot;232&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;886&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;로드 밸런서 대상 그룹 unhealthy 이슈&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://xetown.com/questions/780401&quot;&gt;웹앱이 https이면 api서버와 소켓 서버도 https로 이루어져야 하나요?&lt;/a&gt;&lt;a href=&quot;https://yeojin-dev.github.io/blog/aws-django-intermediate-9/&quot;&gt;AWS로 Django 프로젝트 배포하기(중급) 9. ELB Health Check 통과하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://halfmoon95.tistory.com/11&quot;&gt;EC2 메타데이터 서비스 (IMDS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hyeon9mak.github.io/nginx-upstream-multi-server/&quot;&gt;NGINX 다중 서버 upstream 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://choo.oopy.io/0563a1cd-17b0-4513-9e59-49f0bd89834b&quot;&gt;로드밸런서 대상그룹 unhealthy 해결(with 권한설정)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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;✅&amp;nbsp;&lt;span style=&quot;color: #000000;&quot;&gt;Health Check URL&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTTPS 적용이 안 돼서 어떤 문제인지 확인해 봤더니, 등록한 모든 대상 그룹의 상태가 `Unhealthy`로 나와 있었다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2192&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uia6H/btsFF68k270/MMmzMyz4IIrukyPVBLksT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uia6H/btsFF68k270/MMmzMyz4IIrukyPVBLksT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uia6H/btsFF68k270/MMmzMyz4IIrukyPVBLksT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUia6H%2FbtsFF68k270%2FMMmzMyz4IIrukyPVBLksT0%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;600&quot; height=&quot;181&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2192&quot; data-origin-height=&quot;660&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;span style=&quot;color: #000000;&quot;&gt;생각해 보니 대상 그룹의 상태 확인을 위한 경로를 `root url(/)`로만 지정해 놓은 상태였다. 그래서 헬스 체크를 위한 코드를 추가하고 상태 확인 경로를 수정했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710220225500&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# views.py
@api_view(['GET'])
def health_check(request):
    &quot;&quot;&quot;
    헬스 체크용 함수 -&amp;gt; 별도의 로직 필요없음
    &quot;&quot;&quot;
    return Response(status.HTTP_200_OK)
    
# urls.py
urlpatterns = [
    ...
    path('health-check', views.health_check),
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 Django를 사용한 개발이므로 &lt;b&gt;prod.py&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;(==settings.py)&lt;/span&gt;의 `ALLOWED_HOSTS`에 &lt;b&gt;EC2 인스턴스의 프라이빗 IPv4 주소&lt;/b&gt;를 추가했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1710220346104&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
ALLOWED_HOSTS = [
    # 여기서는 실제 주소 대신 문자열로 대체
    
    &quot;ALLOWED_HOST_IP_PUB&quot;, 
    &quot;ALLOWED_HOST_IP_PRI&quot;,  # here
    &quot;ALLOWED_HOST_DOMAIN&quot;,
]
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 상태가 `Healthy`로 변한 것을 확인할 수 있다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2184&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mRTmZ/btsFGEcTWN9/Cmlkba3cTUSyrwCBteKU8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mRTmZ/btsFGEcTWN9/Cmlkba3cTUSyrwCBteKU8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mRTmZ/btsFGEcTWN9/Cmlkba3cTUSyrwCBteKU8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmRTmZ%2FbtsFGEcTWN9%2FCmlkba3cTUSyrwCBteKU8k%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;600&quot; height=&quot;174&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2184&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>ALB</category>
      <category>AWS</category>
      <category>ELB</category>
      <category>로드밸런서</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/159</guid>
      <comments>https://backend-jaamong.tistory.com/159#entry159comment</comments>
      <pubDate>Fri, 5 Apr 2024 09:35:53 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] EC2 인스턴스 용량(EBS 볼륨) 확장</title>
      <link>https://backend-jaamong.tistory.com/157</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;EC2 인스턴스 용량 확장&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;디스크 용량 확인&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1709712286000&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ df -hT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리눅스 `df` 명령어는 파일 시스템 디스크의 용량을 확인할 때 사용한다. `-h` 옵션은 사람이 보기에 편한 용량 크기로 출력한다. `-T` 옵션은 파일의 타입(Type)을 보여주는 항목을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgklfg/btsFBMUFNyM/t7qpi8SKQQuB5dxlEXgP3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgklfg/btsFBMUFNyM/t7qpi8SKQQuB5dxlEXgP3k/img.png&quot; data-alt=&quot;df -hT 결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgklfg/btsFBMUFNyM/t7qpi8SKQQuB5dxlEXgP3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcgklfg%2FbtsFBMUFNyM%2Ft7qpi8SKQQuB5dxlEXgP3k%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;540&quot; height=&quot;175&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;df -hT 결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 사용 중인 용량을 보니 인스턴스의 최대 용량인 8 GiB가 다 찼다! 추후 무엇을 설치할 수도 있고, 원활한 서비스 운영을 위해 인스턴스의 용량을 늘려야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인스턴스 볼륨(EBS) 추가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 화면에서 인스턴스가 사용하고 있는 블록 디바이스를 확인하고, `볼륨 ID`를 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V2K4l/btsFAFuRmGD/dAgPsS2dTUs9bJRQs141MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V2K4l/btsFAFuRmGD/dAgPsS2dTUs9bJRQs141MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V2K4l/btsFAFuRmGD/dAgPsS2dTUs9bJRQs141MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV2K4l%2FbtsFAFuRmGD%2FdAgPsS2dTUs9bJRQs141MK%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;600&quot; height=&quot;187&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클릭하면 아래와 같은 화면이 나타난다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2294&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buO4w1/btsFBJp9fTr/pz2K3sUH6tXpaDE0i6I5ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buO4w1/btsFBJp9fTr/pz2K3sUH6tXpaDE0i6I5ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buO4w1/btsFBJp9fTr/pz2K3sUH6tXpaDE0i6I5ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuO4w1%2FbtsFBJp9fTr%2Fpz2K3sUH6tXpaDE0i6I5ck%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;600&quot; height=&quot;96&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2294&quot; data-origin-height=&quot;368&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;span style=&quot;color: #000000;&quot;&gt;`볼륨 ID`를 &lt;b&gt;오른쪽 마우슨 버튼&lt;/b&gt;으로 클릭한다. 아래 화면이 나오면 `볼륨 수정`을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2294&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9d5WT/btsFB2QDaTC/FcLk4QICIHXKBfQgf0xPVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9d5WT/btsFB2QDaTC/FcLk4QICIHXKBfQgf0xPVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9d5WT/btsFB2QDaTC/FcLk4QICIHXKBfQgf0xPVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9d5WT%2FbtsFB2QDaTC%2FFcLk4QICIHXKBfQgf0xPVk%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;600&quot; height=&quot;256&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2294&quot; data-origin-height=&quot;978&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;span style=&quot;color: #000000;&quot;&gt;크기를 수정하고 `수정` 버튼을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhFKhd/btsFyvNop5l/TxGug3aNLsBNK9RtuqGcXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhFKhd/btsFyvNop5l/TxGug3aNLsBNK9RtuqGcXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhFKhd/btsFyvNop5l/TxGug3aNLsBNK9RtuqGcXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhFKhd%2FbtsFyvNop5l%2FTxGug3aNLsBNK9RtuqGcXK%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;600&quot; height=&quot;403&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1658&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;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;운영 중인 서비스가 도커 컨테이너를 여러 대를 띄우다 보니 적어도 모두 띄웠을 때 서버가 다운되지 않을 정도의 용량이 필요했다. 그래서 아래 명령어를 입력하여 현재 서비스가 사용하고 있는 리소스를 확인했다. 아래 명령어 이외에도 사용하고 있는 도커 용량을 확인할 수 있는 명령어들을 찾아서 얼마만큼의 리소스가 필요한지 정리하면 좋을 것 같다.&lt;br /&gt;
&lt;pre id=&quot;code_1709713184182&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker system df -v​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`수정` 버튼을 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3XRFB/btsFABF14oH/j462qDk32AaVubGWXKrYLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3XRFB/btsFABF14oH/j462qDk32AaVubGWXKrYLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3XRFB/btsFABF14oH/j462qDk32AaVubGWXKrYLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3XRFB%2FbtsFABF14oH%2Fj462qDk32AaVubGWXKrYLK%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;600&quot; height=&quot;390&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그러고 나면 아래처럼 &lt;b&gt;수정 중&lt;/b&gt;이라는 안내가 나온다. 적용까지는 몇 분 정도 소요될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O9QHe/btsFBMf5vKw/lP44iQkncVTDn6Cz3OjKY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O9QHe/btsFBMf5vKw/lP44iQkncVTDn6Cz3OjKY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O9QHe/btsFBMf5vKw/lP44iQkncVTDn6Cz3OjKY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO9QHe%2FbtsFBMf5vKw%2FlP44iQkncVTDn6Cz3OjKY0%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;600&quot; height=&quot;299&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;542&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;span style=&quot;color: #000000;&quot;&gt;적용이 완료되면 아래처럼 적용된 크기를 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c59Ok5/btsFwB1w1d6/80iuyRBWJCTSP7xyOF3izK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c59Ok5/btsFwB1w1d6/80iuyRBWJCTSP7xyOF3izK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c59Ok5/btsFwB1w1d6/80iuyRBWJCTSP7xyOF3izK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc59Ok5%2FbtsFwB1w1d6%2F80iuyRBWJCTSP7xyOF3izK%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;600&quot; height=&quot;139&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;384&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;파티션 크기 늘리기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리눅스의 `lsblk` 명령어는 블록 장치 관련 작업을 도와주는 유틸리티 CLI 도구로, 블록 장치를 나열한다. 아래 사진은 볼륨 크기를 확장하기 전 상태다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3ftF6/btsFyMaqBsy/pKku7tLYFT9E877G1n9Sx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3ftF6/btsFyMaqBsy/pKku7tLYFT9E877G1n9Sx0/img.png&quot; data-alt=&quot;볼륨 크기 확장 전 lsblk&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3ftF6/btsFyMaqBsy/pKku7tLYFT9E877G1n9Sx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3ftF6%2FbtsFyMaqBsy%2FpKku7tLYFT9E877G1n9Sx0%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;600&quot; height=&quot;285&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;볼륨 크기 확장 전 lsblk&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;첫 번째 열의 각 의미는 순서대로 장치 이름, 해당 주 및 부 장치 번호, 장치가 제거 가능한지 여부(제거 가능한 경우 1), 장치 크기, 장치가 읽기 전용인지 여부, 장치 유형(디스크, 파티션 등), 마지막으로 장치 마운트 지점(사용 가능한 경우)이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 사진은 볼륨 크기를 확장한 후 상태이다. 디스크(disk)의 크기가 32G로 늘어난 것을 확인할 수 있다. 이제 볼륨의 파티션을 확장해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzIjM/btsFxWLfw1E/MOr3sEJKiH37nvum3ehPmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzIjM/btsFxWLfw1E/MOr3sEJKiH37nvum3ehPmK/img.png&quot; data-alt=&quot;볼륨 크기 확장 후 lsblk&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzIjM/btsFxWLfw1E/MOr3sEJKiH37nvum3ehPmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzIjM%2FbtsFxWLfw1E%2FMOr3sEJKiH37nvum3ehPmK%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;600&quot; height=&quot;249&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;볼륨 크기 확장 후 lsblk&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 사진에서 `xvda`는 &lt;b&gt;물리적인 디스크&lt;/b&gt;를 의미하고, `xvda1`, `xvda14`, `xvda15`는 &lt;b&gt;파티션&lt;/b&gt;을 의미한다. 디스크 영역은 32G로 늘어났지만, 파티션의 영역은 여전히 8G만 사용하고 있는 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉 리눅스는 32GB의 디스크를 가지고 있지만, 실제로는 8GB만 사용할 수 있는 상태이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 명령어를 입력하여 파티션을 확장하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709713854773&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo growpart {디스크명} 1
$ sudo growpart /dev/xvda 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후 아래 명령어를 입력하여 파티션이 확장되었는지 확인한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709713876324&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ lsblk&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dm7SX/btsFAhgIXkO/hlLz3tQLmlFbmOfVdt25v1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dm7SX/btsFAhgIXkO/hlLz3tQLmlFbmOfVdt25v1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dm7SX/btsFAhgIXkO/hlLz3tQLmlFbmOfVdt25v1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDm7SX%2FbtsFAhgIXkO%2FhlLz3tQLmlFbmOfVdt25v1%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;600&quot; height=&quot;184&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;확장 전과 비교했을 때 파티션의 크기가 확장된 것을 확인할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ext4 파일 시스템의 크기 늘리기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 디스크 공간을 확장해야 한다. 사용하고 있는 &lt;b&gt;파일 시스템&lt;/b&gt;에 바뀐 파티션의 크기를 적용해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 명령어로 마운트 영역을 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709714009676&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ df -hT&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2IswR/btsFA66WfuZ/oZP8zqwlGIjbfVCvx8Ye3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2IswR/btsFA66WfuZ/oZP8zqwlGIjbfVCvx8Ye3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2IswR/btsFA66WfuZ/oZP8zqwlGIjbfVCvx8Ye3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2IswR%2FbtsFA66WfuZ%2FoZP8zqwlGIjbfVCvx8Ye3k%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;600&quot; height=&quot;146&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;/dev/root&lt;/b&gt;&lt;/i&gt;를 보면 여전히 8GB만 사용하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 디스크 공간을 확장하자. (위 사진에서 `Type` 항목이 `ext4`인 경우)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709714183360&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo resize2fs {파일시스템명}
$ sudo resize2fs /dev/root&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVmYby/btsFCyBG5GO/YKU16aHnPgdI8DMBkcjGz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVmYby/btsFCyBG5GO/YKU16aHnPgdI8DMBkcjGz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVmYby/btsFCyBG5GO/YKU16aHnPgdI8DMBkcjGz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVmYby%2FbtsFCyBG5GO%2FYKU16aHnPgdI8DMBkcjGz0%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;600&quot; height=&quot;154&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;224&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;span style=&quot;color: #000000;&quot;&gt;이제 공간 확장이 완료되었다!&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; AWS 공식 문서&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OS 및 사양에 따라 진행하면 된다. 사용하는 인스턴스의 파일 시스템이 무엇인지 잘 확인하고 진행하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ebs/latest/userguide/recognize-expanded-volume-linux.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/ebs/latest/userguide/recognize-expanded-volume-linux.html&lt;/a&gt;&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;번외. EBS 볼륨 유형 gp2 vs gp3&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;gp3는 gp2보다 20% 가량 비용을 아낄 수 있으면서, &lt;b&gt;볼륨 크기와 관계없이 3,000 IOPS의 기준 성능&lt;/b&gt;과&amp;nbsp;&lt;b&gt;125Mib/s의 처리량&lt;/b&gt;을 제공한다. gp2는 고성능의 IO가 필요한 시스템에서는 불필요하게 더 많은 스토리지 볼륨이 요구된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;  IOPS(Input/Output Operations Per Second)란 EBS 볼륨의 성능을 측정하는 중요한 지표 중 하나로, 초당 입출력 작업 수를 나타냅니다. 즉, 높으면 높을수록 파일의 읽고 쓰는 것이 빨라진다는 뜻이다.&lt;br /&gt;IOPS를 계산할 때는 사용하는 애플리케이션이나 데이터베이스의 I/O 최대크기를 확인해야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;0&quot;&gt;EBS 볼륨 유형의 요금에 관한 자세한 정책 및 내용은 아래 링크를 참고!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &amp;nbsp; &lt;a href=&quot;https://aws.amazon.com/ko/ebs/pricing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aws.amazon.com/ko/ebs/pricing/&lt;/a&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>AWS</category>
      <category>EC2</category>
      <category>GP2</category>
      <category>GP3</category>
      <category>디스크 용량 확인</category>
      <category>용량 확장</category>
      <category>인스턴스 볼륨 추가</category>
      <category>파티션 크기</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/157</guid>
      <comments>https://backend-jaamong.tistory.com/157#entry157comment</comments>
      <pubDate>Fri, 29 Mar 2024 10:51:30 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] EC2 인스턴스 내 Docker 설치와 Django 프로젝트 설정</title>
      <link>https://backend-jaamong.tistory.com/156</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span&gt; &lt;/span&gt; 이 글에서는 아래의 주제를 다룹니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스 내 Docker 설치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트 클론&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파이썬 설치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가상환경(venv) 설치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;패키지(requirements.txt) 설치&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 실행 (docker compose)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;i&gt;&lt;b&gt;Notice&lt;/b&gt;&lt;/i&gt;&lt;/span&gt; EC2 AMI - Ubuntu 22.04 LTS 기준 작성&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인스턴스 내 Docker 설치&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ubuntu에서 Docker를 설치하는 방법은 Docker 공식 홈페이지에 있는 &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;a style=&quot;color: #0593d3;&quot; title=&quot;ubuntu에 docker 설치&quot; href=&quot;https://docs.docker.com/engine/install/ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;Ubuntu 설치&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/a&gt;&lt;/span&gt;를 보고 따라 하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스에 접속한다. 접속 방법은 vscode나 터미널 등으로 하면 된다. vscode로 접속하는 방법은&lt;/span&gt; &lt;b&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/154&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;&lt;/b&gt;를 참고!&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;접속(서버 연결)이 완료됐다면 터미널에 아래와 같이 입력하여 우분투 시스템 패키지를 업데이트한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709354484003&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get update&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker의 `apt` 레포지토리를 설정한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709354484003&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Add Docker's official GPG key:
$ sudo apt-get update
$ sudo apt-get install ca-certificates curl
$ sudo install -m 0755 -d /etc/apt/keyrings
$ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
$ sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
$ echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release &amp;amp;&amp;amp; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null
$ sudo apt-get update&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker 패키지를 설치한다. 설치 시 가장 최신 버전을 원하는 경우 아래와 같이 입력한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709354538634&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로 잘 설치됐는지 확인하기 위하여 아래 명령어를 입력한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709354569615&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo docker run hello-world&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;입력 후 터미널에 나오는 문구가 아래와 같다면 정상적으로 설치가 완료된 것이다.&lt;/span&gt;
&lt;pre id=&quot;code_1709354591893&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
c1ec31eb5944: Pull complete 
Digest: sha256:4bd78111b6914a99dbc560e6a20eab57ff6655aea4a80c50b0c5491968cbc2e6
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the &quot;hello-world&quot; image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;번외.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker 설치 이후 `docker ps` 명령을 입력했을 때 `permission denied` 에러가 발생하는 경우 아래와 같이 진행한다`.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;docker 그룹에 user 추가&lt;/span&gt;
&lt;pre id=&quot;code_1709354800625&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo usermod -aG docker {계정명}
$ cat /etc/group | grep docker&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;서버 재접속 후 `docker ps` 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1번 방법으로 해결이 안 된다면 직접 권한을 부여해야 한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709354836497&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo chmod 666 /var/run/docker.sock&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;위 명령어는 &lt;i&gt;/var/run/docker.sock&lt;/i&gt; 파일의 소유자, 그룹, 다른 모든 사용자들의 권한을 읽기와 쓰기로 설정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;프로젝트 클론&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클론 전 EC2 인스턴스에 본인의 계정을 추가한다. 관련 방법은&lt;/span&gt; &lt;b&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/151&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;&lt;/b&gt; &lt;span style=&quot;color: #000000;&quot;&gt;링크에서 &lt;b&gt;번외 중 계정 추가&lt;/b&gt;를 참고한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;레포지토리 클론&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클론 전에 EC2 인스턴스에 본인의 계정을 추가했다면 `sudo su {계정명}`으로 계정 스위칭을 진행한다. 그다음 `git config --global`을 이용하여 본인의 버전 관리 시스템(GitHub, Gitea,...) 계정을 추가한다. 자세한 Git 계정 설정은 &lt;b&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://git-scm.com/book/ko/v2/%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-Git-%EC%B5%9C%EC%B4%88-%EC%84%A4%EC%A0%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;&lt;/b&gt; 링크를 참고한다.&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;&lt;span style=&quot;color: #000000;&quot;&gt;이제 클론! 터미널에 아래 명령어를 입력한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709355413630&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ git clone {git_remote_url}.git&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서브 모듈이 있다면 아래와 같이 입력한다. 단, 아래 명령어는 &lt;b&gt;Git 2.13&lt;/b&gt; 버전부터 가능하니 그전에 `git --version` 명령어로 버전을 확인하자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709355448866&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ git clone --recurse-submodules &amp;lt;remote-repo-url&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;버전을 충족하지 않는 경우&lt;/span&gt; &lt;b&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/146&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;&lt;/b&gt; &lt;span style=&quot;color: #000000;&quot;&gt;글을 참고하자.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;디렉터리 위치 옮기기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ubuntu에서 각 디렉터리의 의미를 확인해 보고, 적절한 곳에 클론 받은 디렉터리/프로젝트를 옮겨보자.&amp;nbsp;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1709355554104&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;리눅스 디렉토리 종류와 특징&quot; data-og-description=&quot;디렉토리란 파일 저장소를 의미하며, 리눅스 디렉토리는 최상위 디렉토리를 기준으로 하위 디렉토리들이 존...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://m.blog.naver.com/hongjg3229/221814662731&quot; data-og-url=&quot;https://blog.naver.com/hongjg3229/221814662731&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/n3KJ1/hyVumyRlbW/f2YAv27T7vIj8KBHQuQxKK/img.png?width=690&amp;amp;height=419&amp;amp;face=0_0_690_419&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/hongjg3229/221814662731&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://m.blog.naver.com/hongjg3229/221814662731&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/n3KJ1/hyVumyRlbW/f2YAv27T7vIj8KBHQuQxKK/img.png?width=690&amp;amp;height=419&amp;amp;face=0_0_690_419');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;리눅스 디렉토리 종류와 특징&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;디렉토리란 파일 저장소를 의미하며, 리눅스 디렉토리는 최상위 디렉토리를 기준으로 하위 디렉토리들이 존...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 `/home/`에 위치시켰다. (그냥 root에 놓을 걸 그랬나...)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709355588944&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ mv {현재 위치}/{repo 이름} /home/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Permission denied` 에러 발생 시 `mv` 명령어 앞에 `sudo`를 붙여서 다시 실행한다.&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;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;파이썬 설치&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;파이썬 설치 참고 자료&lt;br /&gt;&amp;nbsp; &lt;i&gt; https://mungto.tistory.com/289&lt;/i&gt;&lt;br /&gt;&amp;nbsp;  &lt;i&gt;https://ko.linux-console.net/?p=15722&lt;/i&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; ⚠️ &lt;b&gt;SSL 적용&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파이썬 설치 전에 OpenSSL을 설치 및 적용해야 한다. 그렇지 않으면 pip 사용 시 아래와 같은 에러를 만날 수 있기 때문이다...&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709356050531&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(.venv) {계정명}@{IP}:/home/{프로젝트명}$ pip install -r requirements.txt
WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(&quot;Can't connect to HTTPS URL because the SSL module is not available.&quot;)': /simple/aiobotocore/
WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(&quot;Can't connect to HTTPS URL because the SSL module is not available.&quot;)': /simple/aiobotocore/
WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(&quot;Can't connect to HTTPS URL because the SSL module is not available.&quot;)': /simple/aiobotocore/
WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(&quot;Can't connect to HTTPS URL because the SSL module is not available.&quot;)': /simple/aiobotocore/
WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(&quot;Can't connect to HTTPS URL because the SSL module is not available.&quot;)': /simple/aiobotocore/
Could not fetch URL https://pypi.org/simple/aiobotocore/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/aiobotocore/ (Caused by SSLError(&quot;Can't connect to HTTPS URL because the SSL module is not available.&quot;)) - skipping
ERROR: Could not find a version that satisfies the requirement aiobotocore==2.5.0 (from versions: none)
ERROR: No matching distribution found for aiobotocore==2.5.0
WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
Could not fetch URL https://pypi.org/simple/pip/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/pip/ (Caused by SSLError(&quot;Can't connect to HTTPS URL because the SSL module is not available.&quot;)) - skipping&lt;/code&gt;&lt;/pre&gt;
&lt;figure id=&quot;og_1709356119266&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;SSLError(&amp;quot;Can't connect to HTTPS URL because the SSL module is not available.&quot; data-og-description=&quot;python3.7을 설치한 후 pip까지 설치가 끝난 후 pip로 모듈을 다운로드 받을려고 할때 아래와 같은 에러가 ...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://m.blog.naver.com/theswice/221813774822&quot; data-og-url=&quot;https://blog.naver.com/theswice/221813774822&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b8ITeV/hyVuiDd4md/mu5WLQqlDRqR6knxnr5E40/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/theswice/221813774822&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://m.blog.naver.com/theswice/221813774822&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b8ITeV/hyVuiDd4md/mu5WLQqlDRqR6knxnr5E40/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SSLError(&quot;Can't connect to HTTPS URL because the SSL module is not available.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;python3.7을 설치한 후 pip까지 설치가 끝난 후 pip로 모듈을 다운로드 받을려고 할때 아래와 같은 에러가 ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만일 벌써 발생했다면, 파이썬을 제거한 후 재설치해야 한다. 인터넷에 나오는 대중적인 방법과 여러 방법을 다 시도했을 때 다 실패해서 재설치했다 . (아마 찾으면 대부분 ` &lt;span style=&quot;text-align: start;&quot;&gt;--trusted-host` 옵션을 알려주는데 보안적으로 좋지 않은 방법이라 별로 추천하고 싶지 않다. 그냥 깔끔하게 재설치하자^^) &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Ubuntu 환경에서 특정 버전의 파이썬을 제거하는 방법은&lt;/span&gt; &lt;b&gt;&lt;a href=&quot;https://backend-jaamong.tistory.com/155&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 참고하자.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이제 &lt;b&gt;OpenSSL&lt;/b&gt; 및 관련 라이브러리를 설치한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get update
$ sudo apt-get install openssl libssl-dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;완료했으면 파이썬 설치로 넘어간다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;파이썬 설치&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우분투 시스템 패키지를 업데이트한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get update&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음 링크에 들어가서 원하는 버전을 받을 수 있는 주소를 찾는다.&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&amp;rarr;   &lt;a href=&quot;https://www.python.org/ftp/python/&quot;&gt;https://www.python.org/ftp/python/&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;text-align: left;&quot;&gt;예를 들어, 3.8.18 버전이라면 다음과 같은 주소가 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&amp;rarr; &lt;a href=&quot;https://www.python.org/ftp/python/3.8.18/&quot;&gt;https://www.python.org/ftp/python/3.8.18/&lt;/a&gt;&lt;/span&gt; &lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우분투에서 2번에서 찾은 경로를 이용하여 다운로드를 진행한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo wget https://www.python.org/ftp/python/3.8.18/Python-3.8.18.tgz&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다운로드를 완료하면 압축을 풀고 해당 폴더로 이동한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo tar zxf Python-3.8.18.tgz
$ cd Python-3.8.18/&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파이썬 설치에 필요한 필수 종속성과 패키지를 설치하기 전에 종속성을 설치하려면 다중 저장소 저장소를 활성화해야 한다고 한다. 아래 주어진 명령을 실행하여 활성화할 수 있다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-add-repository multiverse&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;종속성 설치를 진행한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get install build-essential checkinstall 
$ sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설치가 완료되었으면, 컴파일을 진행한다. 위치는 압축을 해제한 파이썬 디렉터리 안이다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo ./configure --enable-optimizations&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 적용하는 단계로, 기존 Python 바이너리를 덮어쓰는 것을 방지한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo -H make altinstall&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;완료 후 파이썬 버전을 확인한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ python3 --version&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;버전이 바뀌지 않았다면 `PATH`를 변경한다. 버전이 바뀌었다면 변경은 넘어가도 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;PATH 변경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 명령어로 위치를 찾는다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ which python
/usr/bin/python3&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 경로를 기억하고 설정을 위해 아래 명령어를 입력한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vim ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파일 맨 하단에 아래와 같이 입력하고 vim을 종료한다. `PATH`와 `=` 사이는 띄어쓰기 없이 입력해야 한다.&amp;nbsp;&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

export PATH={which python 결과}:$PATH&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`PATH`에 1번에서 입력한 명령어의 결과물을 입력한다. 아래는 예시다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

export PATH=/usr/bin/python3:$PATH&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;변경한 내용을 적용하기 위해 아래와 같이 입력한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ source ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;버전을 확인한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ python3 --version&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;가상환경(venv) 설치&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우분투 시스템 패키지를 업데이트한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get update
$ sudo apt-get upgrade&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`pip`가 설치되어 있지 않다면, `python3-pip`를 설치한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ pip --version  # pip 버전 확인
$ sudo apt-get install python3-pip  # 없으면 설치&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파이썬 가상환경 패키지를 설치한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt-get install python3-venv&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가상환경을 설치할 곳으로 이동하여 다음 명령어를 입력하여 가상환경을 생성한다. 이때 개발 환경에 맞는 파이썬 버전의 가상환경을 생성한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 터미널에 python을 입력했을 때 나오는 파이썬 버전으로 생성
$ python3 -m venv {원하는 이름}
$ python3 -m venv .venv

# 또는 특정 버전의 파이썬 버전으로 생성
python3.8 -m venv .venv&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #000000;&quot;&gt;꼭 `.venv`로 할 필요는 없으나, 대부분 해당 이름으로 진행한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설치가 완료되었으면 가상환경을 활성화할 수 있다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356976368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ source .venv/bin/activate  # 활성화
$ deactivate  # 비활성화&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;패키지(requirements.txt) 설치&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위에서 Django 프로젝트를 클론 했을 때 `requirements.txt` 파일이 있을 수 있다. 해당 파일을 이용하여 필요한 패키지들을 설치한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709358771301&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ python3 -m pip install -r requirements.txt&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;⚠️ `pip` 사용 시 `sudo`를 함께 사용하지 마세요. 나중에 권한 문제가 발생합니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;pip 에러 관련 1&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 설치 중에 특정 패키지 구간에서 에러가 난다면, 에러문구를 잘 보고 반드시 구글링 하자. 대부분 필요한 종속성 중에 없는 것을 설치하라고 알려줄 테니, 해당 종속성을 설치하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;pip 에러 관련 2&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 가상환경에서 패키지 설치 시 아래와 동일한 에러가 나면 권한 설정을 해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709358930375&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ERROR: Could not install packages due to an OSError: [Errno 13] Permission denied: '/venv_3.8/lib/python3.8/site-packages/sqlparse'
Check the permissions.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 명령어로 권한 설정을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709358951053&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo chown -R $(whoami) {가상환경 경로}
$ sudo chown -R $(whoami) /home/{프로젝트명}/.venv/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;서버 실행 (docker compose)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 환경 설정이 완료됐다면, 이전 서버에서 사용했던 그대로 &lt;b&gt;docker compose&lt;/b&gt;를 실행하고 오류가 발생하는지 확인하자.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1709359051058&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker compose up --build&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;DB 연결 확인&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잘 띄워졌으면 DB 연동에도 문제가 없는 상태일 것이다. 도커 컨테이너를 띄우는 과정에서 DB 연동으로 에러가 발생한다면 아래 항목을 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보안 그룹에서 해당 DB를 위한 포트(port)가 열려있는지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 DB에 접근 권한이 있는지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB 계정을 정확하게 작성했는지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;접근하는 EC2 인스턴스의 IP를 잘 기입했는지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;API 호출 확인&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모두 해결하고 컨테이너도 잘 띄워졌다면 이번에는 API 호출을 테스트한다. 포스트맨을 이용하여 API를 호출해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2006&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lIZrV/btsFlPeKgwh/v3BAKk1jIZRW8gmW3ByXFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lIZrV/btsFlPeKgwh/v3BAKk1jIZRW8gmW3ByXFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lIZrV/btsFlPeKgwh/v3BAKk1jIZRW8gmW3ByXFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlIZrV%2FbtsFlPeKgwh%2Fv3BAKk1jIZRW8gmW3ByXFk%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;600&quot; height=&quot;38&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2006&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 사진은 포스트맨에서 API를 호출한 화면이다. `host_pub_ip` 변수는 EC2 인스턴스의 &lt;b&gt;public IP&lt;/b&gt;, `host_port`에는 Django 서버용으로 도커 컨테이너에서 열려있는 &lt;b&gt;port&lt;/b&gt;를 의미한다. https 적용 전이므로 &lt;b&gt;http 프로토콜로 요청&lt;/b&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GQV5X/btsFpQQ1Gnf/rbAUgX3Ry6UbA3no7DUWzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GQV5X/btsFpQQ1Gnf/rbAUgX3Ry6UbA3no7DUWzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GQV5X/btsFpQQ1Gnf/rbAUgX3Ry6UbA3no7DUWzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGQV5X%2FbtsFpQQ1Gnf%2FrbAUgX3Ry6UbA3no7DUWzk%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;600&quot; height=&quot;105&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;352&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;color: #000000;&quot;&gt;&lt;i&gt;&lt;b&gt;이제 AWS EC2 인스턴스 생성 및 환경 설정, Django 서버 실행하기까지 모두 끝났다!&lt;/b&gt;&lt;/i&gt;&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;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>docker compose 실행</category>
      <category>ec2 도커 설치</category>
      <category>git clone</category>
      <category>ubuntu 도커 설치</category>
      <category>ubuntu 파이썬 설치</category>
      <category>가상환경 설치</category>
      <category>파이썬 삭제</category>
      <category>패키지 설치</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/156</guid>
      <comments>https://backend-jaamong.tistory.com/156#entry156comment</comments>
      <pubDate>Sat, 23 Mar 2024 09:21:05 +0900</pubDate>
    </item>
    <item>
      <title>VSCode로 AWS EC2 인스턴스 접속</title>
      <link>https://backend-jaamong.tistory.com/154</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Visual Studio Code로 AWS EC2 인스턴스에 접속하는 방법을 다룬다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;과정&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 설치된 vscode에 접속한다. vscode에 remote 관련 extension이 없다면 설치해야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daPNfD/btsFn8wJ1Od/LDX8Ogpkmq56H8KSkyMj7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daPNfD/btsFn8wJ1Od/LDX8Ogpkmq56H8KSkyMj7k/img.png&quot; data-alt=&quot;아래 세 개를 설치&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daPNfD/btsFn8wJ1Od/LDX8Ogpkmq56H8KSkyMj7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaPNfD%2FbtsFn8wJ1Od%2FLDX8Ogpkmq56H8KSkyMj7k%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;600&quot; height=&quot;427&quot; data-filename=&quot;edited_Untitled.png&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아래 세 개를 설치&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. vscode 단축키 `ctrl/command + shift + p`를 입력하여 아래와 같은 검색창이 나타나도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_Untitled.png&quot; data-origin-width=&quot;2662&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQV9oo/btsFoeRgRE8/rGZpkRzLj4PifkRysHcLP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQV9oo/btsFoeRgRE8/rGZpkRzLj4PifkRysHcLP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQV9oo/btsFoeRgRE8/rGZpkRzLj4PifkRysHcLP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQV9oo%2FbtsFoeRgRE8%2FrGZpkRzLj4PifkRysHcLP1%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;600&quot; height=&quot;167&quot; data-filename=&quot;edited_edited_Untitled.png&quot; data-origin-width=&quot;2662&quot; data-origin-height=&quot;740&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;span style=&quot;color: #000000;&quot;&gt;3. 검색창에서 &quot;&lt;i&gt;open ssh configuration file&lt;/i&gt;&quot;을 입력하여 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 클릭하면 아래와 같은 화면이 나온다. 아래 화면에서 채워야 할 요소들이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_Untitled.png&quot; data-origin-width=&quot;2204&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/57g6y/btsFmjMi7i2/9uczF5nTcg504BpjRXOFf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/57g6y/btsFmjMi7i2/9uczF5nTcg504BpjRXOFf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/57g6y/btsFmjMi7i2/9uczF5nTcg504BpjRXOFf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F57g6y%2FbtsFmjMi7i2%2F9uczF5nTcg504BpjRXOFf1%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;600&quot; height=&quot;183&quot; data-filename=&quot;edited_edited_Untitled.png&quot; data-origin-width=&quot;2204&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1709033097243&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Host {vscode에서 접속할 인스턴스의 이름}
    HostName {EC2 인스턴스의 &amp;ldquo;퍼블릭 IPv4 DNS&amp;rdquo; 또는 &amp;ldquo;퍼블릿 IPv4 주소&amp;rdquo;}
    User {별다른 설정이 없다면, ubuntu로 적기}
    IdentityFile {인스턴스 생성 시 같이 생성한 키페어(.pem) File 위치}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 위와 같이 작성하고 저장한다. 다시 vscode 단축키 &lt;span style=&quot;text-align: start;&quot;&gt;`ctrl/command + shift + p`&lt;/span&gt;를 입력하여 검색창을 띄우고, &quot;&lt;i&gt;connect to host&lt;/i&gt;&quot;를 입력하여 클릭한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_Untitled.png&quot; data-origin-width=&quot;2858&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l42w5/btsFhiuAZvB/5iZFJH978YglEvtIGLKHK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l42w5/btsFhiuAZvB/5iZFJH978YglEvtIGLKHK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l42w5/btsFhiuAZvB/5iZFJH978YglEvtIGLKHK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl42w5%2FbtsFhiuAZvB%2F5iZFJH978YglEvtIGLKHK0%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;600&quot; height=&quot;46&quot; data-filename=&quot;edited_edited_Untitled.png&quot; data-origin-width=&quot;2858&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;  연결 시 &quot;&lt;span style=&quot;color: #953b34;&quot;&gt;Permission denied(Public Key)&lt;/span&gt;&quot;와 같은 에러가 발생하면 해당 `.pem` 파일의 권한을 `400`으로 바꾸자. 그리고 해당 파일의 위치가 `.ssh` 폴더 안이 아니면, 폴더 안으로 옮겨주자.&lt;/i&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1709033330429&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ chmod 400 ~/.ssh/{pem_key_name}.pem​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 클릭하여 연결 후, 에러가 없다면 성공!&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;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>AWS ec2</category>
      <category>ssh</category>
      <category>vsCode</category>
      <category>연결</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/154</guid>
      <comments>https://backend-jaamong.tistory.com/154#entry154comment</comments>
      <pubDate>Sat, 16 Mar 2024 08:39:01 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] Route 53으로 도메인 호스팅 하기</title>
      <link>https://backend-jaamong.tistory.com/152</link>
      <description>&lt;h2 style=&quot;color: #000000; padding: 2px 5px; font-weight: bold; background: linear-gradient(to top, #fdeb86 40%, transparent 40%); display: inline-block;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;AWS Route 53&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Amazon Route 53 은 가용성과 확장성이 뛰어난 &lt;b&gt;클라우드 Domain Name System (DNS) 웹 서비스&lt;/b&gt;이다. Route 53는 도메인 구입부터 네임서버 등록까지 DNS에 필요한 모든 기능이 있고, 모니터링 기능까지 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 도메인 등록 기관(가비아, 후이즈 등)에 비해 가격이 비슷하거나 저렴하고, 등록 외에 부가적인 기능 제공 및 안정성, GUI를 제공해 관리가 수월하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 Route 53은 사용자의 요청을 Amazon EC2 인스턴스, Elastic Load Balancing 로드 밸런서, Amazon S3 버킷 등 AWS에서 실행되는 인프라에 효과적으로 연결하고, AWS 외부의 인프라로 라우팅 하는데도 사용이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Route 53으로 도메인 호스팅 하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote style=&quot;font-size: 13px;&quot; data-ke-style=&quot;style2&quot;&gt;  &lt;i&gt;하기 전에 도메인은 Route 53에서 구입하거나, 다른 곳에서 구입하여 준비한다.&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;AWS Route 53&quot; href=&quot;https://us-east-1.console.aws.amazon.com/route53/v2/home?region=ap-northeast-2#Dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS Route 53&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에 접속하여&amp;nbsp;&lt;b&gt;호스팅 영역&lt;/b&gt;을&amp;nbsp;클릭한 후 아래 화면에서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;호스팅 영역 생성&lt;/b&gt;&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-filename=&quot;edited_blob&quot; data-origin-width=&quot;2870&quot; data-origin-height=&quot;1278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clV6fF/btsFm33zBwq/8zkh3D5iTReTj2xXhfZbVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clV6fF/btsFm33zBwq/8zkh3D5iTReTj2xXhfZbVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clV6fF/btsFm33zBwq/8zkh3D5iTReTj2xXhfZbVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclV6fF%2FbtsFm33zBwq%2F8zkh3D5iTReTj2xXhfZbVk%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;600&quot; height=&quot;267&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2870&quot; data-origin-height=&quot;1278&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;span style=&quot;color: #000000;&quot;&gt;클릭 후 아래 화면에서 &lt;b&gt;도메인 이름&lt;/b&gt;을 입력한다. 유형은 &lt;b&gt;&lt;span style=&quot;background-color: #e7f3f8;&quot; data-token-index=&quot;1&quot;&gt;퍼블릭 호스팅 영역&lt;/span&gt;&lt;/b&gt;을 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUgJim/btsFm4H9PyC/6B0jrjOFUPKR6ZqQBpqskk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUgJim/btsFm4H9PyC/6B0jrjOFUPKR6ZqQBpqskk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUgJim/btsFm4H9PyC/6B0jrjOFUPKR6ZqQBpqskk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUgJim%2FbtsFm4H9PyC%2F6B0jrjOFUPKR6ZqQBpqskk%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;600&quot; height=&quot;337&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1270&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;호스탱 영역 생성&lt;/b&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;834&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EQsS2/btsFmfb5GGg/cDDGk3juXhJG2zA6SSC2TK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EQsS2/btsFmfb5GGg/cDDGk3juXhJG2zA6SSC2TK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EQsS2/btsFmfb5GGg/cDDGk3juXhJG2zA6SSC2TK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEQsS2%2FbtsFmfb5GGg%2FcDDGk3juXhJG2zA6SSC2TK%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;600&quot; height=&quot;248&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2018&quot; data-origin-height=&quot;834&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;span style=&quot;color: #000000;&quot;&gt;생성이 완료되면 아래와 같은 화면이 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2872&quot; data-origin-height=&quot;1284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lADcp/btsFm06MJcj/4zj5PhXEeKysTXlWVmBhNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lADcp/btsFm06MJcj/4zj5PhXEeKysTXlWVmBhNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lADcp/btsFm06MJcj/4zj5PhXEeKysTXlWVmBhNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlADcp%2FbtsFm06MJcj%2F4zj5PhXEeKysTXlWVmBhNk%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;600&quot; height=&quot;268&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2872&quot; data-origin-height=&quot;1284&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;span style=&quot;color: #000000;&quot;&gt;위 화면의 유형 &lt;b&gt;NS&lt;/b&gt;(Name Server)에 해당하는 값/트래픽 라우팅 대상 4개의 값이 모두 필요하다. 해당 값들을 도메인을 구입한 곳에서 네임 서버(NS)로 등록한다. 네임서버 등록 시 맨 뒤에 `.`은 들어가지 않으니 주의!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다시 AWS Route 53으로 돌아와서, 아래 화면에서 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;레코드 생성&lt;/b&gt;&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-filename=&quot;edited_blob&quot; data-origin-width=&quot;2866&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUwYNg/btsFofJpeH2/tA4W1cPjwE7kexZuRy2neK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUwYNg/btsFofJpeH2/tA4W1cPjwE7kexZuRy2neK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUwYNg/btsFofJpeH2/tA4W1cPjwE7kexZuRy2neK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUwYNg%2FbtsFofJpeH2%2FtA4W1cPjwE7kexZuRy2neK%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;600&quot; height=&quot;243&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2866&quot; data-origin-height=&quot;1160&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;레코드 유형&lt;/b&gt;을 선택하고 &lt;b&gt;값&lt;/b&gt;을 채워준다. 다 입력했으면 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;레코드 생성&lt;/b&gt;&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-filename=&quot;edited_blob&quot; data-origin-width=&quot;2330&quot; data-origin-height=&quot;1138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TRxFp/btsFoHlnikV/9HTaIk9aedfJOcX6Ztzmi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TRxFp/btsFoHlnikV/9HTaIk9aedfJOcX6Ztzmi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TRxFp/btsFoHlnikV/9HTaIk9aedfJOcX6Ztzmi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTRxFp%2FbtsFoHlnikV%2F9HTaIk9aedfJOcX6Ztzmi1%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;600&quot; height=&quot;293&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2330&quot; data-origin-height=&quot;1138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  &lt;i&gt;&lt;span style=&quot;color: #000000; font-size: 13px;&quot;&gt;레코드 유형은 위 화면과 같이 `A-IPv4 주소 및 일부 AWS 리소스로 트래픽 라우팅`을 선택하고, 값에는 EC2 인스턴스의 `퍼블릭 IPv4 주소` 또는 할당받은 `탄력적 IP 주소`를 입력한다.&lt;/span&gt;&lt;/i&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;끝났다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2Z7Ks/btsFjHHcfze/NyOMcghslUljBTUCPuOwAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2Z7Ks/btsFjHHcfze/NyOMcghslUljBTUCPuOwAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2Z7Ks/btsFjHHcfze/NyOMcghslUljBTUCPuOwAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2Z7Ks%2FbtsFjHHcfze%2FNyOMcghslUljBTUCPuOwAK%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;600&quot; height=&quot;274&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;2042&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture &amp;amp; Tool/AWS</category>
      <category>AWS</category>
      <category>route 53</category>
      <category>도메인 호스팅</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/152</guid>
      <comments>https://backend-jaamong.tistory.com/152#entry152comment</comments>
      <pubDate>Sat, 9 Mar 2024 09:23:11 +0900</pubDate>
    </item>
    <item>
      <title>Ubuntu 환경에서 특정 버전의 파이썬 제거하기</title>
      <link>https://backend-jaamong.tistory.com/155</link>
      <description>&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선 삭제 전에 가상환경을 만들어 놨다면 비활성화를 한 후, 가상환경을 제거한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356517369&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ deactivate 
$ rm -rf {가상환경이름}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설치했던 파이썬의 위치를 확인해야 한다. 보통 `/usr/local/bin`에 위치한다. 아래 명령어로 특정 파이썬 버전의 디렉터리가 존재하는지 확인한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356517369&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ls -al /usr/local/bin&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;존재한다면 아래 명령어로 제거한다. 또한 파이썬과 함께 해당 버전을 가진 이름의 디렉토리가 있다면 해당 디렉터리 및 파일도 제거한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356517369&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo rm /usr/local/bin/{파이썬 버전} 
$ sudo rm /usr/local/bin/python3.8&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파이썬 관련 디렉토리 또한 삭제해야 한다. 보통 `/usr/local/lib`에 위치한다. 3번과 마찬가지로 파이썬과 함께 해당 버전을 가진 이름의 디렉토리 및 파일이 있다면 제거한다.&lt;/span&gt;
&lt;pre id=&quot;code_1709356517369&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo rm -rf /usr/local/lib/{파이썬 버전} 
$ sudo rm -rf /usr/local/lib/python3.8&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;</description>
      <category>Python</category>
      <category>PYTHON</category>
      <category>ubuntu</category>
      <category>제거</category>
      <author>jaamong</author>
      <guid isPermaLink="true">https://backend-jaamong.tistory.com/155</guid>
      <comments>https://backend-jaamong.tistory.com/155#entry155comment</comments>
      <pubDate>Fri, 8 Mar 2024 21:21:31 +0900</pubDate>
    </item>
  </channel>
</rss>